From 4aaea025df1f1a393c6577a7357a95f17e34fd76 Mon Sep 17 00:00:00 2001 From: lapl1412 Date: Fri, 15 Nov 2024 12:04:49 +0700 Subject: [PATCH 1/7] handle language manage, translate message functions, bump firebase package version --- android/app/build.gradle | 2 +- functions/index.js | 142 ++++++----- .../language_cubit.dart/language_cubit.dart | 37 +++ .../language_cubit.dart/language_state.dart | 22 ++ lib/_shared/data/chat_data.dart | 39 ++- lib/_shared/data/country.dart | 23 ++ lib/_shared/data/language.dart | 93 ++++++++ lib/_shared/widgets/chat_bubble_widget.dart | 76 ++++-- .../widgets/language_button_widget.dart | 68 ++++++ lib/features/chat/ui/public_chat_screen.dart | 149 +++++++----- .../bloc/language_manage_bloc.dart | 130 ++++++++++ .../bloc/language_manage_event.dart | 39 +++ .../bloc/language_manage_state.dart | 55 +++++ .../ui/language_manage_screen.dart | 225 ++++++++++++++++++ .../ui/widgets/language_item.dart | 64 +++++ lib/features/login/bloc/login_cubit.dart | 65 +++-- lib/features/login/ui/login_screen.dart | 13 +- lib/l10n/app_en.arb | 7 +- lib/l10n/app_vi.arb | 8 +- lib/main.dart | 63 ++++- lib/network/apis/language_api.dart | 15 ++ lib/network/services/language_services.dart | 42 ++++ lib/repository/database.dart | 21 +- pubspec.lock | 92 +++---- pubspec.yaml | 12 +- 25 files changed, 1232 insertions(+), 270 deletions(-) create mode 100644 lib/_shared/bloc/language_cubit.dart/language_cubit.dart create mode 100644 lib/_shared/bloc/language_cubit.dart/language_state.dart create mode 100644 lib/_shared/data/country.dart create mode 100644 lib/_shared/data/language.dart create mode 100644 lib/_shared/widgets/language_button_widget.dart create mode 100644 lib/features/language_manage/bloc/language_manage_bloc.dart create mode 100644 lib/features/language_manage/bloc/language_manage_event.dart create mode 100644 lib/features/language_manage/bloc/language_manage_state.dart create mode 100644 lib/features/language_manage/ui/language_manage_screen.dart create mode 100644 lib/features/language_manage/ui/widgets/language_item.dart create mode 100644 lib/network/apis/language_api.dart create mode 100644 lib/network/services/language_services.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 6247446..e734054 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -48,7 +48,7 @@ android { applicationId "com.suesitran.publicchat" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 23 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/functions/index.js b/functions/index.js index 5c15131..4dbd3d0 100644 --- a/functions/index.js +++ b/functions/index.js @@ -1,95 +1,93 @@ -/** - * Import function triggers from their respective submodules: - * - * const {onCall} = require("firebase-functions/v2/https"); - * const {onDocumentWritten} = require("firebase-functions/v2/firestore"); - * - * See a full list of supported triggers at https://firebase.google.com/docs/functions - */ const v2 = require("firebase-functions/v2"); const vertexAIApi = require("@google-cloud/vertexai"); +const admin = require("firebase-admin"); -// Create and deploy your first functions -// https://firebase.google.com/docs/functions/get-started +admin.initializeApp(); const project = 'proj-atc'; const location = 'us-central1'; -const textModel = 'gemini-1.5-flash'; +const textModel = 'gemini-1.5-flash'; const visionModel = 'gemini-1.0-pro-vision'; -const vertexAI = new vertexAIApi.VertexAI({project: project, location: location}); - -const generativeVisionModel = vertexAI.getGenerativeModel({ - model: visionModel, -}); +const vertexAI = new vertexAIApi.VertexAI({ project: project, location: location }); const generativeModelPreview = vertexAI.preview.getGenerativeModel({ - model: textModel, + model: textModel, }); -const generationConfig = { - temperature: 1, - topP: 0.95, - topK: 64, - maxOutputTokens: 8192, - responseMimeType: "application/json", - responseSchema: { - type: "object", - properties: { - en: { - type: "string" - } - }, - required: [ - "en" - ] - }, - }; - -// use onDocumentWritten here to prepare to "edit message" feature later -exports.onChatWritten = v2.firestore.onDocumentWritten("/public/{messageId}",async (event) => { - const document = event.data.after.data(); - const message = document["message"]; - console.log(`message: ${message}`); - - // no message? do nothing - if (message == undefined) { - return; - } - const curTranslated = document["translated"]; - - // check if message is translated - if (curTranslated != undefined) { - // message is translated before, - // check the original message - const original = curTranslated["original"]; - - console.log('Original: ', original); - // message is same as original, meaning it's already translated. Do nothing - if (message == original) { - return; - } +// trigger when the chat data on public collection change to translate the message +exports.onChatWritten_001 = v2.firestore.onDocumentWritten("/public/{messageId}", async (event) => { + const document = event.data.after.data(); + const message = document["message"]; + + console.log(`message: ${message}`); + + // no message? do nothing + if (message == undefined) { + return; + } + + const curTranslated = document["translated"]; + + // check if message is translated + if (curTranslated != undefined) { + // message is translated before, + // check the original message + const original = curTranslated["original"]; + + console.log('Original: ', original); + // message is same as original, meaning it's already translated. Do nothing + if (message == original) { + return; } + } + + //get all languages list to translate the message + const db = admin.firestore(); + const languagesCollection = db.collection("languages"); + const languages = await languagesCollection.get(); + const languageCodes = languages.docs.map((e) => e.data().code) + + var translated = {} + + if (languageCodes.length > 0) { + const generationConfig = { + temperature: 1, + topP: 0.95, + topK: 64, + maxOutputTokens: 8192, + responseMimeType: "application/json", + responseSchema: { + type: "object", + properties: languageCodes.reduce((acc, lang) => { + acc[lang] = { type: "string" }; + return acc; + }, {}), + required: languageCodes + }, + }; const chatSession = generativeModelPreview.startChat({ - generationConfig: generationConfig + generationConfig: generationConfig }); - const result = await chatSession.sendMessage(`translate this text to English: ${message}`); + + const result = await chatSession.sendMessage(`translate this text [${languageCodes.join(", ")}]: ${message}`); const response = result.response; console.log('Response:', JSON.stringify(response)); const jsonTranslated = response.candidates[0].content.parts[0].text; console.log('translated json: ', jsonTranslated); // parse this json to get translated text out - const translated = JSON.parse(jsonTranslated); - console.log('final result: ', translated.en); - - // write to message - const data = event.data.after.data(); - return event.data.after.ref.set({ - 'translated': { - 'original':message, - 'en': translated.en - } - }, {merge: true}); + translated = JSON.parse(jsonTranslated); + console.log('final result: ', translated); + + } + + // write to message + return event.data.after.ref.set({ + 'translated': { + 'original': message, + ...translated, + } + }, { merge: true }); }) \ No newline at end of file diff --git a/lib/_shared/bloc/language_cubit.dart/language_cubit.dart b/lib/_shared/bloc/language_cubit.dart/language_cubit.dart new file mode 100644 index 0000000..ece05a9 --- /dev/null +++ b/lib/_shared/bloc/language_cubit.dart/language_cubit.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:public_chat/_shared/data/language.dart'; +import 'package:public_chat/repository/database.dart'; +import 'package:public_chat/service_locator/service_locator.dart'; +import 'package:public_chat/utils/bloc_extensions.dart'; +import 'package:public_chat/utils/locale_support.dart'; + +part 'language_state.dart'; + +class LanguageCubit extends Cubit { + LanguageCubit() : super(const LanguageState()); + + void init(BuildContext context) { + setAppLanguage(Language.fromMapping(code: context.locale.localeName)); + } + + void setAppLanguage(Language language) { + emitSafely(state.copyWith(appLanguage: language)); + } + + void setMessageLanguage(Language? language, {bool saveToDatabase = true}) { + if (saveToDatabase) { + final user = FirebaseAuth.instance.currentUser; + + if (user != null) { + final database = ServiceLocator.instance.get(); + database.saveUser(user, language); + if (language != null) database.addLanguage(language); + } + } + + emitSafely(state.copyWith(messageLanguage: language)); + } +} diff --git a/lib/_shared/bloc/language_cubit.dart/language_state.dart b/lib/_shared/bloc/language_cubit.dart/language_state.dart new file mode 100644 index 0000000..6e28f2f --- /dev/null +++ b/lib/_shared/bloc/language_cubit.dart/language_state.dart @@ -0,0 +1,22 @@ +part of 'language_cubit.dart'; + +final class LanguageState extends Equatable { + final Language? appLanguage; + final Language? messageLanguage; + + const LanguageState({ + this.appLanguage, + this.messageLanguage, + }); + + LanguageState copyWith({ + Language? appLanguage, + Language? messageLanguage, + }) => + LanguageState( + appLanguage: appLanguage ?? this.appLanguage, + messageLanguage: messageLanguage ?? this.messageLanguage); + + @override + List get props => [appLanguage, messageLanguage]; +} diff --git a/lib/_shared/data/chat_data.dart b/lib/_shared/data/chat_data.dart index e60861d..d3e8ee6 100644 --- a/lib/_shared/data/chat_data.dart +++ b/lib/_shared/data/chat_data.dart @@ -1,5 +1,7 @@ import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:equatable/equatable.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:public_chat/_shared/data/language.dart'; final class Message { final String id; @@ -23,20 +25,43 @@ final class Message { {'message': message, 'sender': sender, 'time': timestamp}; } -final class UserDetail { +final class UserDetail extends Equatable { final String displayName; final String? photoUrl; final String uid; + final Language? messageLanguage; - UserDetail.fromFirebaseUser(User user) + const UserDetail({ + required this.uid, + required this.displayName, + required this.photoUrl, + required this.messageLanguage, + }); + + UserDetail.fromFirebaseUser(User user, [this.messageLanguage]) : displayName = user.displayName ?? 'Unknown', photoUrl = user.photoURL, uid = user.uid; - UserDetail.fromMap(this.uid, Map map) - : displayName = map['displayName'], - photoUrl = map['photoUrl']; + factory UserDetail.fromMap(String uid, Map map) { + final messageLanguageData = map['messageLanguage']; - Map toMap() => - {'displayName': displayName, 'photoUrl': photoUrl}; + return UserDetail( + uid: uid, + displayName: map['displayName'], + photoUrl: map['photoUrl'], + messageLanguage: messageLanguageData is Map + ? Language.fromMap(messageLanguageData) + : null, + ); + } + + Map toMap() => { + 'displayName': displayName, + 'photoUrl': photoUrl, + if (messageLanguage != null) 'messageLanguage': messageLanguage!.toMap() + }; + + @override + List get props => [displayName, photoUrl, uid, messageLanguage]; } diff --git a/lib/_shared/data/country.dart b/lib/_shared/data/country.dart new file mode 100644 index 0000000..c5d437f --- /dev/null +++ b/lib/_shared/data/country.dart @@ -0,0 +1,23 @@ +import 'package:equatable/equatable.dart'; + +final class Country extends Equatable { + final String name; + final String code; + + const Country({required this.name, required this.code}); + + factory Country.fromMap(Map json) => Country( + code: json['name'], + name: json['code'], + ); + + factory Country.fromRestCountries(Map json) => Country( + code: json['cca2'], + name: json['name']['common'], + ); + + Map toMap() => {'name': name, 'code': code}; + + @override + List get props => [name, code]; +} diff --git a/lib/_shared/data/language.dart b/lib/_shared/data/language.dart new file mode 100644 index 0000000..1d0e296 --- /dev/null +++ b/lib/_shared/data/language.dart @@ -0,0 +1,93 @@ +import 'dart:ui'; + +import 'package:equatable/equatable.dart'; +import 'package:public_chat/_shared/data/country.dart'; +import 'package:public_chat/network/apis/language_api.dart'; + +final class Language extends Equatable { + final String code; + final String name; + final String? navigateName; + final List countries; + + static const _mappingLanguages = { + 'vi': Language( + code: 'vi', + name: 'Vietnamese', + navigateName: 'Tiếng Việt', + countries: [ + Country(name: 'Việt Nam', code: 'vn'), + ]), + 'en': Language( + code: 'en', + name: 'English', + navigateName: 'English', + countries: [ + Country( + name: 'United States', + code: 'us', + ) + ], + ), + }; + + const Language({ + required this.code, + required this.name, + required this.navigateName, + required this.countries, + }); + + Language copyWith({List? countries}) => Language( + code: code, + name: name, + navigateName: navigateName, + countries: countries ?? this.countries); + + factory Language.fromMapping({ + required String code, + String? name, + String? navigateName, + List? countries, + }) { + final mappingLanguage = _mappingLanguages[code]; + + return Language( + code: code, + name: name ?? mappingLanguage?.name ?? 'N/A', + navigateName: navigateName ?? mappingLanguage?.navigateName, + countries: countries ?? mappingLanguage?.countries ?? [], + ); + } + + String get showName => navigateName ?? name; + + factory Language.fromMap(Map data) { + final countriesData = data['countries']; + + return Language( + code: data['code'], + name: data['name'], + navigateName: data['navigateName'], + countries: countriesData is List + ? countriesData.map((e) => Country.fromMap(e)).toList() + : [], + ); + } + + Map toMap({bool importCountry = true}) => { + 'code': code, + 'name': name, + if (navigateName != null) 'navigateName': navigateName, + if (importCountry && countries.isNotEmpty) + 'countries': countries.map((e) => e.toMap()).toList(), + }; + + Locale get locale => Locale(code, countries.firstOrNull?.code); + + String get flagUrl => + LanguageApi.getLocaleFlagUrl(locale.countryCode ?? locale.languageCode); + + @override + List get props => [code, name, navigateName, countries]; +} diff --git a/lib/_shared/widgets/chat_bubble_widget.dart b/lib/_shared/widgets/chat_bubble_widget.dart index 1882ced..9d2c471 100644 --- a/lib/_shared/widgets/chat_bubble_widget.dart +++ b/lib/_shared/widgets/chat_bubble_widget.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_network/image_network.dart'; +import 'package:public_chat/_shared/bloc/language_cubit.dart/language_cubit.dart'; class ChatBubble extends StatelessWidget { final bool isMine; @@ -69,34 +71,56 @@ class ChatBubble extends StatelessWidget { .bodyMedium ?.copyWith(color: Colors.white), ), - // english version (if there is) + // translations version (if there is) if (translations.isNotEmpty) - ...translations.entries - .where( - (element) => element.key != 'original', - ) - .map( - (e) => Text.rich( - TextSpan(children: [ - TextSpan( - text: '${e.key} ', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith( - fontWeight: FontWeight.bold, - color: - isMine ? Colors.black87 : Colors.grey)), - TextSpan( - text: e.value, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - fontStyle: FontStyle.italic, - color: isMine ? Colors.black87 : Colors.grey), + BlocBuilder( + buildWhen: (previous, current) => + previous.messageLanguage != current.messageLanguage, + builder: (context, state) { + return Column( + children: translations.entries + .where( + (element) => + element.value != message && + element.key == + context + .read() + .state + .messageLanguage + ?.code, ) - ]), - textAlign: isMine ? TextAlign.right : TextAlign.left, - ), - ) + .map( + (e) => Text.rich( + TextSpan(children: [ + TextSpan( + text: '${e.key} ', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + fontWeight: FontWeight.bold, + color: isMine + ? Colors.black87 + : Colors.grey)), + TextSpan( + text: e.value, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + fontStyle: FontStyle.italic, + color: isMine + ? Colors.black87 + : Colors.grey), + ) + ]), + textAlign: isMine ? TextAlign.right : TextAlign.left, + ), + ) + .toList(), + ); + }, + ), ], ), )); diff --git a/lib/_shared/widgets/language_button_widget.dart b/lib/_shared/widgets/language_button_widget.dart new file mode 100644 index 0000000..abbe676 --- /dev/null +++ b/lib/_shared/widgets/language_button_widget.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:public_chat/_shared/bloc/language_cubit.dart/language_cubit.dart'; +import 'package:public_chat/features/language_manage/ui/language_manage_screen.dart'; + +class LanguageButtonWidget extends StatelessWidget { + final bool onlyAppLanguage; + + const LanguageButtonWidget({super.key, this.onlyAppLanguage = true}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final content = onlyAppLanguage + ? Row( + children: [ + if (state.appLanguage != null) + Image.network( + state.appLanguage!.flagUrl, + height: 18, + width: 28, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Container( + height: 18, + width: 28, + color: Colors.blue.shade200, + alignment: Alignment.center, + child: const Icon( + Icons.info, + size: 15, + ), + ), + ), + const SizedBox(width: 10), + Text(state.appLanguage?.code.toUpperCase() ?? 'N/A'), + ], + ) + : const Icon(Icons.settings); + + return Container( + height: 32, + width: onlyAppLanguage ? null : 40, + margin: const EdgeInsets.only(right: 12), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: onlyAppLanguage + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8))), + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + enableDrag: true, + backgroundColor: Colors.transparent, + builder: (context) => LanguageManageScreen( + onlyAppLanguage: onlyAppLanguage, + ), + ), + child: content, + ), + ); + }, + ); + } +} diff --git a/lib/features/chat/ui/public_chat_screen.dart b/lib/features/chat/ui/public_chat_screen.dart index ff74eaf..c8d50c2 100644 --- a/lib/features/chat/ui/public_chat_screen.dart +++ b/lib/features/chat/ui/public_chat_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:public_chat/_shared/bloc/user_manager/user_manager_cubit.dart'; import 'package:public_chat/_shared/data/chat_data.dart'; import 'package:public_chat/_shared/widgets/chat_bubble_widget.dart'; +import 'package:public_chat/_shared/widgets/language_button_widget.dart'; import 'package:public_chat/_shared/widgets/message_box_widget.dart'; import 'package:public_chat/features/chat/bloc/chat_cubit.dart'; import 'package:public_chat/utils/locale_support.dart'; @@ -15,78 +16,94 @@ class PublicChatScreen extends StatelessWidget { @override Widget build(BuildContext context) { - final User? user = FirebaseAuth.instance.currentUser; - return BlocProvider( create: (context) => ChatCubit(), - child: Scaffold( - appBar: AppBar( - title: Text(context.locale.publicRoomTitle), - ), - body: Column( - children: [ - Expanded( - child: Builder( - builder: (context) { - return FirestoreListView( - query: context.read().chatContent, - reverse: true, - itemBuilder: (BuildContext context, - QueryDocumentSnapshot doc) { - if (!doc.exists) { - return const SizedBox.shrink(); - } + child: const _PublicChatScreenBody(), + ); + } +} - final Message message = doc.data(); +class _PublicChatScreenBody extends StatelessWidget { + const _PublicChatScreenBody(); - return BlocProvider.value( - value: UserManagerCubit() - ..queryUserDetail(message.sender), - child: - BlocBuilder( - builder: (context, state) { - String? photoUrl; - String? displayName; + @override + Widget build(BuildContext context) { + final User? user = FirebaseAuth.instance.currentUser; - if (state is UserDetailState) { - photoUrl = state.photoUrl; - displayName = state.displayName; - } + return Scaffold( + appBar: AppBar( + title: Text(context.locale.publicRoomTitle), + actions: const [ + LanguageButtonWidget( + onlyAppLanguage: false, + ) + ], + ), + body: Column( + children: [ + Expanded( + child: Builder( + builder: (context) { + return FirestoreListView( + query: context.read().chatContent, + reverse: true, + itemBuilder: (BuildContext context, + QueryDocumentSnapshot doc) { + if (!doc.exists) { + return const SizedBox.shrink(); + } - return ChatBubble( - isMine: message.sender == user?.uid, - message: message.message, - photoUrl: photoUrl, - displayName: displayName, - translations: message.translations); - }, - ), - ); - }, - emptyBuilder: (context) => const Center( - child: Text( - 'No messages found. Send the first message now!'), - ), - loadingBuilder: (context) => const Center( - child: CircularProgressIndicator(), - ), - ); - }, - ), - ), - MessageBox( - onSendMessage: (value) { - if (user == null) { - // do nothing - return; - } - FirebaseFirestore.instance - .collection('public') - .add(Message(sender: user.uid, message: value).toMap()); + final Message message = doc.data(); + + //lapl1412: i think user need to cache to improve the performance + return BlocProvider.value( + value: UserManagerCubit() + ..queryUserDetail(message.sender), + child: BlocBuilder( + builder: (context, state) { + String? photoUrl; + String? displayName; + + if (state is UserDetailState) { + photoUrl = state.photoUrl; + displayName = state.displayName; + } + + return ChatBubble( + key: Key(message.id), + isMine: message.sender == user?.uid, + message: message.message, + photoUrl: photoUrl, + displayName: displayName, + translations: message.translations); + }, + ), + ); + }, + emptyBuilder: (context) => const Center( + child: Text( + 'No messages found. Send the first message now!'), + ), + loadingBuilder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); }, - ) - ], - )), - ); + ), + ), + MessageBox( + onSendMessage: (value) { + if (user == null) { + // do nothing + return; + } + + context + .read() + .sendChat(uid: user.uid, message: value); + }, + ) + ], + )); } } diff --git a/lib/features/language_manage/bloc/language_manage_bloc.dart b/lib/features/language_manage/bloc/language_manage_bloc.dart new file mode 100644 index 0000000..dd64347 --- /dev/null +++ b/lib/features/language_manage/bloc/language_manage_bloc.dart @@ -0,0 +1,130 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:public_chat/_shared/data/language.dart'; +import 'package:public_chat/network/services/language_services.dart'; +import 'package:public_chat/utils/bloc_extensions.dart'; + +part 'language_manage_event.dart'; +part 'language_manage_state.dart'; + +final class LanguageManageBloc + extends Bloc { + //cache language + static List? _languages; + + final searchTextController = TextEditingController(); + Timer? queryTimer; + + LanguageManageBloc() : super(const LanguageManageState()) { + on(_onInit); + on(_onSearchTextChanged); + on(_onSearch); + on(_onClearSearchText); + } + + @override + Future close() { + queryTimer?.cancel(); + + return super.close(); + } + + void _onInit( + LanguageManageInit event, Emitter emit) async { + emitSafely(state.copyWith( + appLanguage: event.appLanguage, + messageLanguage: event.messageLanguage, + appLanguages: event.supportedLocales + .map((e) => Language.fromMapping(code: e.languageCode)) + .toList())); + + if (!event.isFetchMessageLanguage) return; + + // fetch languages for chat + try { + if (_languages == null) { + var languages = await LanguageServices.fetchLanguages(); + + if (event.supportedLocales.isNotEmpty) { + final prioritizedLanguages = []; + final nonPrioritizedLanguages = []; + + for (final language in languages) { + event.supportedLocales.indexWhere( + (element) => language.code == element.languageCode) != + -1 + ? prioritizedLanguages.add(language) + : nonPrioritizedLanguages.add(language); + } + + languages = [...prioritizedLanguages, ...nonPrioritizedLanguages]; + } + + _languages = languages; + } + + emitSafely(state.copyWith( + status: LanguageFetchStatus.success, + messageLanguages: _languages, + languagesFiltered: _languages, + errorMessage: '')); + } catch (e) { + emitSafely(state.copyWith( + status: LanguageFetchStatus.failed, errorMessage: e.toString())); + } + } + + void _onSearchTextChanged(LanguageManageSearchTextChanged event, + Emitter emit) { + emitSafely( + state.copyWith(searchText: event.searchText.trim().toLowerCase())); + + queryTimer?.cancel(); + queryTimer = Timer( + const Duration(milliseconds: 500), + () { + add(LanguageManageSearch()); + }, + ); + } + + void _onClearSearchText( + LanguageManageClearSearchText event, Emitter emit) { + queryTimer?.cancel(); + searchTextController.clear(); + emitSafely(state.copyWith( + languagesFiltered: state.messageLanguages, searchText: '')); + } + + void _onSearch( + LanguageManageSearch event, Emitter emit) { + bool isContainText(List keys, String query) { + var isContain = false; + + for (final key in keys) { + if (key != null) isContain = key.toLowerCase().contains(query); + if (isContain) break; + } + + return isContain; + } + + final languageFiltered = []; + + for (final language in state.messageLanguages) { + if (isContainText([ + language.name, + language.navigateName, + language.code, + ...language.countries.map((e) => e.name) + ], state.searchText)) { + languageFiltered.add(language); + } + } + + emitSafely(state.copyWith(languagesFiltered: languageFiltered)); + } +} diff --git a/lib/features/language_manage/bloc/language_manage_event.dart b/lib/features/language_manage/bloc/language_manage_event.dart new file mode 100644 index 0000000..c0fd25b --- /dev/null +++ b/lib/features/language_manage/bloc/language_manage_event.dart @@ -0,0 +1,39 @@ +part of 'language_manage_bloc.dart'; + +sealed class LanguageManageEvent extends Equatable { + const LanguageManageEvent(); + + @override + List get props => []; +} + +final class LanguageManageInit extends LanguageManageEvent { + final bool isFetchMessageLanguage; + final List supportedLocales; + final Language? appLanguage; + final Language? messageLanguage; + + const LanguageManageInit({ + required this.isFetchMessageLanguage, + required this.supportedLocales, + required this.appLanguage, + required this.messageLanguage, + }); + + @override + List get props => + [isFetchMessageLanguage, supportedLocales, appLanguage, messageLanguage]; +} + +final class LanguageManageSearch extends LanguageManageEvent {} + +final class LanguageManageClearSearchText extends LanguageManageEvent {} + +final class LanguageManageSearchTextChanged extends LanguageManageEvent { + final String searchText; + + const LanguageManageSearchTextChanged({required this.searchText}); + + @override + List get props => [searchText]; +} diff --git a/lib/features/language_manage/bloc/language_manage_state.dart b/lib/features/language_manage/bloc/language_manage_state.dart new file mode 100644 index 0000000..22b24d6 --- /dev/null +++ b/lib/features/language_manage/bloc/language_manage_state.dart @@ -0,0 +1,55 @@ +part of 'language_manage_bloc.dart'; + +final class LanguageManageState extends Equatable { + final LanguageFetchStatus status; + final List appLanguages, messageLanguages, languagesFiltered; + final String errorMessage, searchText; + final Language? appLanguage; + final Language? messageLanguage; + + const LanguageManageState({ + this.status = LanguageFetchStatus.initial, + this.appLanguages = const [], + this.messageLanguages = const [], + this.languagesFiltered = const [], + this.errorMessage = '', + this.searchText = '', + this.appLanguage, + this.messageLanguage, + }); + + LanguageManageState copyWith({ + LanguageFetchStatus? status, + List? appLanguages, + List? messageLanguages, + List? languagesFiltered, + String? errorMessage, + String? searchText, + Language? appLanguage, + Language? messageLanguage, + }) => + LanguageManageState( + status: status ?? this.status, + appLanguages: appLanguages ?? this.appLanguages, + messageLanguages: messageLanguages ?? this.messageLanguages, + languagesFiltered: languagesFiltered ?? this.languagesFiltered, + errorMessage: errorMessage ?? this.errorMessage, + searchText: searchText ?? this.searchText, + appLanguage: appLanguage ?? this.appLanguage, + messageLanguage: messageLanguage ?? this.messageLanguage, + ); + + @override + List get props => [ + status, + appLanguages, + messageLanguages, + languagesFiltered, + errorMessage, + searchText, + appLanguage, + messageLanguage, + ]; +} + +enum LanguageFetchStatus { initial, success, failed } diff --git a/lib/features/language_manage/ui/language_manage_screen.dart b/lib/features/language_manage/ui/language_manage_screen.dart new file mode 100644 index 0000000..5f302db --- /dev/null +++ b/lib/features/language_manage/ui/language_manage_screen.dart @@ -0,0 +1,225 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:public_chat/_shared/bloc/language_cubit.dart/language_cubit.dart'; +import 'package:public_chat/features/language_manage/bloc/language_manage_bloc.dart'; +import 'package:public_chat/features/language_manage/ui/widgets/language_item.dart'; +import 'package:public_chat/utils/locale_support.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class LanguageManageScreen extends StatelessWidget { + final bool onlyAppLanguage; + + const LanguageManageScreen({super.key, required this.onlyAppLanguage}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => LanguageManageBloc() + ..add(LanguageManageInit( + isFetchMessageLanguage: !onlyAppLanguage, + appLanguage: context.read().state.appLanguage, + messageLanguage: + context.read().state.messageLanguage, + supportedLocales: AppLocalizations.supportedLocales)), + child: _LanguageManageBody(onlyAppLanguage), + ); + } +} + +class _LanguageManageBody extends StatelessWidget { + final bool onlyAppLanguage; + + const _LanguageManageBody(this.onlyAppLanguage); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final minChildSize = onlyAppLanguage ? 0.5 : 0.7; + + return DraggableScrollableSheet( + initialChildSize: minChildSize, + minChildSize: minChildSize, + maxChildSize: 0.9, + snap: true, + builder: (context, scrollController) { + Widget tabHeaderBuilder(String title, String? value) => Tab( + icon: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.w600), + ), + if (value != null) + Text( + "($value)", + style: const TextStyle(fontSize: 14), + ), + ], + ), + ); + + final appLanguages = _AppLanguagesScreen( + state: state, scrollController: scrollController); + final messageLanguages = _MessageLanguagesScreen( + state: state, scrollController: scrollController); + + final tabBar = TabBar( + tabAlignment: + onlyAppLanguage ? TabAlignment.start : TabAlignment.center, + isScrollable: true, + tabs: [ + if (!onlyAppLanguage) + tabHeaderBuilder(context.locale.messageLanguage, + state.messageLanguage?.showName ?? 'N/A'), + tabHeaderBuilder( + context.locale.appLanguage, + onlyAppLanguage + ? null + : state.appLanguage?.showName ?? 'N/A'), + ], + ); + + return DefaultTabController( + length: tabBar.tabs.length, + child: Scaffold( + appBar: AppBar( + title: Text(context.locale.selectLanguage), + bottom: tabBar, + ), + body: onlyAppLanguage + ? appLanguages + : TabBarView( + children: [ + messageLanguages, + appLanguages, + ], + ), + ), + ); + }, + ); + }, + ); + } +} + +class _AppLanguagesScreen extends StatelessWidget { + final LanguageManageState state; + final ScrollController scrollController; + + const _AppLanguagesScreen( + {required this.state, required this.scrollController}); + + @override + Widget build(BuildContext context) { + return ListView.builder( + controller: scrollController, + itemCount: state.appLanguages.length, + itemBuilder: (context, index) { + final language = state.appLanguages[index]; + final isSelected = language.code == state.appLanguage?.code; + + return LanguageItem( + isSelected: isSelected, + index: index, + language: language, + onTap: (language) => + context.read().setAppLanguage(language), + ); + }, + ); + } +} + +class _MessageLanguagesScreen extends StatefulWidget { + final LanguageManageState state; + final ScrollController scrollController; + + const _MessageLanguagesScreen( + {required this.state, required this.scrollController}); + + @override + State<_MessageLanguagesScreen> createState() => + _MessageLanguagesScreenState(); +} + +class _MessageLanguagesScreenState extends State<_MessageLanguagesScreen> + with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + + final Widget body; + + switch (widget.state.status) { + case LanguageFetchStatus.initial: + body = const Center( + child: CircularProgressIndicator(), + ); + break; + case LanguageFetchStatus.failed: + body = Center(child: Text(context.locale.languagesLoadFailed)); + break; + case LanguageFetchStatus.success: + final searchBar = TextFormField( + controller: context.read().searchTextController, + onChanged: (value) => context + .read() + .add(LanguageManageSearchTextChanged(searchText: value)), + decoration: InputDecoration( + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.search), + suffixIcon: Visibility( + visible: widget.state.searchText.isNotEmpty, + child: IconButton( + onPressed: () => context + .read() + .add(LanguageManageClearSearchText()), + icon: const Icon(Icons.close), + color: Colors.red, + ), + ), + hintText: '${context.locale.searchLanguage}...', + ), + ); + + final languages = ListView.builder( + controller: widget.scrollController, + itemCount: widget.state.languagesFiltered.length, + itemBuilder: (context, index) { + final language = widget.state.languagesFiltered[index]; + final isSelected = + language.code == widget.state.messageLanguage?.code; + + return LanguageItem( + isSelected: isSelected, + index: index, + language: language, + onTap: (language) => + context.read().setMessageLanguage(language), + ); + }, + ); + + body = Column( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: searchBar), + Expanded(child: languages), + ], + ); + + break; + } + + return body; + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/features/language_manage/ui/widgets/language_item.dart b/lib/features/language_manage/ui/widgets/language_item.dart new file mode 100644 index 0000000..ed06631 --- /dev/null +++ b/lib/features/language_manage/ui/widgets/language_item.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:public_chat/_shared/data/language.dart'; + +class LanguageItem extends StatelessWidget { + final bool isSelected; + final int index; + final Language language; + final void Function(Language language) onTap; + + const LanguageItem({ + super.key, + required this.isSelected, + required this.index, + required this.language, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: isSelected + ? Colors.blue + : index.isEven + ? null + : Colors.blue.withOpacity(0.1), + elevation: 0, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero)), + onPressed: () { + Navigator.maybePop(context); + onTap(language); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + language.flagUrl, + height: 18, + width: 28, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Container( + height: 18, + width: 28, + color: Colors.blue.shade200, + alignment: Alignment.center, + child: const Icon( + Icons.info, + size: 15, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: Text( + language.navigateName ?? language.name, + style: TextStyle(color: isSelected ? Colors.white : null), + ), + ), + if (isSelected) const Icon(Icons.check, color: Colors.white), + ], + ), + ); + } +} diff --git a/lib/features/login/bloc/login_cubit.dart b/lib/features/login/bloc/login_cubit.dart index 0f0d8f1..19e6177 100644 --- a/lib/features/login/bloc/login_cubit.dart +++ b/lib/features/login/bloc/login_cubit.dart @@ -1,10 +1,12 @@ import 'dart:async'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:firebase_auth/firebase_auth.dart'; -import 'package:flutter/services.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:public_chat/_shared/bloc/language_cubit.dart/language_cubit.dart'; +import 'package:public_chat/_shared/data/chat_data.dart'; import 'package:public_chat/repository/database.dart'; import 'package:public_chat/service_locator/service_locator.dart'; import 'package:public_chat/utils/bloc_extensions.dart'; @@ -25,24 +27,35 @@ class LoginCubit extends Cubit { final GoogleSignIn googleSignIn = GoogleSignIn(); late final StreamSubscription userSubscription; - void requestLogin() async { + void requestLogin(BuildContext context) async { GoogleSignInAccount? googleUser; - try { - googleUser = await googleSignIn.signIn(); - } on PlatformException catch (e) { - emitSafely(LoginFailed(e.toString())); - return null; - } + // try { + googleUser = await googleSignIn.signIn(); + // } on PlatformException catch (e) { + // emitSafely(LoginFailed(e.toString())); + // return null; + // } if (googleUser == null) { emitSafely(const LoginFailed('User cancelled')); return null; } - await _authenticateToFirebase(googleUser); + _authenticateToFirebase(googleUser).then( + (userDetails) { + if (context.mounted) { + context.read().setMessageLanguage( + userDetails?.messageLanguage, + saveToDatabase: false); + } + }, + ); } - Future _authenticateToFirebase(GoogleSignInAccount googleUser) async { + Future _authenticateToFirebase( + GoogleSignInAccount googleUser) async { + UserDetail? userDetails; + final GoogleSignInAuthentication googleAuth = await googleUser.authentication; @@ -51,25 +64,27 @@ class LoginCubit extends Cubit { final FirebaseAuth firebaseAuth = FirebaseAuth.instance; final Database database = ServiceLocator.instance.get(); - try { - final UserCredential userCredential = - await firebaseAuth.signInWithCredential(oAuthCredential); - final User? user = userCredential.user; + // try { + final UserCredential userCredential = + await firebaseAuth.signInWithCredential(oAuthCredential); + final User? user = userCredential.user; - if (user == null) { - emitSafely(const LoginFailed('Unable to get user credential')); - return; - } + if (user == null) { + emitSafely(const LoginFailed('Unable to get user credential')); + } else { + userDetails = (await database.getUser(user.uid)).data(); + + userDetails ??= database.saveUser(user); - database.saveUser(user); emitSafely(LoginSuccess(user.displayName ?? 'Unknown display name')); - } on FirebaseAuthException catch (e) { - emitSafely(LoginFailed(e.toString())); - return; - } catch (e) { - emitSafely(LoginFailed(e.toString())); - return; } + // } on FirebaseAuthException catch (e) { + // emitSafely(LoginFailed(e.toString())); + // } catch (e) { + // emitSafely(LoginFailed(e.toString())); + // } + + return userDetails; } @override diff --git a/lib/features/login/ui/login_screen.dart b/lib/features/login/ui/login_screen.dart index 3629bec..37a154d 100644 --- a/lib/features/login/ui/login_screen.dart +++ b/lib/features/login/ui/login_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:public_chat/_shared/widgets/language_button_widget.dart'; import 'package:public_chat/features/chat/ui/public_chat_screen.dart'; import 'package:public_chat/features/login/bloc/login_cubit.dart'; import 'package:public_chat/features/login/ui/widgets/sign_in_button.dart'; @@ -39,17 +40,23 @@ class _LoginScreenBody extends StatelessWidget { buildSignInButton( label: context.locale.login, onPressed: () => - context.read().requestLogin(), + context.read().requestLogin(context), ) ], ) : buildSignInButton( label: context.locale.login, - onPressed: () => context.read().requestLogin(), + onPressed: () => + context.read().requestLogin(context), ); return Scaffold( - body: Center(child: content), + appBar: AppBar( + actions: const [LanguageButtonWidget()], + ), + body: Center( + child: content, + ), ); }, ); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 206f913..530e9c2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,4 +1,9 @@ { "login":"Login", - "publicRoomTitle": "Public Room" + "publicRoomTitle": "Public Room", + "selectLanguage": "Select language", + "searchLanguage": "Input language, country", + "languagesLoadFailed": "Failed to load languages", + "appLanguage": "Application language", + "messageLanguage": "Message language" } \ No newline at end of file diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb index 6197665..b980507 100644 --- a/lib/l10n/app_vi.arb +++ b/lib/l10n/app_vi.arb @@ -1,5 +1,9 @@ { "login":"Đăng nhập", - "publicRoomTitle": "Phòng chat công cộng" - + "publicRoomTitle": "Phòng chat công cộng", + "selectLanguage": "Chọn ngôn ngữ", + "searchLanguage": "Nhập ngôn ngữ, quốc gia", + "languagesLoadFailed": "Xảy ra lỗi khi tải ngôn ngữ", + "appLanguage": "Ngôn ngữ ứng dụng", + "messageLanguage": "Ngôn ngữ tin nhắn" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 4738fa9..105dc7d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,10 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:public_chat/_shared/bloc/language_cubit.dart/language_cubit.dart'; import 'package:public_chat/features/genai_setting/bloc/genai_bloc.dart'; import 'package:public_chat/features/login/ui/login_screen.dart'; -import 'package:public_chat/firebase_options.dart'; +// import 'package:public_chat/firebase_options.dart'; import 'package:public_chat/service_locator/service_locator.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -11,11 +12,15 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + ServiceLocator.instance.initialise(); - runApp(BlocProvider( - create: (context) => GenaiBloc(), + runApp(MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => GenaiBloc()), + BlocProvider(create: (context) => LanguageCubit()) + ], child: const MainApp(), )); } @@ -25,14 +30,46 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( - localizationsDelegates: [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: AppLocalizations.supportedLocales, - home: LoginScreen()); + return BlocBuilder( + buildWhen: (previous, current) => + previous.appLanguage != current.appLanguage, + builder: (context, state) { + return MaterialApp( + locale: state.appLanguage?.locale, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + home: const _Home()); + }, + ); + } +} + +class _Home extends StatefulWidget { + const _Home(); + + @override + State<_Home> createState() => __HomeWState(); +} + +class __HomeWState extends State<_Home> { + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback( + (timeStamp) { + context.read().init(context); + }, + ); + } + + @override + Widget build(BuildContext context) { + return const LoginScreen(); } } diff --git a/lib/network/apis/language_api.dart b/lib/network/apis/language_api.dart new file mode 100644 index 0000000..5b4dc34 --- /dev/null +++ b/lib/network/apis/language_api.dart @@ -0,0 +1,15 @@ +import 'package:http/http.dart' as http; + +final class LanguageApi { + LanguageApi._(); + + static Future fetchLanguages() { + final url = Uri.parse('https://restcountries.com/v3.1/all'); + + return http.get(url); + } + + static String getLocaleFlagUrl(String countryCode) { + return 'https://flagcdn.com/24x18/${countryCode.toLowerCase()}.png'; + } +} diff --git a/lib/network/services/language_services.dart b/lib/network/services/language_services.dart new file mode 100644 index 0000000..224e13c --- /dev/null +++ b/lib/network/services/language_services.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +import 'package:public_chat/_shared/data/country.dart'; +import 'package:public_chat/_shared/data/language.dart'; +import 'package:public_chat/network/apis/language_api.dart'; + +final class LanguageServices { + LanguageServices._(); + + static Future> fetchLanguages() async { + final response = await LanguageApi.fetchLanguages(); + + if (response != null && response.statusCode == 200) { + final result = {}; + + final List countriesData = jsonDecode(response.body); + + for (final countryJson in countriesData) { + final country = Country.fromRestCountries(countryJson); + final languages = (countryJson['languages'] as Map?)?.entries.map((e) { + final String code = e.key; + return Language.fromMapping( + code: code.length > 2 ? code.substring(0, 2) : code, + name: e.value); + }) ?? + {}; + + for (final language in languages) { + result[language.code] = + (result[language.code] ?? language).copyWith(countries: [ + ...(result[language.code]?.countries ?? []), + country, + ]); + } + } + + return result.values.toList(); + } else { + throw Exception('Failed to load languages: ${response?.body}'); + } + } +} diff --git a/lib/repository/database.dart b/lib/repository/database.dart index b1346b8..b1dcbfa 100644 --- a/lib/repository/database.dart +++ b/lib/repository/database.dart @@ -1,6 +1,7 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:public_chat/_shared/data/chat_data.dart'; +import 'package:public_chat/_shared/data/language.dart'; final class Database { static Database? _instance; @@ -14,6 +15,8 @@ final class Database { final String _publicRoom = 'public'; final String _userList = 'users'; + final String _languageList = 'languages'; + void writePublicMessage(Message message) { FirebaseFirestore.instance.collection(_publicRoom).add(message.toMap()); } @@ -28,12 +31,15 @@ final class Database { .withConverter(fromFirestore: fromFirestore, toFirestore: toFirestore); } - void saveUser(User user) { - final UserDetail userDetail = UserDetail.fromFirebaseUser(user); + UserDetail saveUser(User user, [Language? language]) { + final UserDetail userDetail = UserDetail.fromFirebaseUser(user, language); + FirebaseFirestore.instance .collection(_userList) .doc(user.uid) .set(userDetail.toMap(), SetOptions(merge: true)); + + return userDetail; } Future> getUser(String uid) { @@ -55,6 +61,17 @@ final class Database { .snapshots(); } + void addLanguage(Language language) async { + final collection = FirebaseFirestore.instance.collection(_languageList); + + final existingLanguage = + await collection.where('code', isEqualTo: language.code).get(); + + if (existingLanguage.docs.isEmpty) { + collection.add(language.toMap(importCountry: false)); + } + } + /// ############################################################### /// fromFirestore and toFirestore UserDetail _userDetailFromFirestore( diff --git a/pubspec.lock b/pubspec.lock index 119efcd..25b752b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7" + sha256: "71c01c1998c40b3af1944ad0a5f374b4e6fef7f3d2df487f3970dbeadaeb25a1" url: "https://pub.dev" source: hosted - version: "1.3.35" + version: "1.3.46" analyzer: dependency: transitive description: @@ -85,26 +85,26 @@ packages: dependency: "direct main" description: name: cloud_firestore - sha256: a0f161b92610e078b4962d7e6ebeb66dc9cce0ada3514aeee442f68165d78185 + sha256: d5b73c5f27d95504e45d96e793fe9f9daa62e42b22da85b9f8de4916f46e916d url: "https://pub.dev" source: hosted - version: "4.17.5" + version: "5.5.0" cloud_firestore_platform_interface: dependency: transitive description: name: cloud_firestore_platform_interface - sha256: "6a55b319f8d33c307396b9104512e8130a61904528ab7bd8b5402678fca54b81" + sha256: "8d5a3a501f3b21638199819ab76709d3b87abc9a565ed611b13a238611cfba27" url: "https://pub.dev" source: hosted - version: "6.2.5" + version: "6.5.0" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web - sha256: "89dfa1304d3da48b3039abbb2865e3d30896ef858e569a16804a99f4362283a9" + sha256: "24f302e4ffd88ec2891e33f432b53f59ebd072664c5dc804b7fcbc51ea22b4b3" url: "https://pub.dev" source: hosted - version: "3.12.5" + version: "4.3.4" collection: dependency: transitive description: @@ -173,66 +173,66 @@ packages: dependency: "direct main" description: name: firebase_auth - sha256: cfc2d970829202eca09e2896f0a5aa7c87302817ecc0bdfa954f026046bf10ba + sha256: "49c356bac95ed234805e3bb928a86d5b21a4d3745d77be53ecf2d61409ddb802" url: "https://pub.dev" source: hosted - version: "4.20.0" + version: "5.3.3" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: a0270e1db3b2098a14cb2a2342b3cd2e7e458e0c391b1f64f6f78b14296ec093 + sha256: "9bc336ce673ea90a9dbdb04f0e9a3e52a32321898dc869cdefe6cc0f0db369ed" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.4.9" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: "64e067e763c6378b7e774e872f0f59f6812885e43020e25cde08f42e9459837b" + sha256: "56dcce4293e2a2c648c33ab72c09e888bd0e64cbb1681a32575ec9dc9c2f67f3" url: "https://pub.dev" source: hosted - version: "5.12.0" + version: "5.13.4" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c" + sha256: "2438a75ad803e818ad3bd5df49137ee619c46b6fc7101f4dbc23da07305ce553" url: "https://pub.dev" source: hosted - version: "2.32.0" + version: "3.8.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "1003a5a03a61fc9a22ef49f37cbcb9e46c86313a7b2e7029b9390cf8c6fc32cb" + sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.3.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "23509cb3cddfb3c910c143279ac3f07f06d3120f7d835e4a5d4b42558e978712" + sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5 url: "https://pub.dev" source: hosted - version: "2.17.3" + version: "2.18.1" firebase_ui_firestore: dependency: "direct main" description: name: firebase_ui_firestore - sha256: "4775cab02e196adb883ff4115c11c2ad535a955447dc8aa73f3b932c700e75df" + sha256: "9492b9f989457a05e21c47d006beb4cef09be1c56d0fe8df7158f9b056be63a9" url: "https://pub.dev" source: hosted - version: "1.6.3" + version: "1.7.0" firebase_ui_localizations: dependency: transitive description: name: firebase_ui_localizations - sha256: a7faa62e2d56cb38aae270a8f05c1a8518b04b06dd0f0cc2d4974e4b4782de1c + sha256: "7c0d9f59a7f8dfe728019bc7ac5dc85e119f59ec9ede2e01b128b39b8492090b" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.13.0" firebase_ui_shared: dependency: transitive description: @@ -377,10 +377,10 @@ packages: dependency: transitive description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -433,18 +433,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -481,18 +481,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -726,26 +726,26 @@ packages: dependency: transitive description: name: test - sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" url: "https://pub.dev" source: hosted - version: "1.25.2" + version: "1.25.7" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" test_core: dependency: transitive description: name: test_core - sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.4" typed_data: dependency: transitive description: @@ -798,10 +798,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: @@ -814,18 +814,18 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.0" webkit_inspection_protocol: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b74a4eb..5ddecd6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,18 +1,18 @@ name: public_chat description: "A Public Chat app to chat with Gemini." -publish_to: 'none' +publish_to: "none" version: 0.1.0 environment: - sdk: '>=3.3.3 <4.0.0' + sdk: ">=3.3.3 <4.0.0" dependencies: bloc_test: ^9.1.7 - cloud_firestore: ^4.17.5 + cloud_firestore: ^5.5.0 equatable: ^2.0.5 - firebase_auth: ^4.16.0 - firebase_core: ^2.24.2 - firebase_ui_firestore: ^1.6.3 + firebase_auth: ^5.3.3 + firebase_core: ^3.8.0 + firebase_ui_firestore: ^1.7.0 flutter: sdk: flutter flutter_bloc: ^8.1.6 From 95b6221f94874194036f376aa1ad697e18812bd5 Mon Sep 17 00:00:00 2001 From: lapl1412 Date: Fri, 15 Nov 2024 12:11:30 +0700 Subject: [PATCH 2/7] fix missing init firebase app --- lib/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 105dc7d..a76ed87 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,7 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:public_chat/_shared/bloc/language_cubit.dart/language_cubit.dart'; import 'package:public_chat/features/genai_setting/bloc/genai_bloc.dart'; import 'package:public_chat/features/login/ui/login_screen.dart'; -// import 'package:public_chat/firebase_options.dart'; +import 'package:public_chat/firebase_options.dart'; import 'package:public_chat/service_locator/service_locator.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -12,7 +12,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - // await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); ServiceLocator.instance.initialise(); From 90683a4da0fa71eb174241a4492806772ff62d26 Mon Sep 17 00:00:00 2001 From: lapl1412 Date: Fri, 15 Nov 2024 13:17:22 +0700 Subject: [PATCH 3/7] do not import country data to message langauge json when save user data --- lib/_shared/data/chat_data.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/_shared/data/chat_data.dart b/lib/_shared/data/chat_data.dart index d3e8ee6..7f4713c 100644 --- a/lib/_shared/data/chat_data.dart +++ b/lib/_shared/data/chat_data.dart @@ -59,7 +59,8 @@ final class UserDetail extends Equatable { Map toMap() => { 'displayName': displayName, 'photoUrl': photoUrl, - if (messageLanguage != null) 'messageLanguage': messageLanguage!.toMap() + if (messageLanguage != null) + 'messageLanguage': messageLanguage!.toMap(importCountry: false) }; @override From 38876184739c28f1fd3a1010d961a0f559f815a0 Mon Sep 17 00:00:00 2001 From: lapl1412 Date: Fri, 15 Nov 2024 13:50:12 +0700 Subject: [PATCH 4/7] add http denpendency --- pubspec.lock | 2 +- pubspec.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index 25b752b..9c345bd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -374,7 +374,7 @@ packages: source: hosted version: "0.12.4+2" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 diff --git a/pubspec.yaml b/pubspec.yaml index 5ddecd6..5654dd7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: google_sign_in_web: ^0.12.4+2 image_network: ^2.5.6 intl: ^0.19.0 + http: ^1.2.2 dev_dependencies: flutter_test: From 821063cfd1b4a12718f509b7cc11b1a3cf5e5874 Mon Sep 17 00:00:00 2001 From: lapl1412 Date: Fri, 15 Nov 2024 13:59:28 +0700 Subject: [PATCH 5/7] update test for login screen --- test/features/login/ui/login_screen_test.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/features/login/ui/login_screen_test.dart b/test/features/login/ui/login_screen_test.dart index a09b065..b7585ba 100644 --- a/test/features/login/ui/login_screen_test.dart +++ b/test/features/login/ui/login_screen_test.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:public_chat/_shared/bloc/language_cubit.dart/language_cubit.dart'; import 'package:public_chat/features/login/ui/login_screen.dart'; import '../../../material_wrapper_extension.dart'; @@ -11,7 +13,9 @@ void main() { testWidgets( 'test en', (widgetTester) async { - const Widget widget = LoginScreen(); + Widget widget = BlocProvider( + create: (context) => LanguageCubit()..init(context), + child: const LoginScreen()); await widgetTester.wrapAndPump(widget, locale: const Locale('en')); @@ -22,7 +26,9 @@ void main() { testWidgets( 'test vi', (widgetTester) async { - const Widget widget = LoginScreen(); + Widget widget = BlocProvider( + create: (context) => LanguageCubit()..init(context), + child: const LoginScreen()); await widgetTester.wrapAndPump(widget, locale: const Locale('vi')); From 56889e509bdb8035f9df761487aea23ef88207c1 Mon Sep 17 00:00:00 2001 From: lapl1412 Date: Fri, 15 Nov 2024 14:05:18 +0700 Subject: [PATCH 6/7] update test for login screen --- test/features/login/ui/login_screen_test.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/features/login/ui/login_screen_test.dart b/test/features/login/ui/login_screen_test.dart index b7585ba..e1333d2 100644 --- a/test/features/login/ui/login_screen_test.dart +++ b/test/features/login/ui/login_screen_test.dart @@ -14,8 +14,7 @@ void main() { 'test en', (widgetTester) async { Widget widget = BlocProvider( - create: (context) => LanguageCubit()..init(context), - child: const LoginScreen()); + create: (context) => LanguageCubit(), child: const LoginScreen()); await widgetTester.wrapAndPump(widget, locale: const Locale('en')); @@ -27,8 +26,7 @@ void main() { 'test vi', (widgetTester) async { Widget widget = BlocProvider( - create: (context) => LanguageCubit()..init(context), - child: const LoginScreen()); + create: (context) => LanguageCubit(), child: const LoginScreen()); await widgetTester.wrapAndPump(widget, locale: const Locale('vi')); From aaf558fdbca699fe2c73a808fb8ed3c3c57fa1ae Mon Sep 17 00:00:00 2001 From: lapl1412 Date: Fri, 15 Nov 2024 14:47:13 +0700 Subject: [PATCH 7/7] update IPHONEOS_DEPLOYMENT_TARGET to version 15.0 to work with new firebase bump version --- ios/Runner.xcodeproj/project.pbxproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 2130b50..323fdcd 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -350,7 +350,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -477,7 +477,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -528,7 +528,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos;