From d3e9bdafbde52b0fd083d124d60771d2c649183a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 1 Jul 2021 20:26:01 +0530 Subject: [PATCH 001/148] feat(stream_chat_localizations): Initial implementation Signed-off-by: Sahil Kumar --- .../lib/src/extension.dart | 6 + .../lib/src/stream_chat.dart | 5 - .../lib/src/stream_chat_localizations.dart | 97 ++++++++ .../lib/stream_chat_flutter.dart | 1 + packages/stream_chat_localizations/.gitignore | 74 ++++++ packages/stream_chat_localizations/.metadata | 10 + .../stream_chat_localizations/CHANGELOG.md | 3 + packages/stream_chat_localizations/LICENSE | 219 ++++++++++++++++++ packages/stream_chat_localizations/README.md | 14 ++ .../lib/src/i18n/en.json | 0 .../lib/src/stream_chat_localizations.dart | 129 +++++++++++ .../lib/stream_chat_localizations.dart | 4 + .../stream_chat_localizations/pubspec.yaml | 57 +++++ .../test/stream_chat_localization_test.dart | 12 + 14 files changed, 626 insertions(+), 5 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/stream_chat_localizations.dart create mode 100644 packages/stream_chat_localizations/.gitignore create mode 100644 packages/stream_chat_localizations/.metadata create mode 100644 packages/stream_chat_localizations/CHANGELOG.md create mode 100644 packages/stream_chat_localizations/LICENSE create mode 100644 packages/stream_chat_localizations/README.md create mode 100644 packages/stream_chat_localizations/lib/src/i18n/en.json create mode 100644 packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart create mode 100644 packages/stream_chat_localizations/lib/stream_chat_localizations.dart create mode 100644 packages/stream_chat_localizations/pubspec.yaml create mode 100644 packages/stream_chat_localizations/test/stream_chat_localization_test.dart diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 5161c8e8..1a80d19e 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -103,6 +103,12 @@ extension BuildContextX on BuildContext { // ignore: public_member_api_docs double get textScaleFactor => MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0; + + String translate({ + required String key, + required String defaultValue, + }) => + StreamChatLocalizations.of(this)?.translate(key) ?? defaultValue; } /// Extension on [BorderRadius] diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index 64acae98..dfffce88 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -133,11 +133,6 @@ class StreamChatState extends State { /// The current user as a stream Stream get userStream => widget.client.state.userStream; - @override - void initState() { - super.initState(); - } - @override void didChangeDependencies() { final locale = ui.window.locale; diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_localizations.dart b/packages/stream_chat_flutter/lib/src/stream_chat_localizations.dart new file mode 100644 index 00000000..3ed1a9c3 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/stream_chat_localizations.dart @@ -0,0 +1,97 @@ +import 'package:flutter/widgets.dart'; + +// TODO : Fix localization instructions +// ADDING A NEW STRING +// +// If you (someone contributing to the Stream Chat Flutter) want to add a new +// string to the StreamChatLocalizations object (e.g. because you've added a new +// widget and it has a tooltip), follow these steps: +// +// 1. Add the new getter to StreamChatLocalizations below. +// +// 2. Implement a default value in DefaultMaterialLocalizations below. +// +// 3. Add a test to test/material/localizations_test.dart that verifies that +// this new value is implemented. +// +// 4. Update the flutter_localizations package. To add a new string to the +// flutter_localizations package, you must first add it to the English +// translations (lib/src/l10n/en.json), including a description. +// +// Then you need to add new entries for the string to all of the other +// language locale files by running: +// ``` +// dart dev/tools/localization/bin/gen_missing_localizations.dart +// ``` +// Which will copy the english strings into the other locales as placeholders +// until they can be translated. +// +// Finally you need to re-generate lib/src/l10n/localizations.dart by running: +// ``` +// dart dev/tools/localization/bin/gen_localizations.dart --overwrite +// ``` +// +// There is a README file with further information in the lib/src/l10n/ +// directory. +// +// 5. If you are a Google employee, you should then also follow the instructions +// at go/flutter-l10n. If you're not, don't worry about it. +// +// UPDATING AN EXISTING STRING +// +// If you (someone contributing to the Flutter framework) want to modify an +// existing string in the MaterialLocalizations objects, follow these steps: +// +// 1. Modify the default value of the relevant getter(s) in +// DefaultMaterialLocalizations below. +// +// 2. Update the flutter_localizations package. Modify the out-of-date English +// strings in lib/src/l10n/material_en.arb. +// +// You also need to re-generate lib/src/l10n/localizations.dart by running: +// ``` +// dart dev/tools/localization/bin/gen_localizations.dart --overwrite +// ``` +// +// This script may result in your updated getters being created in newer +// locales and set to the old value of the strings. This is to be expected. +// Leave them as they were generated, and they will be picked up for +// translation. +// +// There is a README file with further information in the lib/src/l10n/ +// directory. +// +// 3. If you are a Google employee, you should then also follow the instructions +// at go/flutter-l10n. If you're not, don't worry about it. + +/// Defines the localized resource values used by the StreamChatFlutter widgets. +/// +/// See also: +/// +/// * [GlobalStreamChatLocalizations], which provides material localizations +/// for many languages. +abstract class StreamChatLocalizations { + /// + String? translate(String key); + + /// The `StreamChatLocalizations` from the closest [Localizations] instance + /// that encloses the given context. + /// + /// If no [StreamChatLocalizations] are available in the given `context`, this + /// method returns null. + /// + /// This method is just a convenient shorthand for: + /// `Localizations.of(context, StreamChatLocalizations)`. + /// + /// References to the localized resources defined by this class are typically + /// written in terms of this method. For example: + /// + /// ```dart + /// tooltip: StreamChatLocalizations.of(context).backButtonTooltip, + /// ``` + static StreamChatLocalizations? of(BuildContext context) => + Localizations.of( + context, + StreamChatLocalizations, + ); +} diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 62bfd299..20b2396a 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -28,6 +28,7 @@ export 'src/reaction_icon.dart'; export 'src/reaction_picker.dart'; export 'src/sending_indicator.dart'; export 'src/stream_chat.dart'; +export 'src/stream_chat_localizations.dart'; export 'src/stream_chat_theme.dart'; export 'src/stream_neumorphic_button.dart'; export 'src/stream_svg_icon.dart'; diff --git a/packages/stream_chat_localizations/.gitignore b/packages/stream_chat_localizations/.gitignore new file mode 100644 index 00000000..1985397a --- /dev/null +++ b/packages/stream_chat_localizations/.gitignore @@ -0,0 +1,74 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 diff --git a/packages/stream_chat_localizations/.metadata b/packages/stream_chat_localizations/.metadata new file mode 100644 index 00000000..936336f9 --- /dev/null +++ b/packages/stream_chat_localizations/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: d79295af24c3ed621c33713ecda14ad196fd9c31 + channel: stable + +project_type: package diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md new file mode 100644 index 00000000..41cc7d81 --- /dev/null +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/stream_chat_localizations/LICENSE b/packages/stream_chat_localizations/LICENSE new file mode 100644 index 00000000..49088d47 --- /dev/null +++ b/packages/stream_chat_localizations/LICENSE @@ -0,0 +1,219 @@ +SOURCE CODE LICENSE AGREEMENT + +IMPORTANT - READ THIS CAREFULLY BEFORE DOWNLOADING, INSTALLING, USING OR +ELECTRONICALLY ACCESSING THIS PROPRIETARY PRODUCT. + +THIS IS A LEGAL AGREEMENT BETWEEN STREAM.IO, INC. (“STREAM.IO”) AND THE +BUSINESS ENTITY OR PERSON FOR WHOM YOU (“YOU”) ARE ACTING (“CUSTOMER”) AS THE +LICENSEE OF THE PROPRIETARY SOFTWARE INTO WHICH THIS AGREEMENT HAS BEEN +INCLUDED (THE “AGREEMENT”). YOU AGREE THAT YOU ARE THE CUSTOMER, OR YOU ARE AN +EMPLOYEE OR AGENT OF CUSTOMER AND ARE ENTERING INTO THIS AGREEMENT FOR LICENSE +OF THE SOFTWARE BY CUSTOMER FOR CUSTOMER’S BUSINESS PURPOSES AS DESCRIBED IN +AND IN ACCORDANCE WITH THIS AGREEMENT. YOU HEREBY AGREE THAT YOU ENTER INTO +THIS AGREEMENT ON BEHALF OF CUSTOMER AND THAT YOU HAVE THE AUTHORITY TO BIND +CUSTOMER TO THIS AGREEMENT. + +STREAM.IO IS WILLING TO LICENSE THE SOFTWARE TO CUSTOMER ONLY ON THE FOLLOWING +CONDITIONS: (1) YOU ARE A CURRENT CUSTOMER OF STREAM.IO; (2) YOU ARE NOT A +COMPETITOR OF STREAM.IO; AND (3) THAT YOU ACCEPT ALL THE TERMS IN THIS +AGREEMENT. BY DOWNLOADING, INSTALLING, CONFIGURING, ACCESSING OR OTHERWISE +USING THE SOFTWARE, INCLUDING ANY UPDATES, UPGRADES, OR NEWER VERSIONS, YOU +REPRESENT, WARRANT AND ACKNOWLEDGE THAT (A) CUSTOMER IS A CURRENT CUSTOMER OF +STREAM.IO; (B) CUSTOMER IS NOT A COMPETITOR OF STREAM.IO; AND THAT (C) YOU HAVE +READ THIS AGREEMENT, UNDERSTAND THIS AGREEMENT, AND THAT CUSTOMER AGREES TO BE +BOUND BY ALL THE TERMS OF THIS AGREEMENT. + +IF YOU DO NOT AGREE TO ALL THE TERMS AND CONDITIONS OF THIS AGREEMENT, +STREAM.IO IS UNWILLING TO LICENSE THE SOFTWARE TO CUSTOMER, AND THEREFORE, DO +NOT COMPLETE THE DOWNLOAD PROCESS, ACCESS OR OTHERWISE USE THE SOFTWARE, AND +CUSTOMER SHOULD IMMEDIATELY RETURN THE SOFTWARE AND CEASE ANY USE OF THE +SOFTWARE. + +1. SOFTWARE. The Stream.io software accompanying this Agreement, may include +Source Code, Executable Object Code, associated media, printed materials and +documentation (collectively, the “Software”). The Software also includes any +updates or upgrades to or new versions of the original Software, if and when +made available to you by Stream.io. “Source Code” means computer programming +code in human readable form that is not suitable for machine execution without +the intervening steps of interpretation or compilation. “Executable Object +Code" means the computer programming code in any other form than Source Code +that is not readily perceivable by humans and suitable for machine execution +without the intervening steps of interpretation or compilation. “Site” means a +Customer location controlled by Customer. “Authorized User” means any employee +or contractor of Customer working at the Site, who has signed a written +confidentiality agreement with Customer or is otherwise bound in writing by +confidentiality and use obligations at least as restrictive as those imposed +under this Agreement. + +2. LICENSE GRANT. Subject to the terms and conditions of this Agreement, in +consideration for the representations, warranties, and covenants made by +Customer in this Agreement, Stream.io grants to Customer, during the term of +this Agreement, a personal, non-exclusive, non-transferable, non-sublicensable +license to: + +a. install and use Software Source Code on password protected computers at a Site, +restricted to Authorized Users; + +b. create derivative works, improvements (whether or not patentable), extensions +and other modifications to the Software Source Code (“Modifications”) to build +unique scalable newsfeeds, activity streams, and in-app messaging via Stream’s +application program interface (“API”); + +c. compile the Software Source Code to create Executable Object Code versions of +the Software Source Code and Modifications to build such newsfeeds, activity +streams, and in-app messaging via the API; + +d. install, execute and use such Executable Object Code versions solely for +Customer’s internal business use (including development of websites through +which data generated by Stream services will be streamed (“Apps”)); + +e. use and distribute such Executable Object Code as part of Customer’s Apps; and + +f. make electronic copies of the Software and Modifications as required for backup +or archival purposes. + +3. RESTRICTIONS. Customer is responsible for all activities that occur in +connection with the Software. Customer will not, and will not attempt to: (a) +sublicense or transfer the Software or any Source Code related to the Software +or any of Customer’s rights under this Agreement, except as otherwise provided +in this Agreement, (b) use the Software Source Code for the benefit of a third +party or to operate a service; (c) allow any third party to access or use the +Software Source Code; (d) sublicense or distribute the Software Source Code or +any Modifications in Source Code or other derivative works based on any part of +the Software Source Code; (e) use the Software in any manner that competes with +Stream.io or its business; or (e) otherwise use the Software in any manner that +exceeds the scope of use permitted in this Agreement. Customer shall use the +Software in compliance with any accompanying documentation any laws applicable +to Customer. + +4. OPEN SOURCE. Customer and its Authorized Users shall not use any software or +software components that are open source in conjunction with the Software +Source Code or any Modifications in Source Code or in any way that could +subject the Software to any open source licenses. + +5. CONTRACTORS. Under the rights granted to Customer under this Agreement, +Customer may permit its employees, contractors, and agencies of Customer to +become Authorized Users to exercise the rights to the Software granted to +Customer in accordance with this Agreement solely on behalf of Customer to +provide services to Customer; provided that Customer shall be liable for the +acts and omissions of all Authorized Users to the extent any of such acts or +omissions, if performed by Customer, would constitute a breach of, or otherwise +give rise to liability to Customer under, this Agreement. Customer shall not +and shall not permit any Authorized User to use the Software except as +expressly permitted in this Agreement. + +6. COMPETITIVE PRODUCT DEVELOPMENT. Customer shall not use the Software in any way +to engage in the development of products or services which could be reasonably +construed to provide a complete or partial functional or commercial alternative +to Stream.io’s products or services (a “Competitive Product”). Customer shall +ensure that there is no direct or indirect use of, or sharing of, Software +source code, or other information based upon or derived from the Software to +develop such products or services. Without derogating from the generality of +the foregoing, development of Competitive Products shall include having direct +or indirect access to, supervising, consulting or assisting in the development +of, or producing any specifications, documentation, object code or source code +for, all or part of a Competitive Product. + +7. LIMITATION ON MODIFICATIONS. Notwithstanding any provision in this Agreement, +Modifications may only be created and used by Customer as permitted by this +Agreement and Modification Source Code may not be distributed to third parties. +Customer will not assert against Stream.io, its affiliates, or their customers, +direct or indirect, agents and contractors, in any way, any patent rights that +Customer may obtain relating to any Modifications for Stream.io, its +affiliates’, or their customers’, direct or indirect, agents’ and contractors’ +manufacture, use, import, offer for sale or sale of any Stream.io products or +services. + +8. DELIVERY AND ACCEPTANCE. The Software will be delivered electronically pursuant +to Stream.io standard download procedures. The Software is deemed accepted upon +delivery. + +9. IMPLEMENTATION AND SUPPORT. Stream.io has no obligation under this Agreement to +provide any support or consultation concerning the Software. + +10. TERM AND TERMINATION. The term of this Agreement begins when the Software is +downloaded or accessed and shall continue until terminated. Either party may +terminate this Agreement upon written notice. This Agreement shall +automatically terminate if Customer is or becomes a competitor of Stream.io or +makes or sells any Competitive Products. Upon termination of this Agreement for +any reason, (a) all rights granted to Customer in this Agreement immediately +cease to exist, (b) Customer must promptly discontinue all use of the Software +and return to Stream.io or destroy all copies of the Software in Customer’s +possession or control. Any continued use of the Software by Customer or attempt +by Customer to exercise any rights under this Agreement after this Agreement +has terminated shall be considered copyright infringement and subject Customer +to applicable remedies for copyright infringement. Sections 2, 5, 6, 8 and 9 +shall survive expiration or termination of this Agreement for any reason. + +11. OWNERSHIP. As between the parties, the Software and all worldwide intellectual +property rights and proprietary rights relating thereto or embodied therein, +are the exclusive property of Stream.io and its suppliers. Stream.io and its +suppliers reserve all rights in and to the Software not expressly granted to +Customer in this Agreement, and no other licenses or rights are granted by +implication, estoppel or otherwise. + +12. WARRANTY DISCLAIMER. USE OF THIS SOFTWARE IS ENTIRELY AT YOURS AND CUSTOMER’S +OWN RISK. THE SOFTWARE IS PROVIDED “AS IS” WITHOUT ANY WARRANTY OF ANY KIND +WHATSOEVER. STREAM.IO DOES NOT MAKE, AND HEREBY DISCLAIMS, ANY WARRANTY OF ANY +KIND, WHETHER EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING WITHOUT +LIMITATION, THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE, TITLE, NON-INFRINGEMENT OF THIRD-PARTY RIGHTS, RESULTS, EFFORTS, +QUALITY OR QUIET ENJOYMENT. STREAM.IO DOES NOT WARRANT THAT THE SOFTWARE IS +ERROR-FREE, WILL FUNCTION WITHOUT INTERRUPTION, WILL MEET ANY SPECIFIC NEED +THAT CUSTOMER HAS, THAT ALL DEFECTS WILL BE CORRECTED OR THAT IT IS +SUFFICIENTLY DOCUMENTED TO BE USABLE BY CUSTOMER. TO THE EXTENT THAT STREAM.IO +MAY NOT DISCLAIM ANY WARRANTY AS A MATTER OF APPLICABLE LAW, THE SCOPE AND +DURATION OF SUCH WARRANTY WILL BE THE MINIMUM PERMITTED UNDER SUCH LAW. +CUSTOMER ACKNOWLEDGES THAT IT HAS RELIED ON NO WARRANTIES OTHER THAN THE +EXPRESS WARRANTIES IN THIS AGREEMENT. + +13. LIMITATION OF LIABILITY. TO THE FULLEST EXTENT PERMISSIBLE BY LAW, STREAM.IO’S +TOTAL LIABILITY FOR ALL DAMAGES ARISING OUT OF OR RELATED TO THE SOFTWARE OR +THIS AGREEMENT, WHETHER IN CONTRACT, TORT (INCLUDING NEGLIGENCE) OR OTHERWISE, +SHALL NOT EXCEED $100. IN NO EVENT WILL STREAM.IO BE LIABLE FOR ANY INDIRECT, +CONSEQUENTIAL, EXEMPLARY, PUNITIVE, SPECIAL OR INCIDENTAL DAMAGES OF ANY KIND +WHATSOEVER, INCLUDING ANY LOST DATA AND LOST PROFITS, ARISING FROM OR RELATING +TO THE SOFTWARE EVEN IF STREAM.IO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. CUSTOMER ACKNOWLEDGES THAT THIS PROVISION REFLECTS THE AGREED UPON +ALLOCATION OF RISK FOR THIS AGREEMENT AND THAT STREAM.IO WOULD NOT ENTER INTO +THIS AGREEMENT WITHOUT THESE LIMITATIONS ON ITS LIABILITY. + +14. General. Customer may not assign or transfer this Agreement, by operation of +law or otherwise, or any of its rights under this Agreement (including the +license rights granted to Customer) to any third party without Stream.io’s +prior written consent, which consent will not be unreasonably withheld or +delayed. Stream.io may assign this Agreement, without consent, including, but +limited to, affiliate or any successor to all or substantially all its business +or assets to which this Agreement relates, whether by merger, sale of assets, +sale of stock, reorganization or otherwise. Any attempted assignment or +transfer in violation of the foregoing will be null and void. Stream.io shall +not be liable hereunder by reason of any failure or delay in the performance of +its obligations hereunder for any cause which is beyond the reasonable control. +All notices, consents, and approvals under this Agreement must be delivered in +writing by courier, by electronic mail, or by certified or registered mail, +(postage prepaid and return receipt requested) to the other party at the +address set forth in the customer agreement between Stream.io and Customer and +will be effective upon receipt or when delivery is refused. This Agreement will +be governed by and interpreted in accordance with the laws of the State of +Colorado, without reference to its choice of laws rules. The United Nations +Convention on Contracts for the International Sale of Goods does not apply to +this Agreement. Any action or proceeding arising from or relating to this +Agreement shall be brought in a federal or state court in Denver, Colorado, and +each party irrevocably submits to the jurisdiction and venue of any such court +in any such action or proceeding. All waivers must be in writing. Any waiver or +failure to enforce any provision of this Agreement on one occasion will not be +deemed a waiver of any other provision or of such provision on any other +occasion. If any provision of this Agreement is unenforceable, such provision +will be changed and interpreted to accomplish the objectives of such provision +to the greatest extent possible under applicable law and the remaining +provisions will continue in full force and effect. Customer shall not violate +any applicable law, rule or regulation, including those regarding the export of +technical data. The headings of Sections of this Agreement are for convenience +and are not to be used in interpreting this Agreement. As used in this +Agreement, the word “including” means “including but not limited to.” This +Agreement (including all exhibits and attachments) constitutes the entire +agreement between the parties regarding the subject hereof and supersedes all +prior or contemporaneous agreements, understandings and communication, whether +written or oral. This Agreement may be amended only by a written document +signed by both parties. The terms of any purchase order or similar document +submitted by Customer to Stream.io will have no effect. diff --git a/packages/stream_chat_localizations/README.md b/packages/stream_chat_localizations/README.md new file mode 100644 index 00000000..3ec4d86e --- /dev/null +++ b/packages/stream_chat_localizations/README.md @@ -0,0 +1,14 @@ +# stream_chat_localizations + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Dart +[package](https://flutter.dev/developing-packages/), +a library module containing code that can be shared easily across +multiple Flutter or Dart projects. + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/stream_chat_localizations/lib/src/i18n/en.json b/packages/stream_chat_localizations/lib/src/i18n/en.json new file mode 100644 index 00000000..e69de29b diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart new file mode 100644 index 00000000..25ca28ea --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -0,0 +1,129 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart' + show StreamChatLocalizations; + +const kStreamChatSupportedLanguages = []; + +/// Implementation of localized strings for the stream chat widgets +/// +/// ## Supported languages +/// +/// This class supports locales with the following [Locale.languageCode]s: +/// +/// {@macro flutter.localizations.material.languages} +/// +/// This list is available programmatically via [kStreamChatSupportedLanguages]. +/// +/// ## Sample code +/// +/// To include the localizations provided by this class in a [MaterialApp], +/// add [GlobalStreamChatLocalizations.delegates] to +/// [MaterialApp.localizationsDelegates], and specify the locales your +/// app supports with [MaterialApp.supportedLocales]: +/// +/// ```dart +/// new MaterialApp( +/// localizationsDelegates: GlobalStreamChatLocalizations.delegates, +/// supportedLocales: [ +/// const Locale('en', 'US'), // American English +/// const Locale('he', 'IL'), // Israeli Hebrew +/// // ... +/// ], +/// // ... +/// ) +/// ``` +/// +class GlobalStreamChatLocalizations implements StreamChatLocalizations { + /// Construct an object that defines the localized values for the widgets + /// library for US English (only). + /// + /// [LocalizationsDelegate] implementations typically call the static [load] + const GlobalStreamChatLocalizations(this.locale, this.translations); + + final Locale locale; + + final Map translations; + + static String getLocalePath(Locale locale) => + 'packages/stream_chat_localizations/i18n/${locale.languageCode}.json'; + + /// Creates an object that provides US English resource values for the + /// lowest levels of the widgets library. + /// + /// The [locale] parameter is ignored. + /// + /// This method is typically used to create a [LocalizationsDelegate]. + /// The [WidgetsApp] does so by default. + static Future load(Locale locale) async { + final localePath = getLocalePath(locale); + final rawTranslations = await rootBundle.loadString(localePath); + Map translations = json.decode(rawTranslations); + translations = translations.map( + (key, value) => MapEntry(key, value?.toString()), + ); + return GlobalStreamChatLocalizations(locale, translations); + } + + /// A [LocalizationsDelegate] for [StreamChatLocalizations]. + /// + /// Most internationalized apps will use [GlobalStreamChatLocalizations.delegates] + /// as the value of [MaterialApp.localizationsDelegates] to include + /// the localizations for both the flutter and stream chat widget libraries. + static const LocalizationsDelegate delegate = + _StreamChatLocalizationsDelegate(); + + /// A value for [MaterialApp.localizationsDelegates] that's typically used by + /// internationalized apps. + /// + /// ## Sample code + /// + /// To include the localizations provided by this class and by + /// [GlobalWidgetsLocalizations] in a [MaterialApp], + /// use [GlobalStreamChatLocalizations.delegates] as the value of + /// [MaterialApp.localizationsDelegates], and specify the locales your + /// app supports with [MaterialApp.supportedLocales]: + /// + /// ```dart + /// new MaterialApp( + /// localizationsDelegates: GlobalStreamChatLocalizations.delegates, + /// supportedLocales: [ + /// const Locale('en', 'US'), // English + /// const Locale('he', 'IL'), // Hebrew + /// ], + /// // ... + /// ) + /// ``` + static const List delegates = [ + delegate, + GlobalCupertinoLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + @override + String? translate(String key) => translations[key]; +} + +class _StreamChatLocalizationsDelegate + extends LocalizationsDelegate { + const _StreamChatLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) => + kStreamChatSupportedLanguages.contains(locale.languageCode); + + @override + Future load(Locale locale) => + GlobalStreamChatLocalizations.load(locale); + + @override + bool shouldReload(_StreamChatLocalizationsDelegate old) => false; + + @override + String toString() => 'StreamChatLocalizations.delegate(' + '${kStreamChatSupportedLanguages.length} locales)'; +} diff --git a/packages/stream_chat_localizations/lib/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/stream_chat_localizations.dart new file mode 100644 index 00000000..9c1146d8 --- /dev/null +++ b/packages/stream_chat_localizations/lib/stream_chat_localizations.dart @@ -0,0 +1,4 @@ +/// Localizations for the StreamChat Flutter library. +library stream_chat_localization; + +export 'src/stream_chat_localizations.dart'; diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml new file mode 100644 index 00000000..34986658 --- /dev/null +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -0,0 +1,57 @@ +name: stream_chat_localizations +description: A new Flutter project. +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + stream_chat_flutter: + path: ../stream_chat_flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/stream_chat_localizations/test/stream_chat_localization_test.dart b/packages/stream_chat_localizations/test/stream_chat_localization_test.dart new file mode 100644 index 00000000..9ea48cb5 --- /dev/null +++ b/packages/stream_chat_localizations/test/stream_chat_localization_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:stream_chat_localization/stream_chat_localizations.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} From 26912431028dfc97d55b8941d25572b44103f36b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 8 Jul 2021 16:43:24 +0530 Subject: [PATCH 002/148] Remove json translations approach, add example for adding language Signed-off-by: Sahil Kumar --- .../stream_chat_flutter/example/lib/main.dart | 46 +++++++- .../stream_chat_flutter/example/pubspec.yaml | 2 + .../lib/src/extension.dart | 6 +- .../lib/src/message_text.dart | 3 + .../lib/src/stream_chat_localizations.dart | 5 +- packages/stream_chat_flutter/pubspec.yaml | 1 + .../lib/src/i18n/en.json | 0 .../lib/src/stream_chat_localizations.dart | 103 +++++++++++------- .../lib/src/stream_chat_localizations_en.dart | 11 ++ .../lib/stream_chat_localizations.dart | 7 +- .../stream_chat_localizations/pubspec.yaml | 37 ------- .../test/stream_chat_localization_test.dart | 22 ++-- 12 files changed, 144 insertions(+), 99 deletions(-) delete mode 100644 packages/stream_chat_localizations/lib/src/i18n/en.json create mode 100644 packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index d3522886..c1246a31 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -1,10 +1,35 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_persistence/stream_chat_persistence.dart'; +import 'package:stream_chat_localizations/stream_chat_localizations.dart'; -final chatPersistentClient = StreamChatPersistenceClient( - logLevel: Level.INFO, -); +/// A custom set of localizations for the 'hi' locale. +class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { + const StreamChatLocalizationsHi() : super(localeName: 'hi'); + + static const LocalizationsDelegate delegate = + _HindiStreamChatLocalizationsDelegate(); + + @override + String get launchUrlError => 'URL लॉन्च नहीं कर सकता'; +} + +class _HindiStreamChatLocalizationsDelegate + extends LocalizationsDelegate { + const _HindiStreamChatLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) => locale.languageCode == 'hi'; + + @override + Future load(Locale locale) => + SynchronousFuture(const StreamChatLocalizationsHi()); + + @override + bool shouldReload(_HindiStreamChatLocalizationsDelegate old) => false; +} void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -14,7 +39,7 @@ void main() async { final client = StreamChatClient( 's2dxdhpxd94g', logLevel: Level.INFO, - )..chatPersistenceClient = chatPersistentClient; + ); /// Set the current user and connect the websocket. In a production scenario, this should be done using /// a backend to generate a user token using our server SDK. @@ -58,6 +83,17 @@ class MyApp extends StatelessWidget { theme: ThemeData.light(), darkTheme: ThemeData.dark(), themeMode: ThemeMode.system, + supportedLocales: [ + Locale('en', 'US'), + Locale('hi', 'IN'), + ], + localizationsDelegates: [ + GlobalStreamChatLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + StreamChatLocalizationsHi.delegate, + ], builder: (context, widget) { return StreamChat( client: client, diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index 408bf9e1..4eb99d86 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -29,6 +29,8 @@ dependencies: # path: ../../stream_chat_flutter_core stream_chat_flutter: path: ../ + stream_chat_localizations: + path: ../../stream_chat_localizations stream_chat_persistence: path: ../../stream_chat_persistence diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 1a80d19e..3a495ca5 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -104,11 +104,7 @@ extension BuildContextX on BuildContext { double get textScaleFactor => MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0; - String translate({ - required String key, - required String defaultValue, - }) => - StreamChatLocalizations.of(this)?.translate(key) ?? defaultValue; + StreamChatLocalizations? get translations => StreamChatLocalizations.of(this); } /// Extension on [BorderRadius] diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index 6d006085..b0bd13ca 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Text widget to display in message class MessageText extends StatelessWidget { @@ -29,6 +30,8 @@ class MessageText extends StatelessWidget { @override Widget build(BuildContext context) { + final texts = context.translations?.launchUrlError ?? 'defaultValue'; + return Text(texts); final text = _replaceMentions(message.text ?? '').replaceAll('\n', '\n\n'); final themeData = Theme.of(context); diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_localizations.dart b/packages/stream_chat_flutter/lib/src/stream_chat_localizations.dart index 3ed1a9c3..b1d81fde 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_localizations.dart @@ -71,9 +71,6 @@ import 'package:flutter/widgets.dart'; /// * [GlobalStreamChatLocalizations], which provides material localizations /// for many languages. abstract class StreamChatLocalizations { - /// - String? translate(String key); - /// The `StreamChatLocalizations` from the closest [Localizations] instance /// that encloses the given context. /// @@ -94,4 +91,6 @@ abstract class StreamChatLocalizations { context, StreamChatLocalizations, ); + + String get launchUrlError; } diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 342914d4..e15f51c7 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -7,6 +7,7 @@ issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: sdk: '>=2.12.0 <3.0.0' + flutter: ">=1.17.0" dependencies: cached_network_image: ^3.0.0 diff --git a/packages/stream_chat_localizations/lib/src/i18n/en.json b/packages/stream_chat_localizations/lib/src/i18n/en.json deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index 25ca28ea..b0ca6cd5 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -1,12 +1,45 @@ -import 'dart:convert'; - +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart' show StreamChatLocalizations; -const kStreamChatSupportedLanguages = []; +part 'stream_chat_localizations_en.dart'; + +/// The set of supported languages, as language code strings. +/// +/// The [GlobalStreamChatLocalizations.delegate] can generate localizations for +/// any [Locale] with a language code from this set. +/// +/// See also: +/// +/// * [getStreamChatTranslation], whose documentation describes these values. +const kStreamChatSupportedLanguages = {'en'}; + +/// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`. +/// +/// All of the function's arguments except `locale` will be passed to the +/// [GlobalStreamChatLocalizations] constructor. (The `localeName` argument of that +/// constructor is specified by the actual subclass constructor by this +/// function.) +/// +/// The following locales are supported by this package: +/// +/// * `en` - English +/// +/// Generally speaking, this method is only intended to be used by +/// [GlobalStreamChatLocalizations.delegate]. +GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { + switch (locale.languageCode) { + case 'en': + return const StreamChatLocalizationsEn(); + } + assert( + false, + 'getStreamChatTranslation() called for unsupported locale "$locale"', + ); + return null; +} /// Implementation of localized strings for the stream chat widgets /// @@ -30,43 +63,21 @@ const kStreamChatSupportedLanguages = []; /// localizationsDelegates: GlobalStreamChatLocalizations.delegates, /// supportedLocales: [ /// const Locale('en', 'US'), // American English -/// const Locale('he', 'IL'), // Israeli Hebrew /// // ... /// ], /// // ... /// ) /// ``` /// -class GlobalStreamChatLocalizations implements StreamChatLocalizations { - /// Construct an object that defines the localized values for the widgets - /// library for US English (only). - /// - /// [LocalizationsDelegate] implementations typically call the static [load] - const GlobalStreamChatLocalizations(this.locale, this.translations); +abstract class GlobalStreamChatLocalizations + implements StreamChatLocalizations { + /// Initializes an object that defines the StreamChat widget's localized + /// strings for the given `localeName`. + const GlobalStreamChatLocalizations({ + required String localeName, + }) : _localeName = localeName; - final Locale locale; - - final Map translations; - - static String getLocalePath(Locale locale) => - 'packages/stream_chat_localizations/i18n/${locale.languageCode}.json'; - - /// Creates an object that provides US English resource values for the - /// lowest levels of the widgets library. - /// - /// The [locale] parameter is ignored. - /// - /// This method is typically used to create a [LocalizationsDelegate]. - /// The [WidgetsApp] does so by default. - static Future load(Locale locale) async { - final localePath = getLocalePath(locale); - final rawTranslations = await rootBundle.loadString(localePath); - Map translations = json.decode(rawTranslations); - translations = translations.map( - (key, value) => MapEntry(key, value?.toString()), - ); - return GlobalStreamChatLocalizations(locale, translations); - } + final String _localeName; /// A [LocalizationsDelegate] for [StreamChatLocalizations]. /// @@ -74,7 +85,7 @@ class GlobalStreamChatLocalizations implements StreamChatLocalizations { /// as the value of [MaterialApp.localizationsDelegates] to include /// the localizations for both the flutter and stream chat widget libraries. static const LocalizationsDelegate delegate = - _StreamChatLocalizationsDelegate(); + _StreamChatLocalizationsDelegate(); /// A value for [MaterialApp.localizationsDelegates] that's typically used by /// internationalized apps. @@ -92,20 +103,19 @@ class GlobalStreamChatLocalizations implements StreamChatLocalizations { /// localizationsDelegates: GlobalStreamChatLocalizations.delegates, /// supportedLocales: [ /// const Locale('en', 'US'), // English - /// const Locale('he', 'IL'), // Hebrew /// ], /// // ... /// ) /// ``` static const List delegates = [ - delegate, + GlobalStreamChatLocalizations.delegate, GlobalCupertinoLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ]; @override - String? translate(String key) => translations[key]; + String get launchUrlError; } class _StreamChatLocalizationsDelegate @@ -116,14 +126,25 @@ class _StreamChatLocalizationsDelegate bool isSupported(Locale locale) => kStreamChatSupportedLanguages.contains(locale.languageCode); + static final _loadedTranslations = + >{}; + @override - Future load(Locale locale) => - GlobalStreamChatLocalizations.load(locale); + Future load(Locale locale) { + assert(isSupported(locale), ''); + return _loadedTranslations.putIfAbsent( + locale, + () => + SynchronousFuture( + getStreamChatTranslation(locale)!, + ), + ); + } @override bool shouldReload(_StreamChatLocalizationsDelegate old) => false; @override - String toString() => 'StreamChatLocalizations.delegate(' + String toString() => 'GlobalStreamChatLocalizations.delegate(' '${kStreamChatSupportedLanguages.length} locales)'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart new file mode 100644 index 00000000..7566be30 --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -0,0 +1,11 @@ +part of 'stream_chat_localizations.dart'; + +/// The translations for English (`en`). +class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { + /// Create an instance of the translation bundle for English. + const StreamChatLocalizationsEn({String localeName = 'en'}) + : super(localeName: localeName); + + @override + String get launchUrlError => 'Cannot launch the url'; +} diff --git a/packages/stream_chat_localizations/lib/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/stream_chat_localizations.dart index 9c1146d8..0cfb60ba 100644 --- a/packages/stream_chat_localizations/lib/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/stream_chat_localizations.dart @@ -1,4 +1,9 @@ /// Localizations for the StreamChat Flutter library. library stream_chat_localization; -export 'src/stream_chat_localizations.dart'; +export 'package:flutter_localizations/flutter_localizations.dart' + show + GlobalCupertinoLocalizations, + GlobalMaterialLocalizations, + GlobalWidgetsLocalizations; +export 'src/stream_chat_localizations.dart' hide getStreamChatTranslation; diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index 34986658..b755a6dd 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -18,40 +18,3 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. -flutter: - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/stream_chat_localizations/test/stream_chat_localization_test.dart b/packages/stream_chat_localizations/test/stream_chat_localization_test.dart index 9ea48cb5..0324dd17 100644 --- a/packages/stream_chat_localizations/test/stream_chat_localization_test.dart +++ b/packages/stream_chat_localizations/test/stream_chat_localization_test.dart @@ -1,12 +1,20 @@ +import 'dart:ui'; + import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_localization/stream_chat_localizations.dart'; +import 'package:stream_chat_localizations/stream_chat_localizations.dart'; void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); - }); + for (final language in kStreamChatSupportedLanguages) { + test('translations exist for $language', () async { + final locale = Locale(language); + expect( + GlobalStreamChatLocalizations.delegate.isSupported(locale), + isTrue, + ); + final localizations = + await GlobalStreamChatLocalizations.delegate.load(locale); + expect(localizations.launchUrlError, isNotNull); + }); + } } From ffb46ecdca728a39be4f51461a2a9ab2c97df411 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 8 Jul 2021 16:59:57 +0530 Subject: [PATCH 003/148] minor changes --- .../lib/src/stream_chat_localizations.dart | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index b0ca6cd5..bef008fd 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -30,15 +30,15 @@ const kStreamChatSupportedLanguages = {'en'}; /// Generally speaking, this method is only intended to be used by /// [GlobalStreamChatLocalizations.delegate]. GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { + final languageCode = locale.languageCode; + assert( + kStreamChatSupportedLanguages.contains(languageCode), + 'getStreamChatTranslation() called for unsupported locale "$locale"', + ); switch (locale.languageCode) { case 'en': return const StreamChatLocalizationsEn(); } - assert( - false, - 'getStreamChatTranslation() called for unsupported locale "$locale"', - ); - return null; } /// Implementation of localized strings for the stream chat widgets @@ -85,7 +85,7 @@ abstract class GlobalStreamChatLocalizations /// as the value of [MaterialApp.localizationsDelegates] to include /// the localizations for both the flutter and stream chat widget libraries. static const LocalizationsDelegate delegate = - _StreamChatLocalizationsDelegate(); + _StreamChatLocalizationsDelegate(); /// A value for [MaterialApp.localizationsDelegates] that's typically used by /// internationalized apps. @@ -127,17 +127,16 @@ class _StreamChatLocalizationsDelegate kStreamChatSupportedLanguages.contains(locale.languageCode); static final _loadedTranslations = - >{}; + >{}; @override Future load(Locale locale) { assert(isSupported(locale), ''); return _loadedTranslations.putIfAbsent( locale, - () => - SynchronousFuture( - getStreamChatTranslation(locale)!, - ), + () => SynchronousFuture( + getStreamChatTranslation(locale)!, + ), ); } From a48389a814317ff8c52faf502cda7fbb4a7d59e9 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 12 Jul 2021 15:01:36 +0530 Subject: [PATCH 004/148] add remaining translation strings Signed-off-by: xsahil03x --- .../attachment_upload_state_builder.dart | 3 +- .../lib/src/attachment/file_attachment.dart | 9 +- .../lib/src/attachment/giphy_attachment.dart | 17 +- .../lib/src/attachment_actions_modal.dart | 13 +- .../lib/src/channel_bottom_sheet.dart | 25 +- .../lib/src/channel_header.dart | 7 +- .../lib/src/channel_info.dart | 21 +- .../lib/src/channel_list_header.dart | 18 +- .../lib/src/channel_list_view.dart | 34 +- .../lib/src/channel_name.dart | 9 +- .../lib/src/channel_preview.dart | 5 +- .../lib/src/date_divider.dart | 5 +- .../lib/src/deleted_message.dart | 3 +- .../lib/src/extension.dart | 4 +- .../lib/src/full_screen_media.dart | 9 +- .../lib/src/image_footer.dart | 3 +- .../stream_chat_localizations.dart | 36 ++ .../lib/src/localization/translations.dart | 517 ++++++++++++++++++ .../lib/src/message_actions_modal.dart | 62 ++- .../lib/src/message_input.dart | 78 ++- .../lib/src/message_list_view.dart | 20 +- .../lib/src/message_reactions_modal.dart | 3 +- .../lib/src/message_search_item.dart | 3 +- .../lib/src/message_search_list_view.dart | 16 +- .../lib/src/message_text.dart | 3 - .../lib/src/message_widget.dart | 21 +- .../lib/src/stream_chat_localizations.dart | 96 ---- .../lib/src/thread_header.dart | 3 +- .../lib/src/typing_indicator.dart | 4 +- .../lib/src/user_item.dart | 8 +- .../lib/src/user_list_view.dart | 20 +- .../stream_chat_flutter/lib/src/utils.dart | 8 +- .../lib/stream_chat_flutter.dart | 2 +- .../lib/src/stream_chat_localizations.dart | 5 +- .../lib/src/stream_chat_localizations_en.dart | 315 +++++++++++ 35 files changed, 1104 insertions(+), 301 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart create mode 100644 packages/stream_chat_flutter/lib/src/localization/translations.dart delete mode 100644 packages/stream_chat_flutter/lib/src/stream_chat_localizations.dart diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart index 825def58..ebbaed7d 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/upload_progress_indicator.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Widget to build in progress typedef InProgressBuilder = Widget Function(BuildContext, int, int); @@ -226,7 +227,7 @@ class _FailedState extends StatelessWidget { horizontal: 12, ), child: Text( - 'UPLOAD ERROR', + context.translations.uploadErrorLabel, style: theme.textTheme.footnote.copyWith( color: theme.colorTheme.white, ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index 7ec5fb04..547c5821 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -7,9 +7,9 @@ import 'package:stream_chat_flutter/src/upload_progress_indicator.dart'; import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; -// ignore: always_use_package_imports -import 'attachment_widget.dart'; +import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; /// Widget for displaying file attachments class FileAttachment extends AttachmentWidget { @@ -285,7 +285,10 @@ class FileAttachment extends AttachmentWidget { progressIndicatorColor: theme.colorTheme.accentBlue, ), success: () => Text(fileSize(size), style: textStyle), - failed: (_) => Text('UPLOAD ERROR', style: textStyle), + failed: (_) => Text( + context.translations.uploadErrorLabel, + style: textStyle, + ), ); } } diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index 1572061e..1ae03ca1 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -4,6 +4,7 @@ import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Widget for showing a GIF attachment class GiphyAttachment extends AttachmentWidget { @@ -71,9 +72,9 @@ class GiphyAttachment extends AttachmentWidget { children: [ StreamSvgIcon.giphyIcon(), const SizedBox(width: 8), - const Text( - 'Giphy', - style: TextStyle(fontWeight: FontWeight.bold), + Text( + context.translations.giphyLabel, + style: const TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(width: 8), if (attachment.title != null) @@ -134,7 +135,7 @@ class GiphyAttachment extends AttachmentWidget { }); }, child: Text( - 'Cancel', + context.translations.cancelLabel.toLowerCase(), style: StreamChatTheme.of(context) .textTheme .bodyBold @@ -166,7 +167,7 @@ class GiphyAttachment extends AttachmentWidget { }); }, child: Text( - 'Shuffle', + context.translations.shuffleLabel, style: StreamChatTheme.of(context) .textTheme .bodyBold @@ -199,7 +200,7 @@ class GiphyAttachment extends AttachmentWidget { }); }, child: Text( - 'Send', + context.translations.sendLabel, style: TextStyle( color: StreamChatTheme.of(context) .colorTheme @@ -234,7 +235,7 @@ class GiphyAttachment extends AttachmentWidget { width: 8, ), Text( - 'Only visible to you', + context.translations.onlyVisibleToYouText, style: StreamChatTheme.of(context) .textTheme .footnote @@ -339,7 +340,7 @@ class GiphyAttachment extends AttachmentWidget { size: 16, ), Text( - 'GIPHY', + context.translations.giphyLabel.toUpperCase(), style: TextStyle( color: StreamChatTheme.of(context).colorTheme.white, fontWeight: FontWeight.bold, diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart index 363fac45..48eea143 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -48,7 +48,7 @@ class AttachmentActionsModal extends StatelessWidget { child: _buildPage(context), ); - Widget _buildPage(context) { + Widget _buildPage(BuildContext context) { final theme = StreamChatTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.end, @@ -69,7 +69,7 @@ class AttachmentActionsModal extends StatelessWidget { children: [ _buildButton( context, - 'Reply', + context.translations.replyLabel, StreamSvgIcon.iconCurveLineLeftUp( size: 24, color: theme.colorTheme.grey, @@ -80,7 +80,7 @@ class AttachmentActionsModal extends StatelessWidget { ), _buildButton( context, - 'Show in Chat', + context.translations.showInChatLabel, StreamSvgIcon.eye( size: 24, color: theme.colorTheme.black, @@ -89,8 +89,9 @@ class AttachmentActionsModal extends StatelessWidget { ), _buildButton( context, - // ignore: lines_longer_than_80_chars - 'Save ${message.attachments[currentIndex].type == 'video' ? 'Video' : 'Image'}', + message.attachments[currentIndex].type == 'video' + ? context.translations.saveVideoLabel + : context.translations.saveImageLabel, StreamSvgIcon.iconSave( size: 24, color: theme.colorTheme.grey, @@ -141,7 +142,7 @@ class AttachmentActionsModal extends StatelessWidget { if (StreamChat.of(context).user?.id == message.user?.id) _buildButton( context, - 'Delete', + context.translations.deleteLabel, StreamSvgIcon.delete( size: 24, color: theme.colorTheme.accentRed, diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index eaabb7e2..9a8a3d1a 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Bottom Sheet with options class ChannelBottomSheet extends StatefulWidget { @@ -149,7 +150,7 @@ class _ChannelBottomSheetState extends State { color: _streamChatThemeData.colorTheme.grey, ), ), - title: 'View Info', + title: context.translations.viewInfoLabel, onTap: widget.onViewInfoTap, ), if (!channel.isDistinct) @@ -160,7 +161,7 @@ class _ChannelBottomSheetState extends State { color: _streamChatThemeData.colorTheme.grey, ), ), - title: 'Leave Group', + title: context.translations.leaveGroupLabel, onTap: () async { setState(() { _showActions = false; @@ -179,7 +180,7 @@ class _ChannelBottomSheetState extends State { color: _streamChatThemeData.colorTheme.accentRed, ), ), - title: 'Delete Conversation', + title: context.translations.deleteConversationLabel, titleColor: _streamChatThemeData.colorTheme.accentRed, onTap: () async { setState(() { @@ -198,7 +199,7 @@ class _ChannelBottomSheetState extends State { color: _streamChatThemeData.colorTheme.grey, ), ), - title: 'Cancel', + title: context.translations.cancelLabel, onTap: () { Navigator.pop(context); }, @@ -219,10 +220,10 @@ class _ChannelBottomSheetState extends State { Future _showDeleteDialog() async { final res = await showConfirmationDialog( context, - title: 'Delete Conversation', - okText: 'DELETE', - question: 'Are you sure you want to delete this conversation?', - cancelText: 'CANCEL', + title: context.translations.deleteConversationLabel, + okText: context.translations.deleteLabel, + question: context.translations.deleteConversationQuestion, + cancelText: context.translations.cancelLabel, icon: StreamSvgIcon.delete( color: _streamChatThemeData.colorTheme.accentRed, ), @@ -237,10 +238,10 @@ class _ChannelBottomSheetState extends State { Future _showLeaveDialog() async { final res = await showConfirmationDialog( context, - title: 'Leave conversation', - okText: 'LEAVE', - question: 'Are you sure you want to leave this conversation?', - cancelText: 'CANCEL', + title: context.translations.leaveConversationLabel, + okText: context.translations.leaveLabel, + question: context.translations.leaveConversationQuestion, + cancelText: context.translations.cancelLabel, icon: StreamSvgIcon.userRemove( color: _streamChatThemeData.colorTheme.accentRed, ), diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 16c1f1b7..a844af1c 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -6,6 +6,7 @@ import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header_paint.png) @@ -121,14 +122,14 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { switch (status) { case ConnectionStatus.connected: - statusString = 'Connected'; + statusString = context.translations.connectedLabel; showStatus = false; break; case ConnectionStatus.connecting: - statusString = 'Reconnecting...'; + statusString = context.translations.reconnectingLabel; break; case ConnectionStatus.disconnected: - statusString = 'Disconnected'; + statusString = context.translations.disconnectedLabel; break; } diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 07920b95..c4ea92c9 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Widget which shows channel info class ChannelInfo extends StatelessWidget { @@ -55,10 +56,13 @@ class ChannelInfo extends StatelessWidget { ) { Widget? alternativeWidget; - if (channel.memberCount != null && channel.memberCount! > 2) { - var text = '${channel.memberCount} Members'; + final memberCount = channel.memberCount; + if (memberCount != null && memberCount > 2) { + var text = context.translations.membersCountText(memberCount); final watcherCount = channel.state?.watcherCount ?? 0; - if (watcherCount > 0) text += ' $watcherCount Online'; + if (watcherCount > 0) { + text += ' ${context.translations.watchersCountText(watcherCount)}'; + } alternativeWidget = Text( text, style: StreamChatTheme.of(context) @@ -75,12 +79,13 @@ class ChannelInfo extends StatelessWidget { if (otherMember != null) { if (otherMember.user?.online == true) { alternativeWidget = Text( - 'Online', + context.translations.userOnlineText, style: textStyle, ); } else { alternativeWidget = Text( - 'Last seen ${Jiffy(otherMember.user?.lastActive).fromNow()}', + context.translations.userLastOnlineText + + Jiffy(otherMember.user?.lastActive).fromNow(), style: textStyle, ); } @@ -111,7 +116,7 @@ class ChannelInfo extends StatelessWidget { ), const SizedBox(width: 10), Text( - 'Searching for Network', + context.translations.searchingForNetworkLabel, style: textStyle, ), ], @@ -125,7 +130,7 @@ class ChannelInfo extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'Offline...', + context.translations.offlineLabel, style: textStyle, ), TextButton( @@ -141,7 +146,7 @@ class ChannelInfo extends StatelessWidget { ..closeConnection() ..openConnection(), child: Text( - 'Try Again', + context.translations.tryAgainLabel, style: textStyle?.copyWith( color: StreamChatTheme.of(context).colorTheme.accentBlue, ), diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 5a4275a6..6c3eea20 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Widget builder for title typedef TitleBuilder = Widget Function( @@ -103,21 +104,20 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { switch (status) { case ConnectionStatus.connected: - statusString = 'Connected'; + statusString = context.translations.connectedLabel; showStatus = false; break; case ConnectionStatus.connecting: - statusString = 'Reconnecting...'; + statusString = context.translations.reconnectingLabel; break; case ConnectionStatus.disconnected: - statusString = 'Disconnected'; + statusString = context.translations.disconnectedLabel; break; } final chatThemeData = StreamChatTheme.of(context); return InfoTile( - // ignore: avoid_bool_literals_in_conditional_expressions - showMessage: showConnectionStateTile ? showStatus : false, + showMessage: showConnectionStateTile && showStatus, message: statusString, child: AppBar( textTheme: Theme.of(context).textTheme, @@ -207,7 +207,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { Widget _buildConnectedTitleState(BuildContext context) { final chatThemeData = StreamChatTheme.of(context); return Text( - 'Stream Chat', + context.translations.streamChatLabel, style: chatThemeData.textTheme.headlineBold.copyWith( color: chatThemeData.colorTheme.black, ), @@ -226,7 +226,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { ), const SizedBox(width: 10), Text( - 'Searching for Network', + context.translations.searchingForNetworkLabel, style: StreamChatTheme.of(context) .channelListHeaderTheme .title @@ -247,7 +247,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'Offline...', + context.translations.offlineLabel, style: chatThemeData.channelListHeaderTheme.title?.copyWith( fontSize: 16, fontWeight: FontWeight.bold, @@ -258,7 +258,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { ..closeConnection() ..openConnection(), child: Text( - 'Try Again', + context.translations.tryAgainLabel, style: chatThemeData.channelListHeaderTheme.title?.copyWith( fontSize: 16, fontWeight: FontWeight.bold, diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index b14e0b75..847a265b 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -7,6 +7,7 @@ import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Callback called when tapping on a channel typedef ChannelTapCallback = void Function(Channel, Widget?); @@ -202,6 +203,7 @@ class _ChannelListViewState extends State { final _slideController = SlidableController(); late final _defaultController = ChannelListController(); + ChannelListController get _channelListController => widget.channelListController ?? _defaultController; @@ -296,7 +298,7 @@ class _ChannelListViewState extends State { Padding( padding: const EdgeInsets.all(8), child: Text( - 'Let’s start chatting!', + context.translations.letsStartChattingLabel, style: chatThemeData.textTheme.headline, ), ), @@ -306,7 +308,7 @@ class _ChannelListViewState extends State { horizontal: 52, ), child: Text( - 'How about sending your first message to a friend?', + context.translations.sendingFirstMessageLabel, textAlign: TextAlign.center, style: chatThemeData.textTheme.body.copyWith( color: chatThemeData.colorTheme.grey, @@ -325,7 +327,7 @@ class _ChannelListViewState extends State { child: TextButton( onPressed: widget.onStartChatPressed, child: Text( - 'Start a chat', + context.translations.startAChatLabel, style: chatThemeData.textTheme.bodyBold.copyWith( color: chatThemeData.colorTheme.accentBlue, ), @@ -461,9 +463,9 @@ class _ChannelListViewState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text.rich( - const TextSpan( + TextSpan( children: [ - WidgetSpan( + const WidgetSpan( child: Padding( padding: EdgeInsets.only( right: 2, @@ -471,14 +473,14 @@ class _ChannelListViewState extends State { child: Icon(Icons.error_outline), ), ), - TextSpan(text: 'Error loading channels'), + TextSpan(text: context.translations.loadingChannelsError), ], ), style: Theme.of(context).textTheme.headline6, ), TextButton( onPressed: () => _channelListController.loadData!(), - child: const Text('Retry'), + child: Text(context.translations.retryLabel), ), ], ), @@ -558,12 +560,12 @@ class _ChannelListViewState extends State { : () async { final res = await showConfirmationDialog( context, - title: 'Delete Conversation', - okText: 'DELETE', - question: - // ignore: lines_longer_than_80_chars - 'Are you sure you want to delete this conversation?', - cancelText: 'CANCEL', + title: + context.translations.deleteConversationLabel, + question: context + .translations.deleteConversationQuestion, + okText: context.translations.deleteLabel, + cancelText: context.translations.cancelLabel, icon: StreamSvgIcon.delete( color: chatThemeData.colorTheme.accentRed, ), @@ -666,10 +668,10 @@ class _ChannelListViewState extends State { .colorTheme .accentRed .withOpacity(.2), - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 16), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), child: Center( - child: Text('Error loading channels'), + child: Text(context.translations.loadingChannelsError), ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index 4819ecc7..ef5dcdee 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// It shows the current [Channel] name using a [Text] widget. /// @@ -44,8 +45,10 @@ class ChannelName extends StatelessWidget { ) => LayoutBuilder( builder: (context, constraints) { - var title = 'No title'; - if (extraData['name'] == null) { + var title = context.translations.noTitleText; + if (extraData['name'] != null) { + title = extraData['name']; + } else { final otherMembers = members?.where((member) => member.userId != client.user!.id); if (otherMembers?.length == 1) { @@ -71,8 +74,6 @@ class ChannelName extends StatelessWidget { title = '${currentMembers.map((e) => e.user?.name).join(', ')} ' '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; } - } else { - title = extraData['name']; } return Text( diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 231b5f94..f21625c6 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -6,6 +6,7 @@ import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png) @@ -183,7 +184,7 @@ class ChannelPreview extends StatelessWidget { startOfDay .subtract(const Duration(days: 1)) .millisecondsSinceEpoch) { - stringDate = 'Yesterday'; + stringDate = context.translations.yesterdayLabel; } else if (startOfDay.difference(lastMessageAt).inDays < 7) { stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; } else { @@ -208,7 +209,7 @@ class ChannelPreview extends StatelessWidget { size: 16, ), Text( - ' Channel is muted', + context.translations.channelIsMutedText, style: chatThemeData.channelPreviewTheme.subtitle, ), ], diff --git a/packages/stream_chat_flutter/lib/src/date_divider.dart b/packages/stream_chat_flutter/lib/src/date_divider.dart index 185683be..e23f0233 100644 --- a/packages/stream_chat_flutter/lib/src/date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/date_divider.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// It shows a date divider depending on the date difference class DateDivider extends StatelessWidget { @@ -24,10 +25,10 @@ class DateDivider extends StatelessWidget { String dayInfo; if (Jiffy(createdAt).isSame(now, Units.DAY)) { - dayInfo = 'Today'; + dayInfo = context.translations.todayLabel; } else if (Jiffy(createdAt) .isSame(now.subtract(const Duration(days: 1)), Units.DAY)) { - dayInfo = 'Yesterday'; + dayInfo = context.translations.yesterdayLabel; } else if (Jiffy(createdAt).isAfter( now.subtract(const Duration(days: 7)), Units.DAY, diff --git a/packages/stream_chat_flutter/lib/src/deleted_message.dart b/packages/stream_chat_flutter/lib/src/deleted_message.dart index 9a4e0ae2..e62a7cfa 100644 --- a/packages/stream_chat_flutter/lib/src/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/deleted_message.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Widget to display deleted message class DeletedMessage extends StatelessWidget { @@ -49,7 +50,7 @@ class DeletedMessage extends StatelessWidget { horizontal: 16, ), child: Text( - 'Message deleted', + context.translations.messageDeletedLabel, style: messageTheme.messageText?.copyWith( fontStyle: FontStyle.italic, color: messageTheme.createdAt?.color, diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 3a495ca5..b185ae3b 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -2,6 +2,7 @@ import 'package:characters/characters.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart'; +import 'package:stream_chat_flutter/src/localization/translations.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; final _emojiChars = Emoji.chars(); @@ -104,7 +105,8 @@ extension BuildContextX on BuildContext { double get textScaleFactor => MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0; - StreamChatLocalizations? get translations => StreamChatLocalizations.of(this); + Translations get translations => + StreamChatLocalizations.of(this) ?? DefaultTranslations.instance; } /// Extension on [BorderRadius] diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index f86ac60b..6810816f 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -10,6 +10,7 @@ import 'package:stream_chat_flutter/src/image_footer.dart'; import 'package:stream_chat_flutter/src/image_header.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_player/video_player.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Return action for coming back from pages enum ReturnActionType { @@ -182,9 +183,11 @@ class _FullScreenMediaState extends State children: [ ImageHeader( userName: widget.userName, - sentAt: - // ignore: lines_longer_than_80_chars - 'Sent ${getDay(widget.message.createdAt.toLocal())} at ${Jiffy(widget.message.createdAt.toLocal()).format('HH:mm')}', + // TODO: Fix this + sentAt: context.translations.sentAtText( + date: widget.message.createdAt, + time: widget.message.createdAt, + ), onBackPressed: () { Navigator.of(context).pop(); }, diff --git a/packages/stream_chat_flutter/lib/src/image_footer.dart b/packages/stream_chat_flutter/lib/src/image_footer.dart index 0a6571cc..3d12bf94 100644 --- a/packages/stream_chat_flutter/lib/src/image_footer.dart +++ b/packages/stream_chat_flutter/lib/src/image_footer.dart @@ -11,6 +11,7 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Footer widget for media display class ImageFooter extends StatefulWidget implements PreferredSizeWidget { @@ -190,7 +191,7 @@ class _ImageFooterState extends State { child: Padding( padding: const EdgeInsets.all(16), child: Text( - 'Photos', + context.translations.photosLabel, style: chatThemeData.textTheme.headlineBold, ), ), diff --git a/packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart b/packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart new file mode 100644 index 00000000..b2a1bd5e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart @@ -0,0 +1,36 @@ +import 'package:flutter/widgets.dart'; + +import 'package:stream_chat_flutter/src/localization/translations.dart' + show Translations; + +/// Defines the localized resource values used by the StreamChatFlutter widgets. +/// +/// See also: +/// +/// * [GlobalStreamChatLocalizations], which provides material localizations +/// for many languages. +abstract class StreamChatLocalizations implements Translations { + /// The `StreamChatLocalizations` from the closest [Localizations] instance + /// that encloses the given context. + /// + /// If no [StreamChatLocalizations] are available in the given `context`, this + /// method returns null. + /// + /// This method is just a convenient shorthand for: + /// `Localizations.of( + /// context, + /// StreamChatLocalizations + /// )`. + /// + /// References to the localized resources defined by this class are typically + /// written in terms of this method. For example: + /// + /// ```dart + /// tooltip: StreamChatLocalizations.of(context).streamChatLabel, + /// ``` + static StreamChatLocalizations? of(BuildContext context) => + Localizations.of( + context, + StreamChatLocalizations, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart new file mode 100644 index 00000000..651f97f8 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -0,0 +1,517 @@ +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart' + show User; + +abstract class Translations { + String get launchUrlError; + + String get loadingUsersError; + + String get retryLabel; + + String get noUsersLabel; + + String get userOnlineText; + + String get userLastOnlineText; + + String userTypingText(Iterable users); + + String get threadReplyLabel; + + String get onlyVisibleToYouText; + + String threadReplyCountText(int count); + + String attachmentsUploadProgressText({ + required int remaining, + required int total, + }); + + String pinnedByUserText({ + required User pinnedBy, + required User currentUser, + }); + + String get emptyMessagesText; + + String get genericErrorText; + + String get loadingMessagesError; + + String resultCountText(int count); + + String get messageDeletedText; + + String get messageDeletedLabel; + + String get messageReactionsText; + + String get emptyChatMessagesText; + + String threadSeparatorText(int replyCount); + + String get connectedLabel; + + String get disconnectedLabel; + + String get reconnectingLabel; + + String get alsoSendAsDirectMessageLabel; + + String get searchGifLabel; + + String get addACommentOrSendLabel; + + String get writeAMessageLabel; + + String get instantCommandsLabel; + + String get fileTooLargeAfterCompressionError; + + String get fileTooLargeError; + + String emojiMatchingQueryText(String query); + + String get addAFileLabel; + + String get uploadAPhotoLabel; + + String get uploadAVideoLabel; + + String get photoFromCameraLabel; + + String get videoFromCameraLabel; + + String get uploadAFileLabel; + + String get somethingWentWrongLabel; + + String get okLabel; + + String get addMoreFilesLabel; + + String get enablePhotoAndVideoAccessMessage; + + String get allowGalleryAccessMessage; + + String get flagMessageLabel; + + String get flagMessageQuestion; + + String get flagLabel; + + String get cancelLabel; + + String get flagMessageSuccessfulLabel; + + String get flagMessageSuccessfulText; + + String get deleteMessageLabel; + + String get deleteMessageQuestion; + + String get deleteLabel; + + String get operationCouldNotBeCompletedText; + + String get replyLabel; + + String togglePinUnpinText({required bool pinned}); + + String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}); + + String get copyMessageLabel; + + String get editMessageLabel; + + String toggleResendOrResendEditedMessage({required bool isUpdateFailed}); + + String get photosLabel; + + String sentAtText({required DateTime date, required DateTime time}); + + String get todayLabel; + + String get yesterdayLabel; + + String get channelIsMutedText; + + String get noTitleText; + + String get letsStartChattingLabel; + + String get sendingFirstMessageLabel; + + String get startAChatLabel; + + String get loadingChannelsError; + + // title: 'Delete Conversation', +// okText: 'DELETE', +// question: +// 'Are you sure you want to delete this conversation?', + + String get deleteConversationLabel; + + String get deleteConversationQuestion; + + String get streamChatLabel; + + String get searchingForNetworkLabel; + + String get offlineLabel; + + String get tryAgainLabel; + + String membersCountText(int count); + + String watchersCountText(int count); + + String get viewInfoLabel; + + String get leaveGroupLabel; + + String get leaveLabel; + + String get leaveConversationLabel; + + String get leaveConversationQuestion; + + String get showInChatLabel; + + String get saveImageLabel; + + String get saveVideoLabel; + + String get uploadErrorLabel; + + String get giphyLabel; + + String get shuffleLabel; + + String get sendLabel; +} + +class DefaultTranslations implements Translations { + const DefaultTranslations._(); + + static const instance = DefaultTranslations._(); + + @override + String get launchUrlError => 'Cannot launch the url'; + + @override + String get loadingUsersError => 'Error loading users'; + + @override + String get noUsersLabel => 'There are no users currently'; + + @override + String get retryLabel => 'Retry'; + + @override + String get userLastOnlineText => 'Last online'; + + @override + String get userOnlineText => 'Online'; + + @override + String userTypingText(Iterable users) { + if (users.isEmpty) return ''; + final first = users.first; + if (users.length == 1) { + return '${first.name} is typing'; + } + return '${first.name} and ${users.length - 1} more are typing'; + } + + @override + String get threadReplyLabel => 'Thread Reply'; + + @override + String get onlyVisibleToYouText => 'Only visible to you'; + + @override + String threadReplyCountText(int count) => '$count Thread Replies'; + + @override + String attachmentsUploadProgressText({ + required int remaining, + required int total, + }) => + 'Uploading $remaining/$total ...'; + + @override + String pinnedByUserText({ + required User pinnedBy, + required User currentUser, + }) { + final pinnedByCurrentUser = currentUser.id == pinnedBy.id; + if (pinnedByCurrentUser) return 'Pinned by You'; + return 'Pinned by ${pinnedBy.name}'; + } + + @override + String get emptyMessagesText => 'There are no messages currently'; + + @override + String get genericErrorText => 'Something went wrong'; + + @override + String get loadingMessagesError => 'Error loading messages'; + + @override + String resultCountText(int count) => '$count results'; + + @override + String get messageDeletedText => 'This message was deleted.'; + + @override + String get messageDeletedLabel => 'Message deleted'; + + @override + String get messageReactionsText => 'Message Reactions'; + + @override + String get emptyChatMessagesText => 'No chats here yet...'; + + @override + String threadSeparatorText(int replyCount) { + if (replyCount == 1) return '1 Reply'; + return '$replyCount Replies'; + } + + @override + String get connectedLabel => 'Connected'; + + @override + String get disconnectedLabel => 'Disconnected'; + + @override + String get reconnectingLabel => 'Reconnecting...'; + + @override + String get alsoSendAsDirectMessageLabel => 'Also send as direct message'; + + @override + String get addACommentOrSendLabel => 'Add a comment or send'; + + @override + String get searchGifLabel => 'Search GIFs'; + + @override + String get writeAMessageLabel => 'Write a message'; + + @override + String get instantCommandsLabel => 'Instant Commands'; + + @override + String get fileTooLargeAfterCompressionError => + 'The file is too large to upload. ' + 'The file size limit is 20MB. ' + 'We tried compressing it, but it was not enough.'; + + @override + String get fileTooLargeError => + 'The file is too large to upload. The file size limit is 20MB.'; + + @override + String emojiMatchingQueryText(String query) => 'Emoji matching "$query"'; + + @override + String get addAFileLabel => 'Add a file'; + + @override + String get photoFromCameraLabel => 'Photo from camera'; + + @override + String get uploadAFileLabel => 'Upload a file'; + + @override + String get uploadAPhotoLabel => 'Upload a photo'; + + @override + String get uploadAVideoLabel => 'Upload a video'; + + @override + String get videoFromCameraLabel => 'Video from camera'; + + @override + String get okLabel => 'OK'; + + @override + String get somethingWentWrongLabel => 'Something went wrong'; + + @override + String get addMoreFilesLabel => 'Add more files'; + + @override + String get enablePhotoAndVideoAccessMessage => + 'Please enable access to your photos' + '\nand videos so you can share them with friends.'; + + @override + String get allowGalleryAccessMessage => 'Allow access to your gallery'; + + @override + String get flagMessageLabel => 'Flag Message'; + + @override + String get flagMessageQuestion => + 'Do you want to send a copy of this message to a' + '\nmoderator for further investigation?'; + + @override + String get flagLabel => 'FLAG'; + + @override + String get cancelLabel => 'CANCEL'; + + @override + String get flagMessageSuccessfulLabel => 'Message flagged'; + + @override + String get flagMessageSuccessfulText => + 'The message has been reported to a moderator.'; + + @override + String get deleteLabel => 'DELETE'; + + @override + String get deleteMessageLabel => 'Delete Message'; + + @override + String get deleteMessageQuestion => + 'Are you sure you want to permanently delete this\nmessage?'; + + @override + String get operationCouldNotBeCompletedText => + 'The operation couldn\'t be completed.'; + + @override + String get replyLabel => 'Reply'; + + @override + String togglePinUnpinText({required bool pinned}) { + if (pinned) return 'Unpin from Conversation'; + return 'Pin to Conversation'; + } + + @override + String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) { + if (isDeleteFailed) return 'Retry Deleting Message'; + return 'Delete Message'; + } + + @override + String get copyMessageLabel => 'Copy Message'; + + @override + String get editMessageLabel => 'Edit Message'; + + @override + String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) { + if (isUpdateFailed) return 'Resend Edited Message'; + return 'Resend'; + } + + @override + String get photosLabel => 'Photos'; + + @override + String sentAtText({required DateTime date, required DateTime time}) => + 'Sent $date at $time'; + + @override + String get todayLabel => 'Today'; + + @override + String get yesterdayLabel => 'Yesterday'; + + @override + String get channelIsMutedText => ' Channel is muted'; + + @override + String get noTitleText => 'No title'; + + @override + String get letsStartChattingLabel => 'Let’s start chatting!'; + + @override + String get sendingFirstMessageLabel => + 'How about sending your first message to a friend?'; + + @override + String get startAChatLabel => 'Start a chat'; + + @override + String get loadingChannelsError => 'Error loading channels'; + + @override + String get deleteConversationLabel => 'Delete Conversation'; + + @override + String get deleteConversationQuestion => + 'Are you sure you want to delete this conversation?'; + + @override + String get streamChatLabel => 'Stream Chat'; + + @override + String get searchingForNetworkLabel => 'Searching for Network'; + + @override + String get offlineLabel => 'Offline...'; + + @override + String get tryAgainLabel => 'Try Again'; + + @override + String membersCountText(int count) { + if (count == 1) return '1 Member'; + return '$count Members'; + } + + @override + String watchersCountText(int count) { + if (count == 1) return '1 Online'; + return '$count Online'; + } + + @override + String get viewInfoLabel => 'View Info'; + + @override + String get leaveGroupLabel => 'Leave Group'; + + @override + String get leaveLabel => 'LEAVE'; + + @override + String get leaveConversationLabel => 'Leave conversation'; + + @override + String get leaveConversationQuestion => + 'Are you sure you want to leave this conversation?'; + + @override + String get showInChatLabel => 'Show in Chat'; + + @override + String get saveImageLabel => 'Save Image'; + + @override + String get saveVideoLabel => 'Save Video'; + + @override + String get uploadErrorLabel => 'UPLOAD ERROR'; + + @override + String get giphyLabel => 'Giphy'; + + @override + String get shuffleLabel => 'Shuffle'; + + @override + String get sendLabel => 'Send'; +} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 296b1e59..b7ba4655 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -268,16 +268,14 @@ class _MessageActionsModalState extends State { final streamChatThemeData = StreamChatTheme.of(context); final answer = await showConfirmationDialog( context, - title: 'Flag Message', + title: context.translations.flagMessageLabel, icon: StreamSvgIcon.flag( color: streamChatThemeData.colorTheme.accentRed, size: 24, ), - question: - // ignore: lines_longer_than_80_chars - 'Do you want to send a copy of this message to a\nmoderator for further investigation?', - okText: 'FLAG', - cancelText: 'CANCEL', + question: context.translations.flagMessageQuestion, + okText: context.translations.okLabel, + cancelText: context.translations.cancelLabel, ); final theme = streamChatThemeData; @@ -290,9 +288,9 @@ class _MessageActionsModalState extends State { color: theme.colorTheme.accentRed, size: 24, ), - details: 'The message has been reported to a moderator.', - title: 'Message flagged', - okText: 'OK', + details: context.translations.flagMessageSuccessfulText, + title: context.translations.flagMessageSuccessfulLabel, + okText: context.translations.okLabel, ); } catch (err) { if (err is StreamChatNetworkError && @@ -303,9 +301,9 @@ class _MessageActionsModalState extends State { color: theme.colorTheme.accentRed, size: 24, ), - details: 'The message has been reported to a moderator.', - title: 'Message flagged', - okText: 'OK', + details: context.translations.flagMessageSuccessfulText, + title: context.translations.flagMessageSuccessfulLabel, + okText: context.translations.okLabel, ); } else { _showErrorAlert(); @@ -335,14 +333,14 @@ class _MessageActionsModalState extends State { }); final answer = await showConfirmationDialog( context, - title: 'Delete message', + title: context.translations.deleteMessageLabel, icon: StreamSvgIcon.flag( color: StreamChatTheme.of(context).colorTheme.accentRed, size: 24, ), - question: 'Are you sure you want to permanently delete this\nmessage?', - okText: 'DELETE', - cancelText: 'CANCEL', + question: context.translations.deleteMessageQuestion, + okText: context.translations.deleteLabel, + cancelText: context.translations.cancelLabel, ); if (answer == true) { @@ -366,9 +364,9 @@ class _MessageActionsModalState extends State { color: StreamChatTheme.of(context).colorTheme.accentRed, size: 24, ), - details: 'The operation couldn\'t be completed.', - title: 'Something went wrong', - okText: 'OK', + details: context.translations.operationCouldNotBeCompletedText, + title: context.translations.somethingWentWrongLabel, + okText: context.translations.okLabel, ); } @@ -390,7 +388,7 @@ class _MessageActionsModalState extends State { ), const SizedBox(width: 16), Text( - 'Reply', + context.translations.replyLabel, style: streamChatThemeData.textTheme.body, ), ], @@ -412,7 +410,7 @@ class _MessageActionsModalState extends State { ), const SizedBox(width: 16), Text( - 'Flag Message', + context.translations.flagMessageLabel, style: streamChatThemeData.textTheme.body, ), ], @@ -435,7 +433,9 @@ class _MessageActionsModalState extends State { ), const SizedBox(width: 16), Text( - '${widget.message.pinned ? 'Unpin from' : 'Pin to'} Conversation', + context.translations.togglePinUnpinText( + pinned: widget.message.pinned, + ), style: streamChatThemeData.textTheme.body, ), ], @@ -458,7 +458,9 @@ class _MessageActionsModalState extends State { ), const SizedBox(width: 16), Text( - isDeleteFailed ? 'Retry Deleting Message' : 'Delete Message', + context.translations.toggleDeleteRetryDeleteMessageText( + isDeleteFailed: isDeleteFailed, + ), style: StreamChatTheme.of(context) .textTheme .body @@ -487,7 +489,7 @@ class _MessageActionsModalState extends State { ), const SizedBox(width: 16), Text( - 'Copy Message', + context.translations.copyMessageLabel, style: streamChatThemeData.textTheme.body, ), ], @@ -512,7 +514,7 @@ class _MessageActionsModalState extends State { ), const SizedBox(width: 16), Text( - 'Edit Message', + context.translations.editMessageLabel, style: streamChatThemeData.textTheme.body, ), ], @@ -544,7 +546,9 @@ class _MessageActionsModalState extends State { ), const SizedBox(width: 16), Text( - isUpdateFailed ? 'Resend Edited Message' : 'Resend', + context.translations.toggleResendOrResendEditedMessage( + isUpdateFailed: isUpdateFailed, + ), style: streamChatThemeData.textTheme.body, ), ], @@ -588,8 +592,8 @@ class _MessageActionsModalState extends State { color: streamChatThemeData.colorTheme.greyGainsboro, ), ), - const Text( - 'Edit Message', + Text( + context.translations.editMessageLabel, style: TextStyle(fontWeight: FontWeight.bold), ), IconButton( @@ -636,7 +640,7 @@ class _MessageActionsModalState extends State { ), const SizedBox(width: 16), Text( - 'Thread Reply', + context.translations.threadReplyLabel, style: streamChatThemeData.textTheme.body, ), ], diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 39211116..8bea5313 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -24,6 +24,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:substring_highlight/substring_highlight.dart'; import 'package:video_compress/video_compress.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Builder for attachment thumbnails typedef AttachmentThumbnailBuilder = Widget Function( @@ -229,16 +230,12 @@ class MessageInput extends StatefulWidget { /// Use this method to get the current [StreamChatState] instance static MessageInputState of(BuildContext context) { MessageInputState? messageInputState; - messageInputState = context.findAncestorStateOfType(); - - if (messageInputState == null) { - throw Exception( - // ignore: lines_longer_than_80_chars - 'You must have a MessageInput widget as ancestor of your widget tree'); - } - - return messageInputState; + assert( + messageInputState != null, + 'You must have a MessageInput widget as ancestor of your widget tree', + ); + return messageInputState!; } } @@ -440,7 +437,7 @@ class MessageInputState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Text( - 'Also send as direct message', + context.translations.alsoSendAsDirectMessageLabel, style: _streamChatTheme.textTheme.footnote.copyWith( color: _streamChatTheme.colorTheme.black.withOpacity(0.5), ), @@ -564,7 +561,7 @@ class MessageInputState extends State { style: _streamChatTheme.messageInputTheme.inputTextStyle, autofocus: widget.autofocus, textAlignVertical: TextAlignVertical.center, - decoration: _getInputDecoration(), + decoration: _getInputDecoration(context), textCapitalization: TextCapitalization.sentences, ), ) @@ -576,11 +573,11 @@ class MessageInputState extends State { ); } - InputDecoration _getInputDecoration() { + InputDecoration _getInputDecoration(BuildContext context) { final passedDecoration = _streamChatTheme.messageInputTheme.inputDecoration; return InputDecoration( isDense: true, - hintText: _getHint(), + hintText: _getHint(context), hintStyle: _streamChatTheme.messageInputTheme.inputTextStyle!.copyWith( color: _streamChatTheme.colorTheme.grey, ), @@ -729,14 +726,14 @@ class MessageInputState extends State { ); } - String _getHint() { + String _getHint(BuildContext context) { if (_commandEnabled && _chosenCommand!.name == 'giphy') { - return 'Search GIFs'; + return context.translations.searchGifLabel; } if (_attachments.isNotEmpty) { - return 'Add a comment or send'; + return context.translations.addACommentOrSendLabel; } - return 'Write a message'; + return context.translations.writeAMessageLabel; } void _checkEmoji(String s, BuildContext context) { @@ -859,7 +856,7 @@ class MessageInputState extends State { ), ), Text( - 'Instant Commands', + context.translations.instantCommandsLabel, style: TextStyle( color: _streamChatTheme.colorTheme.black.withOpacity(.5), @@ -1114,8 +1111,7 @@ class MessageInputState extends State { if (mediaInfo.filesize! > widget.maxAttachmentSize) { _showErrorAlert( - // ignore: lines_longer_than_80_chars - 'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.', + context.translations.fileTooLargeAfterCompressionError, ); return; } @@ -1126,9 +1122,7 @@ class MessageInputState extends State { path: mediaInfo.path, ); } else { - _showErrorAlert( - 'The file is too large to upload. The file size limit is 20MB.', - ); + _showErrorAlert(context.translations.fileTooLargeError); return; } } @@ -1396,7 +1390,9 @@ class MessageInputState extends State { ), Flexible( child: Text( - 'Emoji matching "$query"', + context.translations.emojiMatchingQueryText( + query, + ), style: TextStyle( color: _streamChatTheme.colorTheme.black .withOpacity(.5), @@ -1744,17 +1740,17 @@ class MessageInputState extends State { builder: (_) => Column( mainAxisSize: MainAxisSize.min, children: [ - const ListTile( + ListTile( title: Text( - 'Add a file', - style: TextStyle( + context.translations.addAFileLabel, + style: const TextStyle( fontWeight: FontWeight.bold, ), ), ), ListTile( leading: const Icon(Icons.image), - title: const Text('Upload a photo'), + title: Text(context.translations.uploadAPhotoLabel), onTap: () { pickFile(DefaultAttachmentTypes.image); Navigator.pop(context); @@ -1762,7 +1758,7 @@ class MessageInputState extends State { ), ListTile( leading: const Icon(Icons.video_library), - title: const Text('Upload a video'), + title: Text(context.translations.uploadAVideoLabel), onTap: () { pickFile(DefaultAttachmentTypes.video); Navigator.pop(context); @@ -1771,7 +1767,7 @@ class MessageInputState extends State { if (!kIsWeb) ListTile( leading: const Icon(Icons.camera_alt), - title: const Text('Photo from camera'), + title: Text(context.translations.photoFromCameraLabel), onTap: () { pickFile(DefaultAttachmentTypes.image, true); Navigator.pop(context); @@ -1780,7 +1776,7 @@ class MessageInputState extends State { if (!kIsWeb) ListTile( leading: const Icon(Icons.videocam), - title: const Text('Video from camera'), + title: Text(context.translations.videoFromCameraLabel), onTap: () { pickFile(DefaultAttachmentTypes.video, true); Navigator.pop(context); @@ -1788,7 +1784,7 @@ class MessageInputState extends State { ), ListTile( leading: const Icon(Icons.insert_drive_file), - title: const Text('Upload a file'), + title: Text(context.translations.uploadAFileLabel), onTap: () { pickFile(DefaultAttachmentTypes.file); Navigator.pop(context); @@ -1888,8 +1884,7 @@ class MessageInputState extends State { if (mediaInfo.filesize! > widget.maxAttachmentSize) { _showErrorAlert( - // ignore: lines_longer_than_80_chars - 'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.', + context.translations.fileTooLargeAfterCompressionError, ); return; } @@ -1900,9 +1895,7 @@ class MessageInputState extends State { path: mediaInfo.path, ); } else { - _showErrorAlert( - 'The file is too large to upload. The file size limit is 20MB.', - ); + _showErrorAlert(context.translations.fileTooLargeError); return; } } @@ -2079,7 +2072,7 @@ class MessageInputState extends State { height: 26, ), Text( - 'Something went wrong', + context.translations.somethingWentWrongLabel, style: _streamChatTheme.textTheme.headlineBold, ), const SizedBox( @@ -2107,7 +2100,7 @@ class MessageInputState extends State { Navigator.of(context).pop(); }, child: Text( - 'OK', + context.translations.okLabel, style: _streamChatTheme.textTheme.bodyBold .copyWith(color: _streamChatTheme.colorTheme.accentBlue), ), @@ -2253,7 +2246,7 @@ class __PickerWidgetState extends State<_PickerWidget> { color: widget.streamChatTheme.colorTheme.whiteSmoke, alignment: Alignment.center, child: Text( - 'Add more files', + context.translations.addMoreFilesLabel, style: TextStyle( color: widget.streamChatTheme.colorTheme.accentBlue, fontWeight: FontWeight.bold, @@ -2285,8 +2278,7 @@ class __PickerWidgetState extends State<_PickerWidget> { color: widget.streamChatTheme.colorTheme.greyGainsboro, ), Text( - // ignore: lines_longer_than_80_chars - 'Please enable access to your photos \nand videos so you can share them with friends.', + context.translations.enablePhotoAndVideoAccessMessage, style: widget.streamChatTheme.textTheme.body.copyWith( color: widget.streamChatTheme.colorTheme.grey), textAlign: TextAlign.center, @@ -2294,7 +2286,7 @@ class __PickerWidgetState extends State<_PickerWidget> { const SizedBox(height: 6), Center( child: Text( - 'Allow access to your gallery', + context.translations.allowGalleryAccessMessage, style: widget.streamChatTheme.textTheme.bodyBold.copyWith( color: widget.streamChatTheme.colorTheme.accentBlue, ), diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index e6bf7a17..72a2a314 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -17,6 +17,7 @@ import 'package:stream_chat_flutter/src/system_message.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:visibility_detector/visibility_detector.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Widget builder for message /// [defaultMessageWidget] is the default [MessageWidget] configuration @@ -353,6 +354,7 @@ class _MessageListViewState extends State { bool _inBetweenList = false; late final _defaultController = MessageListController(); + MessageListController get _messageListController => widget.messageListController ?? _defaultController; @@ -366,7 +368,7 @@ class _MessageListViewState extends State { emptyBuilder: widget.emptyBuilder ?? (context) => Center( child: Text( - 'No chats here yet...', + context.translations.emptyChatMessagesText, style: _streamTheme.textTheme.footnote.copyWith( color: _streamTheme.colorTheme.black.withOpacity(.5)), ), @@ -378,7 +380,7 @@ class _MessageListViewState extends State { errorWidgetBuilder: widget.errorWidgetBuilder ?? (BuildContext context, Object error) => Center( child: Text( - 'Something went wrong', + context.translations.genericErrorText, style: _streamTheme.textTheme.footnote.copyWith( color: _streamTheme.colorTheme.black.withOpacity(.5)), ), @@ -423,14 +425,14 @@ class _MessageListViewState extends State { var showStatus = true; switch (status) { case ConnectionStatus.connected: - statusString = 'Connected'; + statusString = context.translations.connectedLabel; showStatus = false; break; case ConnectionStatus.connecting: - statusString = 'Reconnecting...'; + statusString = context.translations.reconnectingLabel; break; case ConnectionStatus.disconnected: - statusString = 'Disconnected'; + statusString = context.translations.disconnectedLabel; break; } @@ -619,7 +621,7 @@ class _MessageListViewState extends State { return widget.threadSeparatorBuilder!.call(context); } - final replyCount = widget.parentMessage!.replyCount; + final replyCount = widget.parentMessage!.replyCount!; return DecoratedBox( decoration: BoxDecoration( gradient: _streamTheme.colorTheme.bgGradient, @@ -627,7 +629,7 @@ class _MessageListViewState extends State { child: Padding( padding: const EdgeInsets.all(8), child: Text( - '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', + context.translations.threadSeparatorText(replyCount), textAlign: TextAlign.center, style: _streamTheme.channelTheme.channelHeaderTheme.subtitle, ), @@ -1253,8 +1255,8 @@ class _LoadingIndicator extends StatelessWidget { initialData: false, errorBuilder: (context, error) => Container( color: streamTheme.colorTheme.accentRed.withOpacity(.2), - child: const Center( - child: Text('Error loading messages'), + child: Center( + child: Text(context.translations.loadingMessagesError), ), ), builder: (context, data) { diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 0292a05a..f87b8740 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -7,6 +7,7 @@ import 'package:stream_chat_flutter/src/stream_chat.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Modal widget for displaying message reactions class MessageReactionsModal extends StatelessWidget { @@ -154,7 +155,7 @@ class MessageReactionsModal extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - 'Message Reactions', + context.translations.messageReactionsText, style: chatThemeData.textTheme.headlineBold, ), const SizedBox(height: 16), diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart index 1f8a31ea..0444d37f 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// It shows the current [Message] preview. /// @@ -98,7 +99,7 @@ class MessageSearchItem extends StatelessWidget { Widget _buildSubtitle(BuildContext context, Message message) { var text = message.text; if (message.isDeleted) { - text = 'This message was deleted.'; + text = context.translations.messageDeletedText; } else if (message.attachments.isNotEmpty) { final parts = [ ...message.attachments.map((e) { diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index 1a67702f..9d6d36d6 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -3,6 +3,7 @@ import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/message_search_item.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Callback called when tapping on a user typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); @@ -140,6 +141,7 @@ class MessageSearchListView extends StatefulWidget { class _MessageSearchListViewState extends State { late final _defaultController = MessageSearchListController(); + MessageSearchListController get _messageSearchListController => widget.messageSearchListController ?? _defaultController; @@ -160,8 +162,8 @@ class _MessageSearchListViewState extends State { constraints: BoxConstraints( minHeight: viewportConstraints.maxHeight, ), - child: const Center( - child: Text('There are no messages currently'), + child: Center( + child: Text(context.translations.emptyMessagesText), ), ), ), @@ -175,7 +177,7 @@ class _MessageSearchListViewState extends State { showMessage: widget.showErrorTile, tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, - message: 'An error occurred.', + message: context.translations.genericErrorText, child: Container(), ); }, @@ -226,10 +228,10 @@ class _MessageSearchListViewState extends State { .colorTheme .accentRed .withOpacity(.2), - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 16), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), child: Center( - child: Text('Error loading messages'), + child: Text(context.translations.loadingMessagesError), ), ), ); @@ -292,7 +294,7 @@ class _MessageSearchListViewState extends State { horizontal: 8, ), child: Text( - '${items.length} results', + context.translations.resultCountText(items.length), style: TextStyle( color: chatThemeData.colorTheme.grey, ), diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index b0bd13ca..6d006085 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Text widget to display in message class MessageText extends StatelessWidget { @@ -30,8 +29,6 @@ class MessageText extends StatelessWidget { @override Widget build(BuildContext context) { - final texts = context.translations?.launchUrlError ?? 'defaultValue'; - return Text(texts); final text = _replaceMentions(message.text ?? '').replaceAll('\n', '\n\n'); final themeData = Theme.of(context); diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 7c95bf0a..03c77a44 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -16,6 +16,7 @@ import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/src/url_attachment.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Widget builder for building attachments typedef AttachmentBuilder = Widget Function( @@ -840,7 +841,7 @@ class _MessageWidgetState extends State ), const SizedBox(width: 8), Text( - 'Only visible to you', + context.translations.onlyVisibleToYouText, style: chatThemeData.textTheme.footnote .copyWith(color: chatThemeData.colorTheme.grey), ), @@ -854,9 +855,9 @@ class _MessageWidgetState extends State final showThreadParticipants = threadParticipants?.isNotEmpty == true; final replyCount = widget.message.replyCount; - var msg = 'Thread Reply'; + var msg = context.translations.threadReplyLabel; if (showThreadReplyIndicator && replyCount! > 1) { - msg = '$replyCount Thread Replies'; + msg = context.translations.threadReplyCountText(replyCount); } // ignore: prefer_function_declarations_over_variables @@ -1201,7 +1202,10 @@ class _MessageWidgetState extends State ); } return Text( - 'Uploading $uploadRemaining/$totalAttachments ...', + context.translations.attachmentsUploadProgressText( + remaining: uploadRemaining, + total: totalAttachments, + ), style: style, ); } @@ -1275,8 +1279,8 @@ class _MessageWidgetState extends State } Widget _buildPinnedMessage(Message message) { - final pinnedBy = message.pinnedBy; - final pinnedByMe = _streamChat.user!.id == pinnedBy!.id; + final pinnedBy = message.pinnedBy!; + final currentUser = _streamChat.user!; return Padding( padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8), @@ -1290,7 +1294,10 @@ class _MessageWidgetState extends State width: 4, ), Text( - 'Pinned by ${pinnedByMe ? 'You' : pinnedBy.name}', + context.translations.pinnedByUserText( + pinnedBy: pinnedBy, + currentUser: currentUser, + ), style: TextStyle( color: _streamChatTheme.colorTheme.grey, fontSize: 13, diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_localizations.dart b/packages/stream_chat_flutter/lib/src/stream_chat_localizations.dart deleted file mode 100644 index b1d81fde..00000000 --- a/packages/stream_chat_flutter/lib/src/stream_chat_localizations.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:flutter/widgets.dart'; - -// TODO : Fix localization instructions -// ADDING A NEW STRING -// -// If you (someone contributing to the Stream Chat Flutter) want to add a new -// string to the StreamChatLocalizations object (e.g. because you've added a new -// widget and it has a tooltip), follow these steps: -// -// 1. Add the new getter to StreamChatLocalizations below. -// -// 2. Implement a default value in DefaultMaterialLocalizations below. -// -// 3. Add a test to test/material/localizations_test.dart that verifies that -// this new value is implemented. -// -// 4. Update the flutter_localizations package. To add a new string to the -// flutter_localizations package, you must first add it to the English -// translations (lib/src/l10n/en.json), including a description. -// -// Then you need to add new entries for the string to all of the other -// language locale files by running: -// ``` -// dart dev/tools/localization/bin/gen_missing_localizations.dart -// ``` -// Which will copy the english strings into the other locales as placeholders -// until they can be translated. -// -// Finally you need to re-generate lib/src/l10n/localizations.dart by running: -// ``` -// dart dev/tools/localization/bin/gen_localizations.dart --overwrite -// ``` -// -// There is a README file with further information in the lib/src/l10n/ -// directory. -// -// 5. If you are a Google employee, you should then also follow the instructions -// at go/flutter-l10n. If you're not, don't worry about it. -// -// UPDATING AN EXISTING STRING -// -// If you (someone contributing to the Flutter framework) want to modify an -// existing string in the MaterialLocalizations objects, follow these steps: -// -// 1. Modify the default value of the relevant getter(s) in -// DefaultMaterialLocalizations below. -// -// 2. Update the flutter_localizations package. Modify the out-of-date English -// strings in lib/src/l10n/material_en.arb. -// -// You also need to re-generate lib/src/l10n/localizations.dart by running: -// ``` -// dart dev/tools/localization/bin/gen_localizations.dart --overwrite -// ``` -// -// This script may result in your updated getters being created in newer -// locales and set to the old value of the strings. This is to be expected. -// Leave them as they were generated, and they will be picked up for -// translation. -// -// There is a README file with further information in the lib/src/l10n/ -// directory. -// -// 3. If you are a Google employee, you should then also follow the instructions -// at go/flutter-l10n. If you're not, don't worry about it. - -/// Defines the localized resource values used by the StreamChatFlutter widgets. -/// -/// See also: -/// -/// * [GlobalStreamChatLocalizations], which provides material localizations -/// for many languages. -abstract class StreamChatLocalizations { - /// The `StreamChatLocalizations` from the closest [Localizations] instance - /// that encloses the given context. - /// - /// If no [StreamChatLocalizations] are available in the given `context`, this - /// method returns null. - /// - /// This method is just a convenient shorthand for: - /// `Localizations.of(context, StreamChatLocalizations)`. - /// - /// References to the localized resources defined by this class are typically - /// written in terms of this method. For example: - /// - /// ```dart - /// tooltip: StreamChatLocalizations.of(context).backButtonTooltip, - /// ``` - static StreamChatLocalizations? of(BuildContext context) => - Localizations.of( - context, - StreamChatLocalizations, - ); - - String get launchUrlError; -} diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index bfde9a4c..8caf5ada 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header_paint.png) @@ -149,7 +150,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { children: [ title ?? Text( - 'Thread Reply', + context.translations.threadReplyLabel, style: chatThemeData.channelTheme.channelHeaderTheme.title, ), const SizedBox(height: 2), diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index 4c0ad19f..48752f07 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Widget to show the current list of typing users class TypingIndicator extends StatelessWidget { @@ -63,8 +64,7 @@ class TypingIndicator extends StatelessWidget { height: 4, ), Text( - // ignore: lines_longer_than_80_chars - ' ${data.elementAt(0).name}${data.length == 1 ? '' : ' and ${data.length - 1} more'} ${data.length == 1 ? 'is' : 'are'} typing', + context.translations.userTypingText(data), maxLines: 1, style: style, ), diff --git a/packages/stream_chat_flutter/lib/src/user_item.dart b/packages/stream_chat_flutter/lib/src/user_item.dart index e9d9f7c0..4b42eb6a 100644 --- a/packages/stream_chat_flutter/lib/src/user_item.dart +++ b/packages/stream_chat_flutter/lib/src/user_item.dart @@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/user_list_view.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// /// It shows the current [User] preview. @@ -86,12 +87,13 @@ class UserItem extends StatelessWidget { ); } - Widget _buildLastActive(context) { + Widget _buildLastActive(BuildContext context) { final chatTheme = StreamChatTheme.of(context); return Text( user.online == true - ? 'Online' - : 'Last online ${Jiffy(user.lastActive).fromNow()}', + ? context.translations.userOnlineText + : context.translations.userLastOnlineText + + Jiffy(user.lastActive).fromNow(), style: chatTheme.textTheme.footnote .copyWith(color: chatTheme.colorTheme.black.withOpacity(.5)), ); diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index f64f3c76..3ad1a337 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Callback called when tapping on a user typedef UserTapCallback = void Function(User, Widget?); @@ -155,6 +156,7 @@ class _UserListViewState extends State bool get _isListView => widget.crossAxisCount == 1; late final _defaultController = UserListController(); + UserListController get _userListController => widget.userListController ?? _defaultController; @@ -207,9 +209,9 @@ class _UserListViewState extends State mainAxisAlignment: MainAxisAlignment.center, children: [ Text.rich( - const TextSpan( + TextSpan( children: [ - WidgetSpan( + const WidgetSpan( child: Padding( padding: EdgeInsets.only( right: 2, @@ -217,14 +219,14 @@ class _UserListViewState extends State child: Icon(Icons.error_outline), ), ), - TextSpan(text: 'Error loading users'), + TextSpan(text: context.translations.loadingUsersError), ], ), style: Theme.of(context).textTheme.headline6, ), TextButton( onPressed: () => _userListController.loadData!(), - child: const Text('Retry'), + child: Text(context.translations.retryLabel), ), ], ), @@ -237,8 +239,8 @@ class _UserListViewState extends State constraints: BoxConstraints( minHeight: viewportConstraints.maxHeight, ), - child: const Center( - child: Text('There are no users currently'), + child: Center( + child: Text(context.translations.noUsersLabel), ), ), ), @@ -387,10 +389,10 @@ class _UserListViewState extends State .colorTheme .accentRed .withOpacity(.2), - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 16), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), child: Center( - child: Text('Error loading users'), + child: Text(context.translations.loadingUsersError), ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index d923b752..e6fda991 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -4,17 +4,15 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; /// Launch URL Future launchURL(BuildContext context, String? url) async { if (url != null && await canLaunch(url)) { await launch(url); } else { - // ignore: deprecated_member_use - Scaffold.of(context).showSnackBar( - const SnackBar( - content: Text('Cannot launch the url'), - ), + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.translations.launchUrlError)), ); } } diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 20b2396a..af8e66d7 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -15,6 +15,7 @@ export 'src/full_screen_media.dart'; export 'src/image_footer.dart'; export 'src/image_header.dart'; export 'src/info_tile.dart'; +export 'src/localization/stream_chat_localizations.dart'; export 'src/mention_tile.dart'; export 'src/message_action.dart'; export 'src/message_input.dart'; @@ -28,7 +29,6 @@ export 'src/reaction_icon.dart'; export 'src/reaction_picker.dart'; export 'src/sending_indicator.dart'; export 'src/stream_chat.dart'; -export 'src/stream_chat_localizations.dart'; export 'src/stream_chat_theme.dart'; export 'src/stream_neumorphic_button.dart'; export 'src/stream_svg_icon.dart'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index bef008fd..4dba5948 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart' - show StreamChatLocalizations; + show StreamChatLocalizations, User; part 'stream_chat_localizations_en.dart'; @@ -113,9 +113,6 @@ abstract class GlobalStreamChatLocalizations GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ]; - - @override - String get launchUrlError; } class _StreamChatLocalizationsDelegate diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index 7566be30..66abf0ce 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -8,4 +8,319 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get launchUrlError => 'Cannot launch the url'; + + @override + String get loadingUsersError => 'Error loading users'; + + @override + String get noUsersLabel => 'There are no users currently'; + + @override + String get retryLabel => 'Retry'; + + @override + String get userLastOnlineText => 'Last online'; + + @override + String get userOnlineText => 'Online'; + + @override + String userTypingText(Iterable users) { + if (users.isEmpty) return ''; + final first = users.first; + if (users.length == 1) { + return '${first.name} is typing'; + } + return '${first.name} and ${users.length - 1} more are typing'; + } + + @override + String get threadReplyLabel => 'Thread Reply'; + + @override + String get onlyVisibleToYouText => 'Only visible to you'; + + @override + String threadReplyCountText(int count) => '$count Thread Replies'; + + @override + String attachmentsUploadProgressText({ + required int remaining, + required int total, + }) => + 'Uploading $remaining/$total ...'; + + @override + String pinnedByUserText({ + required User pinnedBy, + required User currentUser, + }) { + final pinnedByCurrentUser = currentUser.id == pinnedBy.id; + if (pinnedByCurrentUser) return 'Pinned by You'; + return 'Pinned by ${pinnedBy.name}'; + } + + @override + String get emptyMessagesText => 'There are no messages currently'; + + @override + String get genericErrorText => 'Something went wrong'; + + @override + String get loadingMessagesError => 'Error loading messages'; + + @override + String resultCountText(int count) => '$count results'; + + @override + String get messageDeletedText => 'This message was deleted.'; + + @override + String get messageDeletedLabel => 'Message deleted'; + + @override + String get messageReactionsText => 'Message Reactions'; + + @override + String get emptyChatMessagesText => 'No chats here yet...'; + + @override + String threadSeparatorText(int replyCount) { + if (replyCount == 1) return '1 Reply'; + return '$replyCount Replies'; + } + + @override + String get connectedLabel => 'Connected'; + + @override + String get disconnectedLabel => 'Disconnected'; + + @override + String get reconnectingLabel => 'Reconnecting...'; + + @override + String get alsoSendAsDirectMessageLabel => 'Also send as direct message'; + + @override + String get addACommentOrSendLabel => 'Add a comment or send'; + + @override + String get searchGifLabel => 'Search GIFs'; + + @override + String get writeAMessageLabel => 'Write a message'; + + @override + String get instantCommandsLabel => 'Instant Commands'; + + @override + String get fileTooLargeAfterCompressionError => + 'The file is too large to upload. ' + 'The file size limit is 20MB. ' + 'We tried compressing it, but it was not enough.'; + + @override + String get fileTooLargeError => + 'The file is too large to upload. The file size limit is 20MB.'; + + @override + String emojiMatchingQueryText(String query) => 'Emoji matching "$query"'; + + @override + String get addAFileLabel => 'Add a file'; + + @override + String get photoFromCameraLabel => 'Photo from camera'; + + @override + String get uploadAFileLabel => 'Upload a file'; + + @override + String get uploadAPhotoLabel => 'Upload a photo'; + + @override + String get uploadAVideoLabel => 'Upload a video'; + + @override + String get videoFromCameraLabel => 'Video from camera'; + + @override + String get okLabel => 'OK'; + + @override + String get somethingWentWrongLabel => 'Something went wrong'; + + @override + String get addMoreFilesLabel => 'Add more files'; + + @override + String get enablePhotoAndVideoAccessMessage => + 'Please enable access to your photos' + '\nand videos so you can share them with friends.'; + + @override + String get allowGalleryAccessMessage => 'Allow access to your gallery'; + + @override + String get flagMessageLabel => 'Flag Message'; + + @override + String get flagMessageQuestion => + 'Do you want to send a copy of this message to a' + '\nmoderator for further investigation?'; + + @override + String get flagLabel => 'FLAG'; + + @override + String get cancelLabel => 'CANCEL'; + + @override + String get flagMessageSuccessfulLabel => 'Message flagged'; + + @override + String get flagMessageSuccessfulText => + 'The message has been reported to a moderator.'; + + @override + String get deleteLabel => 'DELETE'; + + @override + String get deleteMessageLabel => 'Delete Message'; + + @override + String get deleteMessageQuestion => + 'Are you sure you want to permanently delete this\nmessage?'; + + @override + String get operationCouldNotBeCompletedText => + 'The operation couldn\'t be completed.'; + + @override + String get replyLabel => 'Reply'; + + @override + String togglePinUnpinText({required bool pinned}) { + if (pinned) return 'Unpin from Conversation'; + return 'Pin to Conversation'; + } + + @override + String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) { + if (isDeleteFailed) return 'Retry Deleting Message'; + return 'Delete Message'; + } + + @override + String get copyMessageLabel => 'Copy Message'; + + @override + String get editMessageLabel => 'Edit Message'; + + @override + String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) { + if (isUpdateFailed) return 'Resend Edited Message'; + return 'Resend'; + } + + @override + String get photosLabel => 'Photos'; + + @override + String sentAtText({required DateTime date, required DateTime time}) => + 'Sent $date at $time'; + + @override + String get todayLabel => 'Today'; + + @override + String get yesterdayLabel => 'Yesterday'; + + @override + String get channelIsMutedText => ' Channel is muted'; + + @override + String get noTitleText => 'No title'; + + @override + String get letsStartChattingLabel => 'Let’s start chatting!'; + + @override + String get sendingFirstMessageLabel => + 'How about sending your first message to a friend?'; + + @override + String get startAChatLabel => 'Start a chat'; + + @override + String get loadingChannelsError => 'Error loading channels'; + + @override + String get deleteConversationLabel => 'Delete Conversation'; + + @override + String get deleteConversationQuestion => + 'Are you sure you want to delete this conversation?'; + + @override + String get streamChatLabel => 'Stream Chat'; + + @override + String get searchingForNetworkLabel => 'Searching for Network'; + + @override + String get offlineLabel => 'Offline...'; + + @override + String get tryAgainLabel => 'Try Again'; + + @override + String membersCountText(int count) { + if (count == 1) return '1 Member'; + return '$count Members'; + } + + @override + String watchersCountText(int count) { + if (count == 1) return '1 Online'; + return '$count Online'; + } + + @override + String get viewInfoLabel => 'View Info'; + + @override + String get leaveGroupLabel => 'Leave Group'; + + @override + String get leaveLabel => 'LEAVE'; + + @override + String get leaveConversationLabel => 'Leave conversation'; + + @override + String get leaveConversationQuestion => + 'Are you sure you want to leave this conversation?'; + + @override + String get showInChatLabel => 'Show in Chat'; + + @override + String get saveImageLabel => 'Save Image'; + + @override + String get saveVideoLabel => 'Save Video'; + + @override + String get uploadErrorLabel => 'UPLOAD ERROR'; + + @override + String get giphyLabel => 'Giphy'; + + @override + String get shuffleLabel => 'Shuffle'; + + @override + String get sendLabel => 'Send'; } From df62e8ba6cde939e751cdcb74aff553a02ab2f44 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 19 Jul 2021 10:53:44 +0200 Subject: [PATCH 005/148] update photo_manager package --- packages/stream_chat_flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 590ed055..75fd6fac 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -30,7 +30,7 @@ dependencies: lottie: ^1.0.1 meta: ^1.3.0 path_provider: ^2.0.1 - photo_manager: ^1.1.6 + photo_manager: ^1.2.6+1 photo_view: ^0.11.1 rxdart: ^0.27.0 scrollable_positioned_list: ^0.2.0-nullsafety.0 From f50ea0bac49e3d845e248b0bd42b080224a803e1 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 19 Jul 2021 12:05:57 +0200 Subject: [PATCH 006/148] add paginationLimit to message list core and ui package --- .../lib/src/message_input.dart | 1 + .../lib/src/message_list_view.dart | 9 ++++++- .../lib/src/message_list_core.dart | 25 ++++++++++++++++--- .../lib/src/stream_channel.dart | 15 ++++++++--- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 5a4f6822..a24cb95b 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -2309,6 +2309,7 @@ class __PickerWidgetState extends State<_PickerWidget> { return InkWell( onTap: () async { + print('on tap'); PhotoManager.openSetting(); }, child: Container( diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 822a2f5a..1a20212b 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -166,11 +166,15 @@ class MessageListView extends StatefulWidget { this.showFloatingDateDivider = true, this.threadSeparatorBuilder, this.messageListController, + this.paginationLimit = 20, }) : super(key: key); /// Function used to build a custom message widget final MessageBuilder? messageBuilder; + /// Limit used during pagination + final int paginationLimit; + /// Function used to build a custom system message widget final SystemMessageBuilder? systemMessageBuilder; @@ -328,6 +332,7 @@ class _MessageListViewState extends State { @override Widget build(BuildContext context) => MessageListCore( + paginationLimit: widget.paginationLimit, messageFilter: widget.messageFilter, loadingBuilder: widget.loadingBuilder ?? (context) => const Center( @@ -640,7 +645,9 @@ class _MessageListViewState extends State { ); Future _paginateData( - StreamChannelState? channel, QueryDirection direction) => + StreamChannelState? channel, + QueryDirection direction, + ) => _messageListController.paginateData!(direction: direction); int? _getTopElementIndex(Iterable values) { diff --git a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart index dc90cc91..4acf0ba7 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart @@ -71,6 +71,7 @@ class MessageListCore extends StatefulWidget { this.parentMessage, this.messageListController, this.messageFilter, + this.paginationLimit = 20, }) : super(key: key); /// A [MessageListController] allows pagination. @@ -86,6 +87,9 @@ class MessageListCore extends StatefulWidget { /// Function used to build an empty widget final WidgetBuilder emptyBuilder; + /// Limit used to paginate messages + final int paginationLimit; + /// Callback triggered when an error occurs while performing the given /// request. /// @@ -163,13 +167,20 @@ class MessageListCoreState extends State { /// Fetches more messages with updated pagination and updates the widget. /// /// Optionally pass the fetch direction, defaults to [QueryDirection.top] + /// Optionally pass a limit, defaults to 20 Future paginateData({ QueryDirection direction = QueryDirection.top, }) { if (!_isThreadConversation) { - return _streamChannel!.queryMessages(direction: direction); + return _streamChannel!.queryMessages( + direction: direction, + limit: widget.paginationLimit, + ); } else { - return _streamChannel!.getReplies(widget.parentMessage!.id); + return _streamChannel!.getReplies( + widget.parentMessage!.id, + limit: widget.paginationLimit, + ); } } @@ -179,7 +190,10 @@ class MessageListCoreState extends State { if (newStreamChannel != _streamChannel) { if (_streamChannel == null /*only first time*/ && _isThreadConversation) { - newStreamChannel.getReplies(widget.parentMessage!.id); + newStreamChannel.getReplies( + widget.parentMessage!.id, + limit: widget.paginationLimit, + ); } _streamChannel = newStreamChannel; } @@ -197,7 +211,10 @@ class MessageListCoreState extends State { if (widget.parentMessage?.id != widget.parentMessage?.id) { if (_isThreadConversation) { - _streamChannel!.getReplies(widget.parentMessage!.id); + _streamChannel!.getReplies( + widget.parentMessage!.id, + limit: widget.paginationLimit, + ); } } } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index 1dcf9c3c..fb7b79f2 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -147,9 +147,18 @@ class StreamChannelState extends State { } /// Calls [channel.query] updating [queryMessage] stream - Future queryMessages({QueryDirection? direction = QueryDirection.top}) { - if (direction == QueryDirection.top) return _queryTopMessages(); - return _queryBottomMessages(); + Future queryMessages({ + QueryDirection? direction = QueryDirection.top, + int limit = 20, + }) { + if (direction == QueryDirection.top) { + return _queryTopMessages( + limit: limit, + ); + } + return _queryBottomMessages( + limit: limit, + ); } /// Calls [channel.getReplies] updating [queryMessage] stream From 57594c403c09e76305efe676013936e183ea35bc Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 19 Jul 2021 12:10:20 +0200 Subject: [PATCH 007/148] add test --- .../test/message_list_core_test.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat_flutter_core/test/message_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_list_core_test.dart index b038411b..1a894890 100644 --- a/packages/stream_chat_flutter_core/test/message_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_list_core_test.dart @@ -151,7 +151,9 @@ void main() { (tester) async { const messageListCoreKey = Key('messageListCore'); final controller = MessageListController(); + final paginationLimit = 10; final messageListCore = MessageListCore( + paginationLimit: paginationLimit, key: messageListCoreKey, messageListBuilder: (_, __) => const Offstage(), loadingBuilder: (BuildContext context) => const Offstage(), @@ -165,10 +167,6 @@ void main() { final mockChannel = MockChannel(); when(() => mockChannel.state.isUpToDate).thenReturn(true); - // when(() => mockChannel.query( - // messagesPagination: any(named: 'messagesPagination'), - // preferOffline: any(named: 'preferOffline'), - // )).thenAnswer((_) => mockChannel.state); final messages = _generateMessages(); when(() => mockChannel.state.messages).thenReturn(messages); when(() => mockChannel.state.messagesStream) @@ -191,7 +189,10 @@ void main() { await coreState.paginateData(); verify(() => mockChannel.query( - messagesPagination: any(named: 'messagesPagination'), + messagesPagination: any( + named: 'messagesPagination', + that: wrapMatcher((it) => it.limit == paginationLimit), + ), preferOffline: any(named: 'preferOffline'), )).called(1); }, From 6c08c5a784f773a047680abf47e50402eeaf5f5d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 19 Jul 2021 12:11:34 +0200 Subject: [PATCH 008/148] add test --- .../stream_chat_flutter_core/test/message_list_core_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter_core/test/message_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_list_core_test.dart index 1a894890..e61d0abb 100644 --- a/packages/stream_chat_flutter_core/test/message_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_list_core_test.dart @@ -151,7 +151,7 @@ void main() { (tester) async { const messageListCoreKey = Key('messageListCore'); final controller = MessageListController(); - final paginationLimit = 10; + const paginationLimit = 10; final messageListCore = MessageListCore( paginationLimit: paginationLimit, key: messageListCoreKey, From 67a36d9c35a34b7ca07460cc2cc04ec37eff8117 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 19 Jul 2021 16:51:49 +0530 Subject: [PATCH 009/148] chore: add remaining translations Signed-off-by: xsahil03x --- .../stream_chat_flutter/example/lib/main.dart | 44 +++---------------- .../lib/src/attachment/file_attachment.dart | 2 +- .../lib/src/full_screen_media.dart | 2 +- .../lib/src/gallery_footer.dart | 4 +- .../lib/src/gallery_header.dart | 4 +- .../lib/src/localization/translations.dart | 25 +++++++++++ .../lib/src/message_input.dart | 4 +- .../lib/src/message_search_item.dart | 6 ++- .../lib/src/message_text.dart | 11 ++--- .../lib/src/system_message.dart | 4 +- .../lib/src/thread_header.dart | 2 +- .../lib/src/stream_chat_localizations_en.dart | 15 +++++++ 12 files changed, 65 insertions(+), 58 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index 91d44176..de2433ab 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -5,32 +5,6 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_localizations/stream_chat_localizations.dart'; -/// A custom set of localizations for the 'hi' locale. -class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { - const StreamChatLocalizationsHi() : super(localeName: 'hi'); - - static const LocalizationsDelegate delegate = - _HindiStreamChatLocalizationsDelegate(); - - @override - String get launchUrlError => 'URL लॉन्च नहीं कर सकता'; -} - -class _HindiStreamChatLocalizationsDelegate - extends LocalizationsDelegate { - const _HindiStreamChatLocalizationsDelegate(); - - @override - bool isSupported(Locale locale) => locale.languageCode == 'hi'; - - @override - Future load(Locale locale) => - SynchronousFuture(const StreamChatLocalizationsHi()); - - @override - bool shouldReload(_HindiStreamChatLocalizationsDelegate old) => false; -} - void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -96,18 +70,12 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) => MaterialApp( theme: ThemeData.light(), darkTheme: ThemeData.dark(), - themeMode: ThemeMode.system, - supportedLocales: [ - Locale('en', 'US'), - Locale('hi', 'IN'), - ], - localizationsDelegates: [ - GlobalStreamChatLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - StreamChatLocalizationsHi.delegate, - ], + localizationsDelegates: const [ + GlobalStreamChatLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], builder: (context, widget) => StreamChat( client: client, child: widget, diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index 7377ba20..f57e1ee6 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -76,7 +76,7 @@ class FileAttachment extends AttachmentWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - attachment.title ?? 'File', + attachment.title ?? context.translations.fileText, style: StreamChatTheme.of(context).textTheme.bodyBold, maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index 287612ad..13ec3b3b 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -200,7 +200,7 @@ class _FullScreenMediaState extends State ); }, ), - if (widget.message.type != 'ephemeral') + if (!widget.message.isEphemeral) GalleryFooter( currentPage: _currentPage, totalPages: widget.mediaAttachments.length, diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index ed8c39f9..03d575ae 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -136,7 +136,9 @@ class _GalleryFooterState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - '${widget.currentPage + 1} of ${widget.totalPages}', + '${widget.currentPage + 1} ' + '${context.translations.ofText} ' + '${widget.totalPages}', style: galleryFooterThemeData.titleTextStyle, ), ], diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart index 6c4ecfee..e0852800 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -67,7 +67,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { : const SizedBox(), backgroundColor: galleryHeaderThemeData.backgroundColor, actions: [ - if (message.type != 'ephemeral') + if (!message.isEphemeral) IconButton( icon: StreamSvgIcon.iconMenuPoint( color: galleryHeaderThemeData.iconMenuPointColor, @@ -78,7 +78,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { ), ], centerTitle: true, - title: message.type != 'ephemeral' + title: !message.isEphemeral ? InkWell( onTap: onTitleTap, child: SizedBox( diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 651f97f8..9b270bd5 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -190,6 +190,16 @@ abstract class Translations { String get shuffleLabel; String get sendLabel; + + String get withText; + + String get inText; + + String get youText; + + String get ofText; + + String get fileText; } class DefaultTranslations implements Translations { @@ -514,4 +524,19 @@ class DefaultTranslations implements Translations { @override String get sendLabel => 'Send'; + + @override + String get withText => 'with'; + + @override + String get inText => 'in'; + + @override + String get youText => 'You'; + + @override + String get ofText => 'of'; + + @override + String get fileText => 'File'; } diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 702cfe4e..456357ce 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -2254,10 +2254,10 @@ class _PickerWidget extends StatefulWidget { final StreamChatThemeData streamChatTheme; @override - __PickerWidgetState createState() => __PickerWidgetState(); + _PickerWidgetState createState() => _PickerWidgetState(); } -class __PickerWidgetState extends State<_PickerWidget> { +class _PickerWidgetState extends State<_PickerWidget> { Future? requestPermission; @override diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart index 0444d37f..de6c5e75 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -50,12 +50,14 @@ class MessageSearchItem extends StatelessWidget { title: Row( children: [ Text( - user.id == StreamChat.of(context).user?.id ? 'You' : user.name, + user.id == StreamChat.of(context).user?.id + ? context.translations.youText + : user.name, style: chatThemeData.channelPreviewTheme.title, ), if (channelName != null) ...[ Text( - ' in ', + ' ${context.translations.inText} ', style: chatThemeData.channelPreviewTheme.title?.copyWith( fontWeight: FontWeight.normal, ), diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index 6d006085..8fcf331c 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -43,15 +43,10 @@ class MessageText extends StatelessWidget { final mentionedUser = message.mentionedUsers.firstWhereOrNull( (u) => '@${u.name}' == link, ); - if (mentionedUser == null) { - return; - } - if (onMentionTap != null) { - onMentionTap!(mentionedUser); - } else { - print('tap on ${mentionedUser.name}'); - } + if (mentionedUser == null) return; + + onMentionTap?.call(mentionedUser); } else { if (onLinkTap != null) { onLinkTap!(link); diff --git a/packages/stream_chat_flutter/lib/src/system_message.dart b/packages/stream_chat_flutter/lib/src/system_message.dart index 11c80300..297810b3 100644 --- a/packages/stream_chat_flutter/lib/src/system_message.dart +++ b/packages/stream_chat_flutter/lib/src/system_message.dart @@ -13,8 +13,8 @@ class SystemMessage extends StatelessWidget { /// This message final Message message; - // ignore: lines_longer_than_80_chars - /// The function called when tapping on the message when the message is not failed + /// The function called when tapping on the message + /// when the message is not failed final void Function(Message)? onMessageTap; @override diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index 8caf5ada..a5eb62db 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -112,7 +112,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'with ', + '${context.translations.withText} ', style: chatThemeData.channelTheme.channelHeaderTheme.subtitle, ), Flexible( diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index 66abf0ce..91f41b64 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -323,4 +323,19 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get sendLabel => 'Send'; + + @override + String get withText => 'with'; + + @override + String get inText => 'in'; + + @override + String get youText => 'You'; + + @override + String get ofText => 'of'; + + @override + String get fileText => 'File'; } From 297092802dd8a34044be769fba1f30ba73488115 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 19 Jul 2021 16:56:08 +0530 Subject: [PATCH 010/148] chore: flutter format Signed-off-by: xsahil03x --- .../lib/src/channel_list_view.dart | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 5ef8d706..0977f662 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -548,19 +548,18 @@ class _ChannelListViewState extends State { color: backgroundColor, iconWidget: StreamSvgIcon.delete( color: chatThemeData.colorTheme.accentError, - ), - onTap: widget.onDeletePressed != null - ? () { - widget.onDeletePressed!(channel); - } - : () async { - final res = await showConfirmationDialog( - context, - title: - context.translations.deleteConversationLabel, - question: context - .translations.deleteConversationQuestion, - okText: context.translations.deleteLabel, + ), + onTap: widget.onDeletePressed != null + ? () { + widget.onDeletePressed?.call(channel); + } + : () async { + final res = await showConfirmationDialog( + context, + title: context.translations.deleteConversationLabel, + question: + context.translations.deleteConversationQuestion, + okText: context.translations.deleteLabel, cancelText: context.translations.cancelLabel, icon: StreamSvgIcon.delete( color: chatThemeData.colorTheme.accentError, From 5f7b3ffae85119619adb7b36717334521c3f15c7 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 19 Jul 2021 17:42:36 +0530 Subject: [PATCH 011/148] test: fix tests Signed-off-by: xsahil03x --- .../stream_chat_flutter/lib/src/attachment_actions_modal.dart | 2 +- packages/stream_chat_flutter/lib/src/extension.dart | 3 ++- .../stream_chat_flutter/lib/src/message_actions_modal.dart | 4 ++-- packages/stream_chat_flutter/lib/src/message_input.dart | 1 - packages/stream_chat_flutter/lib/src/message_list_view.dart | 1 - packages/stream_chat_flutter/lib/src/message_widget.dart | 1 - .../test/src/attachment_actions_modal_test.dart | 1 + .../test/src/message_action_modal_test.dart | 4 ++-- 8 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart index 0c224d20..9b64cce7 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -142,7 +142,7 @@ class AttachmentActionsModal extends StatelessWidget { if (StreamChat.of(context).user?.id == message.user?.id) _buildButton( context, - context.translations.deleteLabel, + context.translations.deleteLabel.capitalize(), StreamSvgIcon.delete( size: 24, color: theme.colorTheme.accentError, diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index b185ae3b..5d67ee50 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -10,7 +10,8 @@ final _emojiChars = Emoji.chars(); /// String extension extension StringExtension on String { /// Returns the capitalized string - String capitalize() => '${this[0].toUpperCase()}${substring(1)}'; + String capitalize() => + '${this[0].toUpperCase()}${substring(1).toLowerCase()}'; /// Returns whether the string contains only emoji's or not. /// diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 2f37d637..3e0afe75 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -274,7 +274,7 @@ class _MessageActionsModalState extends State { size: 24, ), question: context.translations.flagMessageQuestion, - okText: context.translations.okLabel, + okText: context.translations.flagLabel, cancelText: context.translations.cancelLabel, ); @@ -594,7 +594,7 @@ class _MessageActionsModalState extends State { ), Text( context.translations.editMessageLabel, - style: TextStyle(fontWeight: FontWeight.bold), + style: const TextStyle(fontWeight: FontWeight.bold), ), IconButton( visualDensity: VisualDensity.compact, diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 456357ce..e5e6ca48 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -24,7 +24,6 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:substring_highlight/substring_highlight.dart'; import 'package:video_compress/video_compress.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; export 'package:video_compress/video_compress.dart' show VideoQuality; diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 2c250eaa..ac124b46 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -17,7 +17,6 @@ import 'package:stream_chat_flutter/src/system_message.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:visibility_detector/visibility_detector.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Widget builder for message /// [defaultMessageWidget] is the default [MessageWidget] configuration diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 32e4794b..44038282 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -16,7 +16,6 @@ import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/src/url_attachment.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Widget builder for building attachments typedef AttachmentBuilder = Widget Function( diff --git a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart b/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart index 3f210742..59a49ac7 100644 --- a/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment_actions_modal_test.dart @@ -7,6 +7,7 @@ import 'package:stream_chat_flutter/src/attachment_actions_modal.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'mocks.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; class MockAttachmentDownloader extends Mock { ProgressCallback? progressCallback; diff --git a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart index 953434de..3aec707b 100644 --- a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart @@ -717,7 +717,7 @@ void main() { await tester.tap(find.text('Delete Message')); await tester.pumpAndSettle(); - expect(find.text('Delete message'), findsOneWidget); + expect(find.text('Delete Message'), findsOneWidget); await tester.tap(find.text('DELETE')); await tester.pumpAndSettle(); @@ -773,7 +773,7 @@ void main() { await tester.tap(find.text('Delete Message')); await tester.pumpAndSettle(); - expect(find.text('Delete message'), findsOneWidget); + expect(find.text('Delete Message'), findsOneWidget); await tester.tap(find.text('DELETE')); await tester.pumpAndSettle(); From d76f182f226f032b43486dd7653fbad3bcc56b59 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 20 Jul 2021 09:23:30 +0200 Subject: [PATCH 012/148] added changelog entry --- packages/stream_chat_flutter/CHANGELOG.md | 5 +++++ packages/stream_chat_flutter_core/CHANGELOG.md | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index e606d32d..9a00c312 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## Upcoming + +✅ Added +- Added `MessageListView.paginationLimit` + ## 2.0.0 🛑️ Breaking Changes from `1.5.4` diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 2f7abfe2..a823a0db 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,8 @@ +## Upcoming + +✅ Added +- Added `MessageListCore.paginationLimit` + ## 2.0.0 🛑️ Breaking Changes from `1.5.3` From 1939cc1bc8ef1f225e68cb69879f23b605452782 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 20 Jul 2021 13:46:53 +0530 Subject: [PATCH 013/148] feat: Added gradient avatar --- .../lib/src/gradient_avatar.dart | 199 ++++++++++++++++++ .../lib/src/stream_chat_theme.dart | 8 +- 2 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/gradient_avatar.dart diff --git a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart new file mode 100644 index 00000000..afc02fb6 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart @@ -0,0 +1,199 @@ +import 'dart:math'; +import 'dart:ui'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +class GradientAvatar extends StatefulWidget { + final String name; + final String userId; + + const GradientAvatar({ + Key? key, + required this.name, + required this.userId, + }) : super(key: key); + + @override + _GradientAvatarState createState() => _GradientAvatarState(); +} + +class _GradientAvatarState extends State { + @override + Widget build(BuildContext context) { + return Center( + child: RepaintBoundary( + child: Container( + width: 100.0, + height: 100.0, + child: Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + painter: DemoPainter(widget.userId), + child: SizedBox.expand(), + ), + FittedBox( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Opacity( + opacity: 0.8, + child: Text( + getShortenedName(widget.name), + style: TextStyle( + color: Colors.white, + fontSize: 112.0, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + String getShortenedName(String name) { + List parts = name.split(' ')..removeWhere((e) => e == ''); + + if (parts.length > 2) { + parts = parts.take(2).toList(); + } + + String result = ''; + + for (int i = 0; i < parts.length; i++) { + result = result + parts[i][0]; + } + + return result; + } +} + +class DemoPainter extends CustomPainter { + static const int rowCount = 5; + static const int columnCount = 5; + + String userId; + + DemoPainter(this.userId); + + @override + void paint(Canvas canvas, Size size) { + var rowUnit = size.width / columnCount; + var columnUnit = size.height / rowCount; + var rand = Random(userId.length); + + List squares = []; + Set points = {}; + List gradient = colorGradients[rand.nextInt(colorGradients.length)]; + + for (int i = 0; i < rowCount; i++) { + for (int j = 0; j < columnCount; j++) { + var off1 = Offset(rowUnit * j, columnUnit * i); + var off2 = Offset(rowUnit * (j + 1), columnUnit * i); + var off3 = Offset(rowUnit * (j + 1), columnUnit * (i + 1)); + var off4 = Offset(rowUnit * j, columnUnit * (i + 1)); + + points.addAll([off1, off2, off3, off4]); + + var p1 = points.toList().indexOf(off1); + var p2 = points.toList().indexOf(off2); + var p3 = points.toList().indexOf(off3); + var p4 = points.toList().indexOf(off4); + + squares.add( + Offset4(p1, p2, p3, p4, i, j, rowCount, columnCount, gradient)); + } + } + + var list = transformPoints(points, size); + squares.forEach((e) => e.draw(canvas, list)); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } + + List transformPoints(Set points, Size size) { + List transformedList = []; + var orgList = points.toList(); + var rand = Random(userId.length); + + for (int i = 0; i < points.length; i++) { + var orgDx = orgList[i].dx; + var orgDy = orgList[i].dy; + + if (orgDx == 0 || + orgDy == 0 || + orgDx == size.width || + orgDy == size.height) { + transformedList.add(Offset(orgDx, orgDy)); + continue; + } + + int sign1 = rand.nextInt(2) == 1 ? 1 : -1; + int sign2 = rand.nextInt(2) == 1 ? 1 : -1; + + double dx = 0.6 * sign1 * rand.nextInt(size.width ~/ columnCount); + double dy = 0.6 * sign2 * rand.nextInt(size.height ~/ rowCount); + + transformedList.add(Offset(orgDx + dx, orgDy + dy)); + } + + return transformedList; + } +} + +class Offset4 { + int p1; + int p2; + int p3; + int p4; + int row; + int column; + int rowSize; + int colSize; + List gradient; + + Offset4(this.p1, this.p2, this.p3, this.p4, this.row, this.column, + this.rowSize, this.colSize, this.gradient); + + void draw(Canvas canvas, List points) { + Paint paint = Paint() + ..color = Color.fromARGB(255, Random().nextInt(255), + Random().nextInt(255), Random().nextInt(255)) + ..shader = ui.Gradient.linear( + points[p1], + points[p3], + gradient, + ); + + final backgroundPath = Path() + ..moveTo(points[p1].dx, points[p1].dy) + ..lineTo(points[p2].dx, points[p2].dy) + ..lineTo(points[p3].dx, points[p3].dy) + ..lineTo(points[p4].dx, points[p4].dy) + ..lineTo(points[p1].dx, points[p1].dy) + ..close(); + + canvas.drawPath(backgroundPath, paint); + } +} + +const colorGradients = [ + [Color(0xffffafbd), Color(0xffffc3a0)], + [Color(0xff2193b0), Color(0xff6dd5ed)], + [Color(0xffcc2b5e), Color(0xff753a88)], + [Color(0xffee9ca7), Color(0xffffdde1)], + [Color(0xff42275a), Color(0xff734b6d)], + [Color(0xffde6262), Color(0xffffb88c)], + [Color(0xff56ab2f), Color(0xffa8e063)], + [Color(0xff614385), Color(0xff516395)], + [Color(0xffeacda3), Color(0xffd6ae7b)], + [Color(0xff02aab0), Color(0xff00cdac)], +]; diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index c62e92ec..74976e07 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/channel_header.dart'; import 'package:stream_chat_flutter/src/channel_preview.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/gradient_avatar.dart'; import 'package:stream_chat_flutter/src/message_input.dart'; import 'package:stream_chat_flutter/src/reaction_icon.dart'; import 'package:stream_chat_flutter/src/utils.dart'; @@ -270,10 +271,9 @@ class StreamChatThemeData { colorTheme: colorTheme, primaryIconTheme: iconTheme, defaultUserImage: (context, user) => Center( - child: CachedNetworkImage( - filterQuality: FilterQuality.high, - imageUrl: getRandomPicUrl(user), - fit: BoxFit.cover, + child: GradientAvatar( + name: user.name, + userId: user.id, ), ), channelPreviewTheme: channelPreviewTheme, From ec1c2fce8cefae5a7336b69edc00c021c2b34b72 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 20 Jul 2021 13:53:33 +0530 Subject: [PATCH 014/148] feat: Added docs and cleanup --- .../lib/src/gradient_avatar.dart | 180 +++++++++++------- 1 file changed, 109 insertions(+), 71 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart index afc02fb6..aef80712 100644 --- a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart @@ -4,68 +4,71 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; +/// Fallback user avatar with a polygon gradient overlayed with text class GradientAvatar extends StatefulWidget { - final String name; - final String userId; - + /// Constructor for [GradientAvatar] const GradientAvatar({ Key? key, required this.name, required this.userId, }) : super(key: key); + /// Name of user to shorten and display + final String name; + + /// ID of user to be used for key + final String userId; + @override _GradientAvatarState createState() => _GradientAvatarState(); } class _GradientAvatarState extends State { @override - Widget build(BuildContext context) { - return Center( - child: RepaintBoundary( - child: Container( - width: 100.0, - height: 100.0, - child: Stack( - alignment: Alignment.center, - children: [ - CustomPaint( - painter: DemoPainter(widget.userId), - child: SizedBox.expand(), - ), - FittedBox( - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Opacity( - opacity: 0.8, - child: Text( - getShortenedName(widget.name), - style: TextStyle( - color: Colors.white, - fontSize: 112.0, - fontWeight: FontWeight.bold, + Widget build(BuildContext context) => Center( + child: RepaintBoundary( + child: SizedBox( + width: 100, + height: 100, + child: Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + painter: DemoPainter(widget.userId), + child: const SizedBox.expand(), + ), + FittedBox( + child: Padding( + padding: const EdgeInsets.all(32), + child: Opacity( + opacity: 0.8, + child: Text( + getShortenedName(widget.name), + style: const TextStyle( + color: Colors.white, + fontSize: 112, + fontWeight: FontWeight.bold, + ), ), ), ), ), - ), - ], + ], + ), ), ), - ), - ); - } + ); String getShortenedName(String name) { - List parts = name.split(' ')..removeWhere((e) => e == ''); + var parts = name.split(' ')..removeWhere((e) => e == ''); if (parts.length > 2) { parts = parts.take(2).toList(); } - String result = ''; + var result = ''; - for (int i = 0; i < parts.length; i++) { + for (var i = 0; i < parts.length; i++) { result = result + parts[i][0]; } @@ -73,60 +76,65 @@ class _GradientAvatarState extends State { } } +/// Painter for bg polygon gradient class DemoPainter extends CustomPainter { + /// Constructor for [DemoPainter] + DemoPainter(this.userId); + + /// Init grid row count static const int rowCount = 5; + + /// Init grid column count static const int columnCount = 5; + /// User ID used for key String userId; - DemoPainter(this.userId); - @override void paint(Canvas canvas, Size size) { - var rowUnit = size.width / columnCount; - var columnUnit = size.height / rowCount; - var rand = Random(userId.length); + final rowUnit = size.width / columnCount; + final columnUnit = size.height / rowCount; + final rand = Random(userId.length); - List squares = []; - Set points = {}; - List gradient = colorGradients[rand.nextInt(colorGradients.length)]; + final squares = []; + final points = {}; + final gradient = colorGradients[rand.nextInt(colorGradients.length)]; - for (int i = 0; i < rowCount; i++) { - for (int j = 0; j < columnCount; j++) { - var off1 = Offset(rowUnit * j, columnUnit * i); - var off2 = Offset(rowUnit * (j + 1), columnUnit * i); - var off3 = Offset(rowUnit * (j + 1), columnUnit * (i + 1)); - var off4 = Offset(rowUnit * j, columnUnit * (i + 1)); + for (var i = 0; i < rowCount; i++) { + for (var j = 0; j < columnCount; j++) { + final off1 = Offset(rowUnit * j, columnUnit * i); + final off2 = Offset(rowUnit * (j + 1), columnUnit * i); + final off3 = Offset(rowUnit * (j + 1), columnUnit * (i + 1)); + final off4 = Offset(rowUnit * j, columnUnit * (i + 1)); points.addAll([off1, off2, off3, off4]); - var p1 = points.toList().indexOf(off1); - var p2 = points.toList().indexOf(off2); - var p3 = points.toList().indexOf(off3); - var p4 = points.toList().indexOf(off4); + final p1 = points.toList().indexOf(off1); + final p2 = points.toList().indexOf(off2); + final p3 = points.toList().indexOf(off3); + final p4 = points.toList().indexOf(off4); squares.add( Offset4(p1, p2, p3, p4, i, j, rowCount, columnCount, gradient)); } } - var list = transformPoints(points, size); + final list = transformPoints(points, size); squares.forEach((e) => e.draw(canvas, list)); } @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return false; - } + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; + /// Transforms initial grid into a polygon grid List transformPoints(Set points, Size size) { - List transformedList = []; - var orgList = points.toList(); - var rand = Random(userId.length); + final transformedList = []; + final orgList = points.toList(); + final rand = Random(userId.length); - for (int i = 0; i < points.length; i++) { - var orgDx = orgList[i].dx; - var orgDy = orgList[i].dy; + for (var i = 0; i < points.length; i++) { + final orgDx = orgList[i].dx; + final orgDy = orgList[i].dy; if (orgDx == 0 || orgDy == 0 || @@ -136,11 +144,11 @@ class DemoPainter extends CustomPainter { continue; } - int sign1 = rand.nextInt(2) == 1 ? 1 : -1; - int sign2 = rand.nextInt(2) == 1 ? 1 : -1; + final sign1 = rand.nextInt(2) == 1 ? 1 : -1; + final sign2 = rand.nextInt(2) == 1 ? 1 : -1; - double dx = 0.6 * sign1 * rand.nextInt(size.width ~/ columnCount); - double dy = 0.6 * sign2 * rand.nextInt(size.height ~/ rowCount); + final dx = 0.6 * sign1 * rand.nextInt(size.width ~/ columnCount); + final dy = 0.6 * sign2 * rand.nextInt(size.height ~/ rowCount); transformedList.add(Offset(orgDx + dx, orgDy + dy)); } @@ -149,22 +157,51 @@ class DemoPainter extends CustomPainter { } } +/// Class for storing and drawing four points of a polygon class Offset4 { + /// Constructor for [Offset4] + Offset4( + this.p1, + this.p2, + this.p3, + this.p4, + this.row, + this.column, + this.rowSize, + this.colSize, + this.gradient, + ); + + /// Point 1 int p1; + + /// Point 2 int p2; + + /// Point 3 int p3; + + /// Point 4 int p4; + + /// Position of polygon on grid int row; + + /// Position of polygon on grid int column; + + /// Max row size int rowSize; + + /// Max col size int colSize; + + /// Gradient to be applied to polygon List gradient; - Offset4(this.p1, this.p2, this.p3, this.p4, this.row, this.column, - this.rowSize, this.colSize, this.gradient); - + /// Draw the polygon on canvas void draw(Canvas canvas, List points) { - Paint paint = Paint() + final paint = Paint() ..color = Color.fromARGB(255, Random().nextInt(255), Random().nextInt(255), Random().nextInt(255)) ..shader = ui.Gradient.linear( @@ -185,6 +222,7 @@ class Offset4 { } } +/// Gradient list for polygons const colorGradients = [ [Color(0xffffafbd), Color(0xffffc3a0)], [Color(0xff2193b0), Color(0xff6dd5ed)], From 5670e18c0f938156df4aea9c2d4540553d830518 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 20 Jul 2021 13:54:19 +0530 Subject: [PATCH 015/148] fix: remove deps --- packages/stream_chat_flutter/lib/src/stream_chat_theme.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 74976e07..d8d50077 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -1,4 +1,3 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/channel_header.dart'; @@ -7,7 +6,6 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/gradient_avatar.dart'; import 'package:stream_chat_flutter/src/message_input.dart'; import 'package:stream_chat_flutter/src/reaction_icon.dart'; -import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; From 7df908f9c21d894e31a0d85a5e8e56033279c184 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 20 Jul 2021 14:14:06 +0530 Subject: [PATCH 016/148] fix: text style correction --- .../lib/src/gradient_avatar.dart | 44 +++++++++---------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart index aef80712..45cc1e96 100644 --- a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart @@ -27,34 +27,30 @@ class _GradientAvatarState extends State { @override Widget build(BuildContext context) => Center( child: RepaintBoundary( - child: SizedBox( - width: 100, - height: 100, - child: Stack( - alignment: Alignment.center, - children: [ - CustomPaint( - painter: DemoPainter(widget.userId), - child: const SizedBox.expand(), - ), - FittedBox( - child: Padding( - padding: const EdgeInsets.all(32), - child: Opacity( - opacity: 0.8, - child: Text( - getShortenedName(widget.name), - style: const TextStyle( - color: Colors.white, - fontSize: 112, - fontWeight: FontWeight.bold, - ), + child: Stack( + alignment: Alignment.center, + children: [ + CustomPaint( + painter: DemoPainter(widget.userId), + child: const SizedBox.expand(), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Opacity( + opacity: 0.7, + child: FittedBox( + child: Text( + getShortenedName(widget.name), + style: const TextStyle( + color: Colors.white, + fontSize: 120, + fontWeight: FontWeight.w500, ), ), ), ), - ], - ), + ), + ], ), ), ); From 069ec910a6013c3e5302b54c5f7f3689a105132f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 20 Jul 2021 10:54:20 +0200 Subject: [PATCH 017/148] feat: add MessageListView.reverse property --- .../example/android/app/build.gradle | 4 ++-- .../example/android/build.gradle | 4 ++-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../lib/src/message_list_view.dart | 21 +++++++++++++++---- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/stream_chat_flutter/example/android/app/build.gradle b/packages/stream_chat_flutter/example/android/app/build.gradle index 9bb36eac..fbd6268e 100644 --- a/packages/stream_chat_flutter/example/android/app/build.gradle +++ b/packages/stream_chat_flutter/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 30 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -41,7 +41,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.example" minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/packages/stream_chat_flutter/example/android/build.gradle b/packages/stream_chat_flutter/example/android/build.gradle index 9afae7f8..3e0873de 100644 --- a/packages/stream_chat_flutter/example/android/build.gradle +++ b/packages/stream_chat_flutter/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.5.20' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.2' + classpath 'com.android.tools.build:gradle:4.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/packages/stream_chat_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/stream_chat_flutter/example/android/gradle/wrapper/gradle-wrapper.properties index 493072b3..3df6b338 100644 --- a/packages/stream_chat_flutter/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/stream_chat_flutter/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 822a2f5a..505fc097 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1,5 +1,6 @@ // ignore_for_file: lines_longer_than_80_chars import 'dart:async'; +import 'dart:math'; import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; @@ -166,11 +167,19 @@ class MessageListView extends StatefulWidget { this.showFloatingDateDivider = true, this.threadSeparatorBuilder, this.messageListController, + this.reverse = false, }) : super(key: key); /// Function used to build a custom message widget final MessageBuilder? messageBuilder; + /// Whether the view scrolls in the reading direction. + /// + /// Defaults to true. + /// + /// See [ScrollView.reverse]. + final bool reverse; + /// Function used to build a custom system message widget final SystemMessageBuilder? systemMessageBuilder; @@ -445,7 +454,7 @@ class _MessageListViewState extends State { initialAlignment: initialAlignment ?? 0, physics: widget.scrollPhysics, itemScrollController: _scrollController, - reverse: true, + reverse: widget.reverse, addAutomaticKeepAlives: false, itemCount: itemCount, @@ -608,7 +617,8 @@ class _MessageListViewState extends State { } Positioned _buildFloatingDateDivider(int itemCount) => Positioned( - top: 20, + top: widget.reverse ? 20 : null, + bottom: widget.reverse ? null : 20, child: BetterStreamBuilder>( initialData: _itemPositionListener.itemPositions.value, stream: _itemPositionStream, @@ -700,8 +710,11 @@ class _MessageListViewState extends State { ); } }, - child: StreamSvgIcon.down( - color: _streamTheme.colorTheme.textHighEmphasis, + child: Transform.rotate( + angle: widget.reverse ? 0 : pi, + child: StreamSvgIcon.down( + color: _streamTheme.colorTheme.textHighEmphasis, + ), ), ), if (showUnreadCount) From a8fc76c85f3c0ec98e76c923ae26eb4eaaf0b5e9 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 20 Jul 2021 10:55:06 +0200 Subject: [PATCH 018/148] update changelog --- packages/stream_chat_flutter/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index e606d32d..c4c8e10c 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +✅ Added + +- Added `MessageListView.reverse` property + ## 2.0.0 🛑️ Breaking Changes from `1.5.4` From dff0ad9af27cd198eb011347df45605253922e83 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 20 Jul 2021 10:55:25 +0200 Subject: [PATCH 019/148] fix default --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 505fc097..a344ceb4 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -167,7 +167,7 @@ class MessageListView extends StatefulWidget { this.showFloatingDateDivider = true, this.threadSeparatorBuilder, this.messageListController, - this.reverse = false, + this.reverse = true, }) : super(key: key); /// Function used to build a custom message widget From ab35409b15ac7c84462e8dc59e0c2d7776179d8d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 20 Jul 2021 11:17:12 +0200 Subject: [PATCH 020/148] use up svg icon --- .../lib/src/message_list_view.dart | 13 +++++++------ .../lib/src/stream_svg_icon.dart | 12 ++++++++++++ packages/stream_chat_flutter/lib/svgs/Icon_up.svg | 3 +++ 3 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/svgs/Icon_up.svg diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index a344ceb4..74933321 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -710,12 +710,13 @@ class _MessageListViewState extends State { ); } }, - child: Transform.rotate( - angle: widget.reverse ? 0 : pi, - child: StreamSvgIcon.down( - color: _streamTheme.colorTheme.textHighEmphasis, - ), - ), + child: widget.reverse + ? StreamSvgIcon.down( + color: _streamTheme.colorTheme.textHighEmphasis, + ) + : StreamSvgIcon.up( + color: _streamTheme.colorTheme.textHighEmphasis, + ), ), if (showUnreadCount) Positioned( diff --git a/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart b/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart index fb5017e1..73c33131 100644 --- a/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart +++ b/packages/stream_chat_flutter/lib/src/stream_svg_icon.dart @@ -37,6 +37,18 @@ class StreamSvgIcon extends StatelessWidget { height: size, ); + /// [StreamSvgIcon] type + factory StreamSvgIcon.up({ + double? size, + Color? color, + }) => + StreamSvgIcon( + assetName: 'Icon_up.svg', + color: color, + width: size, + height: size, + ); + /// [StreamSvgIcon] type factory StreamSvgIcon.attach({ double? size, diff --git a/packages/stream_chat_flutter/lib/svgs/Icon_up.svg b/packages/stream_chat_flutter/lib/svgs/Icon_up.svg new file mode 100644 index 00000000..d3d53810 --- /dev/null +++ b/packages/stream_chat_flutter/lib/svgs/Icon_up.svg @@ -0,0 +1,3 @@ + + + From cfa9d2539911d49ff0314a4e8150fd777b044a8c Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 20 Jul 2021 15:39:45 +0530 Subject: [PATCH 021/148] fix: Replaced stack text with canvas text --- .../lib/src/gradient_avatar.dart | 67 ++++++++++++------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart index 45cc1e96..a7e18983 100644 --- a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart @@ -27,30 +27,12 @@ class _GradientAvatarState extends State { @override Widget build(BuildContext context) => Center( child: RepaintBoundary( - child: Stack( - alignment: Alignment.center, - children: [ - CustomPaint( - painter: DemoPainter(widget.userId), - child: const SizedBox.expand(), - ), - Padding( - padding: const EdgeInsets.all(8), - child: Opacity( - opacity: 0.7, - child: FittedBox( - child: Text( - getShortenedName(widget.name), - style: const TextStyle( - color: Colors.white, - fontSize: 120, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ), - ], + child: CustomPaint( + painter: DemoPainter( + widget.userId, + getShortenedName(widget.name), + ), + child: const SizedBox.expand(), ), ), ); @@ -75,7 +57,10 @@ class _GradientAvatarState extends State { /// Painter for bg polygon gradient class DemoPainter extends CustomPainter { /// Constructor for [DemoPainter] - DemoPainter(this.userId); + DemoPainter( + this.userId, + this.username, + ); /// Init grid row count static const int rowCount = 5; @@ -86,6 +71,9 @@ class DemoPainter extends CustomPainter { /// User ID used for key String userId; + /// User name to display + String username; + @override void paint(Canvas canvas, Size size) { final rowUnit = size.width / columnCount; @@ -117,6 +105,35 @@ class DemoPainter extends CustomPainter { final list = transformPoints(points, size); squares.forEach((e) => e.draw(canvas, list)); + + final smallerSide = size.width > size.height ? size.width : size.height; + + final textSize = smallerSide / 3; + + final dxShift = (username.length == 2 ? 1.45 : 0.75) * textSize / 2; + final dyShift = (username.length == 2 ? 1.0 : 1.8) * textSize / 2; + + final fontSize = username.length == 2 ? textSize : textSize * 1.5; + + TextPainter( + text: TextSpan( + text: username, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.w500, + color: Colors.white.withOpacity(0.7), + ), + ), + textAlign: TextAlign.center, + textDirection: TextDirection.ltr) + ..layout(maxWidth: size.width) + ..paint( + canvas, + Offset( + (size.width / 2) - dxShift, + (size.height / 2) - dyShift, + ), + ); } @override From 293cb927c12cad0f5a9d2d58abf92ae5bc87546c Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 20 Jul 2021 12:14:09 +0200 Subject: [PATCH 022/148] fix icon --- packages/stream_chat_flutter/lib/svgs/Icon_up.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/svgs/Icon_up.svg b/packages/stream_chat_flutter/lib/svgs/Icon_up.svg index d3d53810..60e6888f 100644 --- a/packages/stream_chat_flutter/lib/svgs/Icon_up.svg +++ b/packages/stream_chat_flutter/lib/svgs/Icon_up.svg @@ -1,3 +1,3 @@ - - + + From 88ece480a27dd8a0bb633ddf8bee7c04a1fd4e92 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 20 Jul 2021 15:44:47 +0530 Subject: [PATCH 023/148] fix: cleanup --- .../stream_chat_flutter/lib/src/gradient_avatar.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart index a7e18983..0cb8c41c 100644 --- a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart @@ -93,10 +93,12 @@ class DemoPainter extends CustomPainter { points.addAll([off1, off2, off3, off4]); - final p1 = points.toList().indexOf(off1); - final p2 = points.toList().indexOf(off2); - final p3 = points.toList().indexOf(off3); - final p4 = points.toList().indexOf(off4); + final pointsList = points.toList(); + + final p1 = pointsList.indexOf(off1); + final p2 = pointsList.indexOf(off2); + final p3 = pointsList.indexOf(off3); + final p4 = pointsList.indexOf(off4); squares.add( Offset4(p1, p2, p3, p4, i, j, rowCount, columnCount, gradient)); From 6794bde9e496b567ef2fff3273b8f0c78881fd81 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 20 Jul 2021 12:39:08 +0200 Subject: [PATCH 024/148] fix analyzer --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 74933321..942d02f5 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1,6 +1,5 @@ // ignore_for_file: lines_longer_than_80_chars import 'dart:async'; -import 'dart:math'; import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; From 2b36f009c2eb43e18b473270d7345b10cf8d02c4 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 20 Jul 2021 18:09:12 +0530 Subject: [PATCH 025/148] chore: minor fixes, add support for "hi" locale Signed-off-by: xsahil03x --- .../stream_chat_flutter/example/lib/main.dart | 4 + .../lib/src/channel_info.dart | 4 +- .../lib/src/channel_preview.dart | 2 +- .../lib/src/full_screen_media.dart | 18 - .../lib/src/localization/translations.dart | 25 +- .../lib/src/user_item.dart | 4 +- .../lib/stream_chat_flutter.dart | 2 + .../lib/src/stream_chat_localizations.dart | 7 +- .../lib/src/stream_chat_localizations_en.dart | 19 +- .../lib/src/stream_chat_localizations_hi.dart | 353 ++++++++++++++++++ 10 files changed, 403 insertions(+), 35 deletions(-) create mode 100644 packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index de2433ab..d643dfea 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -70,6 +70,10 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) => MaterialApp( theme: ThemeData.light(), darkTheme: ThemeData.dark(), + supportedLocales: const [ + Locale('en'), + Locale('hi'), + ], localizationsDelegates: const [ GlobalStreamChatLocalizations.delegate, GlobalCupertinoLocalizations.delegate, diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index e9c2b7c9..4e860745 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -84,8 +84,8 @@ class ChannelInfo extends StatelessWidget { ); } else { alternativeWidget = Text( - context.translations.userLastOnlineText + - Jiffy(otherMember.user?.lastActive).fromNow(), + '${context.translations.userLastOnlineText} ' + '${Jiffy(otherMember.user?.lastActive).fromNow()}', style: textStyle, ); } diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 432f3354..4c0d2b42 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -198,7 +198,7 @@ class ChannelPreview extends StatelessWidget { size: 16, ), Text( - context.translations.channelIsMutedText, + ' ${context.translations.channelIsMutedText}', style: chatThemeData.channelPreviewTheme.subtitle, ), ], diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index 13ec3b3b..5e498a32 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; import 'package:photo_view/photo_view.dart'; import 'package:stream_chat_flutter/src/gallery_footer.dart'; import 'package:stream_chat_flutter/src/gallery_header.dart'; @@ -183,7 +182,6 @@ class _FullScreenMediaState extends State children: [ GalleryHeader( userName: widget.userName, - // TODO: Fix this sentAt: context.translations.sentAtText( date: widget.message.createdAt, time: widget.message.createdAt, @@ -225,22 +223,6 @@ class _FullScreenMediaState extends State ), ); - String getDay(DateTime dateTime) { - final now = DateTime.now(); - - if (DateTime(dateTime.year, dateTime.month, dateTime.day) == - DateTime(now.year, now.month, now.day)) { - return 'today'; - } else if (DateTime(now.year, now.month, now.day) - .difference(dateTime) - .inHours < - 24) { - return 'yesterday'; - } else { - return 'on ${Jiffy(dateTime).MMMd}'; - } - } - @override void dispose() async { for (final package in videoPackages.values) { diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 9b270bd5..4344dc8a 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -1,3 +1,4 @@ +import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart' show User; @@ -146,11 +147,6 @@ abstract class Translations { String get loadingChannelsError; - // title: 'Delete Conversation', -// okText: 'DELETE', -// question: -// 'Are you sure you want to delete this conversation?', - String get deleteConversationLabel; String get deleteConversationQuestion; @@ -274,7 +270,7 @@ class DefaultTranslations implements Translations { String resultCountText(int count) => '$count results'; @override - String get messageDeletedText => 'This message was deleted.'; + String get messageDeletedText => 'This message is deleted.'; @override String get messageDeletedLabel => 'Message deleted'; @@ -428,9 +424,22 @@ class DefaultTranslations implements Translations { @override String get photosLabel => 'Photos'; + String _getDay(DateTime dateTime) { + final now = Jiffy(DateTime.now()); + final date = Jiffy(dateTime); + + if (date.isSame(now, Units.DAY)) { + return 'today'; + } else if (now.diff(date, Units.HOUR) < 24) { + return 'yesterday'; + } else { + return 'on ${date.MMMd}'; + } + } + @override String sentAtText({required DateTime date, required DateTime time}) => - 'Sent $date at $time'; + 'Sent ${_getDay(date)} at ${Jiffy(time.toLocal()).format('HH:mm')}'; @override String get todayLabel => 'Today'; @@ -439,7 +448,7 @@ class DefaultTranslations implements Translations { String get yesterdayLabel => 'Yesterday'; @override - String get channelIsMutedText => ' Channel is muted'; + String get channelIsMutedText => 'Channel is muted'; @override String get noTitleText => 'No title'; diff --git a/packages/stream_chat_flutter/lib/src/user_item.dart b/packages/stream_chat_flutter/lib/src/user_item.dart index 99b35df1..a55693aa 100644 --- a/packages/stream_chat_flutter/lib/src/user_item.dart +++ b/packages/stream_chat_flutter/lib/src/user_item.dart @@ -92,8 +92,8 @@ class UserItem extends StatelessWidget { return Text( user.online == true ? context.translations.userOnlineText - : context.translations.userLastOnlineText + - Jiffy(user.lastActive).fromNow(), + : '${context.translations.userLastOnlineText} ' + '${Jiffy(user.lastActive).fromNow()}', style: chatTheme.textTheme.footnote.copyWith( color: chatTheme.colorTheme.textHighEmphasis.withOpacity(.5)), ); diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 4b4fd9eb..eb690a9b 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -1,3 +1,4 @@ +export 'package:jiffy/jiffy.dart'; export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; export 'src/attachment/attachment.dart'; @@ -16,6 +17,7 @@ export 'src/gallery_footer.dart'; export 'src/gallery_header.dart'; export 'src/info_tile.dart'; export 'src/localization/stream_chat_localizations.dart'; +export 'src/localization/translations.dart' show DefaultTranslations; export 'src/mention_tile.dart'; export 'src/message_action.dart'; export 'src/message_input.dart'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index 4dba5948..82662134 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -1,11 +1,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart' show StreamChatLocalizations, User; part 'stream_chat_localizations_en.dart'; +part 'stream_chat_localizations_hi.dart'; + /// The set of supported languages, as language code strings. /// /// The [GlobalStreamChatLocalizations.delegate] can generate localizations for @@ -14,7 +17,7 @@ part 'stream_chat_localizations_en.dart'; /// See also: /// /// * [getStreamChatTranslation], whose documentation describes these values. -const kStreamChatSupportedLanguages = {'en'}; +const kStreamChatSupportedLanguages = {'en', 'hi'}; /// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`. /// @@ -38,6 +41,8 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { switch (locale.languageCode) { case 'en': return const StreamChatLocalizationsEn(); + case 'hi': + return const StreamChatLocalizationsHi(); } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index 91f41b64..88b88840 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -73,7 +73,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String resultCountText(int count) => '$count results'; @override - String get messageDeletedText => 'This message was deleted.'; + String get messageDeletedText => 'This message is deleted.'; @override String get messageDeletedLabel => 'Message deleted'; @@ -227,9 +227,22 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get photosLabel => 'Photos'; + String _getDay(DateTime dateTime) { + final now = Jiffy(DateTime.now()); + final date = Jiffy(dateTime); + + if (date.isSame(now, Units.DAY)) { + return 'today'; + } else if (now.diff(date, Units.HOUR) < 24) { + return 'yesterday'; + } else { + return 'on ${date.MMMd}'; + } + } + @override String sentAtText({required DateTime date, required DateTime time}) => - 'Sent $date at $time'; + 'Sent ${_getDay(date)} at ${Jiffy(time.toLocal()).format('HH:mm')}'; @override String get todayLabel => 'Today'; @@ -238,7 +251,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String get yesterdayLabel => 'Yesterday'; @override - String get channelIsMutedText => ' Channel is muted'; + String get channelIsMutedText => 'Channel is muted'; @override String get noTitleText => 'No title'; diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart new file mode 100644 index 00000000..5666b77d --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -0,0 +1,353 @@ +part of 'stream_chat_localizations.dart'; + +/// The translations for English (`hi`). +class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { + /// Create an instance of the translation bundle for Hindi. + const StreamChatLocalizationsHi({String localeName = 'hi'}) + : super(localeName: localeName); + + @override + String get launchUrlError => 'यूआरएल लॉन्च नहीं कर सकते'; + + @override + String get loadingUsersError => 'यूजर लोड करने में समस्या'; + + @override + String get noUsersLabel => 'वर्तमान में कोई यूजर नहीं हैं'; + + @override + String get retryLabel => 'पुन: प्रयास करे'; + + @override + String get userLastOnlineText => 'अंतिम ऑनलाइन'; + + @override + String get userOnlineText => 'ऑनलाइन'; + + @override + String userTypingText(Iterable users) { + if (users.isEmpty) return ''; + final first = users.first; + if (users.length == 1) { + return '${first.name} टाइप कर रहा है'; + } + return '${first.name} और ${users.length - 1} और टाइप कर रहे हैं'; + } + + @override + String get threadReplyLabel => 'थ्रेड जवाब'; + + @override + String get onlyVisibleToYouText => 'केवल आपको दिखाई दे रहा है'; + + @override + String threadReplyCountText(int count) => '$count थ्रेड जवाब'; + + @override + String attachmentsUploadProgressText({ + required int remaining, + required int total, + }) => + 'अपलोडिंग $remaining/$total ...'; + + @override + String pinnedByUserText({ + required User pinnedBy, + required User currentUser, + }) { + final pinnedByCurrentUser = currentUser.id == pinnedBy.id; + if (pinnedByCurrentUser) return 'आपके द्वारा पिन किया गया'; + return '${pinnedBy.name} द्वारा पिन किया गया'; + } + + @override + String get emptyMessagesText => 'वर्तमान में कोई संदेश नहीं है'; + + @override + String get genericErrorText => 'कुछ समस्या हो गई'; + + @override + String get loadingMessagesError => 'संदेश लोड करने में समस्या'; + + @override + String resultCountText(int count) => '$count परिणाम'; + + @override + String get messageDeletedText => 'यह संदेश हटा दिया गया है।'; + + @override + String get messageDeletedLabel => 'संदेश हटाये'; + + @override + String get messageReactionsText => 'संदेश प्रतिक्रियाएं'; + + @override + String get emptyChatMessagesText => 'यहां अभी तक कोई चैट नहीं...'; + + @override + String threadSeparatorText(int replyCount) { + if (replyCount == 1) return '1 जवाब'; + return '$replyCount जवाब'; + } + + @override + String get connectedLabel => 'कनेक्टेड'; + + @override + String get disconnectedLabel => 'डिस्कनेक्टेड'; + + @override + String get reconnectingLabel => 'पुनः कनेक्टिंग...'; + + @override + String get alsoSendAsDirectMessageLabel => 'सीधे संदेश के रूप में भी भेजें'; + + @override + String get addACommentOrSendLabel => 'एक टिप्पणी जोड़ें या भेजें'; + + @override + String get searchGifLabel => 'जीआईएफ खोजें'; + + @override + String get writeAMessageLabel => 'एक सन्देश लिखिए'; + + @override + String get instantCommandsLabel => 'तत्काल आदेश'; + + @override + String get fileTooLargeAfterCompressionError => + 'फ़ाइल अपलोड करने के लिए बहुत बड़ी है। ' + 'फ़ाइल आकार सीमा 20MB है। ' + 'हमने इसे कंप्रेस करने की कोशिश की, लेकिन यह काफी नहीं था।'; + + @override + String get fileTooLargeError => + 'फ़ाइल अपलोड करने के लिए बहुत बड़ी है। फ़ाइल आकार सीमा 20MB है।'; + + @override + String emojiMatchingQueryText(String query) => '"$query" से मिलते हुए इमोजी'; + + @override + String get addAFileLabel => 'एक फ़ाइल जोड़ें'; + + @override + String get photoFromCameraLabel => 'कैमरे से फोटो'; + + @override + String get uploadAFileLabel => 'एक फाइल अपलोड करें'; + + @override + String get uploadAPhotoLabel => 'एक फोटो अपलोड करो'; + + @override + String get uploadAVideoLabel => 'एक वीडियो अपलोड करें'; + + @override + String get videoFromCameraLabel => 'कैमरे से वीडियो'; + + @override + String get okLabel => 'ठीक'; + + @override + String get somethingWentWrongLabel => 'लोड करने में समस्या'; + + @override + String get addMoreFilesLabel => 'और फ़ाइलें जोड़ें'; + + @override + String get enablePhotoAndVideoAccessMessage => + 'कृपया अपने फ़ोटो और वीडियो तक पहुंच सक्षम करें' + '\nताकि आप उन्हें मित्रों के साथ साझा कर सकें।'; + + @override + String get allowGalleryAccessMessage => 'अपनी गैलरी तक पहुंच की अनुमति दें'; + + @override + String get flagMessageLabel => 'फ्लैग संदेश'; + + @override + String get flagMessageQuestion => 'क्या आप आगे की जांच के लिए इस संदेश की' + '\nएक प्रति मॉडरेटर को भेजना चाहते हैं?'; + + @override + String get flagLabel => 'फ्लैग'; + + @override + String get cancelLabel => 'रद्द करें'; + + @override + String get flagMessageSuccessfulLabel => 'संदेश फ्लैग हो गया'; + + @override + String get flagMessageSuccessfulText => + 'संदेश की रिपोर्ट एक मॉडरेटर को कर दी गई है।'; + + @override + String get deleteLabel => 'हटाएँ'; + + @override + String get deleteMessageLabel => 'संदेश हटाएं'; + + @override + String get deleteMessageQuestion => + 'क्या आप वाकई इस संदेश को स्थायी रूप से\nहटाना चाहते हैं?'; + + @override + String get operationCouldNotBeCompletedText => + 'कार्रवाई पूरी नहीं की जा सकी.'; + + @override + String get replyLabel => 'रिप्लाई'; + + @override + String togglePinUnpinText({required bool pinned}) { + if (pinned) return 'बातचीत से अनपिन करें'; + return 'बातचीत में पिन करें'; + } + + @override + String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) { + if (isDeleteFailed) return 'संदेश हटाने का पुनः प्रयास करें'; + return 'संदेश को हटाएं'; + } + + @override + String get copyMessageLabel => 'संदेश कॉपी करें'; + + @override + String get editMessageLabel => 'संदेश एडिट करें'; + + @override + String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) { + if (isUpdateFailed) return 'एडिट संदेश फिर से भेजें'; + return 'पुन: भेजें'; + } + + @override + String get photosLabel => 'तस्वीरें'; + + String _getDay(DateTime dateTime) { + final now = Jiffy(DateTime.now()); + final date = Jiffy(dateTime); + + if (date.isSame(now, Units.DAY)) { + return 'आज'; + } else if (now.diff(date, Units.HOUR) < 24) { + return 'कल'; + } else { + return '${date.MMMd} को'; + } + } + + @override + String sentAtText({required DateTime date, required DateTime time}) => + '${_getDay(date)} ${Jiffy(time.toLocal()).format('HH:mm')} बजे भेजा गया'; + + @override + String get todayLabel => 'आज'; + + @override + String get yesterdayLabel => 'बिता हुआ कल'; + + @override + String get channelIsMutedText => 'चैनल मौन है'; + + @override + String get noTitleText => 'कोई शीर्षक नहीं'; + + @override + String get letsStartChattingLabel => 'चलो चैट करना शुरू करें!'; + + @override + String get sendingFirstMessageLabel => + 'किसी मित्र को अपना पहला संदेश भेजने के बारे में क्या विचार है?'; + + @override + String get startAChatLabel => 'चैट शुरू करें'; + + @override + String get loadingChannelsError => 'चैनल लोड करने में समस्या'; + + @override + String get deleteConversationLabel => 'वार्तालाप हटाए'; + + @override + String get deleteConversationQuestion => + 'क्या आप वाकई इस वार्तालाप को हटाना चाहते हैं?'; + + @override + String get streamChatLabel => 'स्ट्रीम चैट'; + + @override + String get searchingForNetworkLabel => 'नेटवर्क खोज रहे हैं'; + + @override + String get offlineLabel => 'ऑफलाइन...'; + + @override + String get tryAgainLabel => 'पुनः प्रयास करें'; + + @override + String membersCountText(int count) { + if (count == 1) return '1 सदस्य'; + return '$count सदस्य'; + } + + @override + String watchersCountText(int count) { + if (count == 1) return '1 ऑनलाइन'; + return '$count ऑनलाइन'; + } + + @override + String get viewInfoLabel => 'जानकारी देखें'; + + @override + String get leaveGroupLabel => 'समूह छोड़े'; + + @override + String get leaveLabel => 'छोड़े'; + + @override + String get leaveConversationLabel => 'वार्तालाप छोड़े'; + + @override + String get leaveConversationQuestion => + 'क्या आप वाकई इस बातचीत को छोड़ना चाहते हैं?'; + + @override + String get showInChatLabel => 'चैट में दिखाएं'; + + @override + String get saveImageLabel => 'चित्र को सेव करें'; + + @override + String get saveVideoLabel => 'वीडियो को सेव करे'; + + @override + String get uploadErrorLabel => 'अपलोड समस्या'; + + @override + String get giphyLabel => 'जिफ़ी'; + + @override + String get shuffleLabel => 'बदलें'; + + @override + String get sendLabel => 'भेजें'; + + @override + String get withText => 'विद'; + + @override + String get inText => 'इन'; + + @override + String get youText => 'आप'; + + @override + String get ofText => 'ऑफ़'; + + @override + String get fileText => 'फ़ाइल'; +} From 0691f8066a3b3fa3c2de50da2bf1d31c90375842 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 20 Jul 2021 16:03:11 +0200 Subject: [PATCH 026/148] add quick links to docs --- packages/stream_chat/README.md | 1 + packages/stream_chat_flutter/README.md | 3 ++- packages/stream_chat_flutter_core/README.md | 1 + packages/stream_chat_persistence/README.md | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat/README.md b/packages/stream_chat/README.md index 1ee52ec5..74afad4c 100644 --- a/packages/stream_chat/README.md +++ b/packages/stream_chat/README.md @@ -11,6 +11,7 @@ - [Register](https://getstream.io/chat/trial/) to get an API key for Stream Chat - [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/) - [Chat UI Kit](https://getstream.io/chat/ui-kit/) +- [Chat Client Docs](https://getstream.io/chat/docs/flutter-dart/?language=dart) ### Changelog diff --git a/packages/stream_chat_flutter/README.md b/packages/stream_chat_flutter/README.md index 41ea079d..811135a6 100644 --- a/packages/stream_chat_flutter/README.md +++ b/packages/stream_chat_flutter/README.md @@ -14,7 +14,8 @@ - [Register](https://getstream.io/chat/trial/) to get an API key for Stream Chat - [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/) - [Chat UI Kit](https://getstream.io/chat/ui-kit/) - +- [UI Docs](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/introduction/) +- [Chat Client Docs](https://getstream.io/chat/docs/flutter-dart/?language=dart) ### Changelog diff --git a/packages/stream_chat_flutter_core/README.md b/packages/stream_chat_flutter_core/README.md index 4aaf27d9..462498f6 100644 --- a/packages/stream_chat_flutter_core/README.md +++ b/packages/stream_chat_flutter_core/README.md @@ -12,6 +12,7 @@ - [Register](https://getstream.io/chat/trial/) to get an API key for Stream Chat - [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/) - [Chat UI Kit](https://getstream.io/chat/ui-kit/) +- [Core Docs](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter_core/introduction/) ### Changelog diff --git a/packages/stream_chat_persistence/README.md b/packages/stream_chat_persistence/README.md index 7f94eda1..0851335b 100644 --- a/packages/stream_chat_persistence/README.md +++ b/packages/stream_chat_persistence/README.md @@ -13,6 +13,7 @@ - [Register](https://getstream.io/chat/trial/) to get an API key for Stream Chat - [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/) - [Chat UI Kit](https://getstream.io/chat/ui-kit/) +- [Flutter Offline Docs](https://getstream.io/chat/docs/flutter-dart/flutter_offline/) This package provides a persistence client for fetching and saving chat data locally. Stream Chat Persistence uses [Moor](https://github.com/simolus3/moor) as a disk cache. From cdee8c9ef29fbcac6c3a9d52c7bee1b58883a95e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 20 Jul 2021 19:47:56 +0530 Subject: [PATCH 027/148] fix(Jiffy): check if locale is supported before setting it Signed-off-by: xsahil03x --- packages/stream_chat_flutter/lib/src/stream_chat.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index f55f565d..5dab2fd5 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -136,7 +136,11 @@ class StreamChatState extends State { @override void didChangeDependencies() { final locale = ui.window.locale; - Jiffy.locale(locale.languageCode); + final languageCode = locale.languageCode; + final availableLocales = Jiffy.getAllAvailableLocales(); + if (availableLocales.contains(languageCode)) { + Jiffy.locale(languageCode); + } super.didChangeDependencies(); } } From 5831a21f1e2a782fe3519a0656899e817ce70869 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 20 Jul 2021 18:05:18 +0200 Subject: [PATCH 028/148] update chat basics image --- .../docs/Flutter/assets/chat_basics.png | Bin 28913 -> 63987 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docusaurus/docs/Flutter/assets/chat_basics.png b/docusaurus/docs/Flutter/assets/chat_basics.png index 4eb0aa67639ff65c4331c28c45737a04b88be8f1..82e27cfa49ceac64fa7d27c37f280058397d9693 100644 GIT binary patch literal 63987 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 Nsj-z&{jGaR{|{{gfH(jE literal 28913 zcmc$`1yEhh*X|h-5(vRvLxAA!4#C~s-8HxqEV#RSAh=5i?ryheuvQ>!-Xz4Z|6~)ay}kMOZkr*EUFex^or}qYZcL=z^Qb377k8r*rp52Sd)h zjG<@SoPqRrf`~{@FDw`=iBE!0FIe{HxBsX2t|)=$@Bgd!|Du`yH2+^U^Plhk51Ui` z)I$XkZ*&xqG=`PFB-aVQ{Y^ep-S5iq4lVpx0MTPPi|3$^MRkpoI|7m{*0w#)M?ylvuedq<>xQ8=Z*q@uD(Hr9;_3K_DIF|=%HR+`L!Ehbev?%W4FWHM zz(T|QT_2%N*%=7mF9qam7qm|4v0lh%JDU+laN|vH7mIRyh^BvrqnBNoqF4w6TY@1f z`l5`a)UO%73xMHG?>N^A3LbP}{>Xom`~%N`V|8-bwlwULGdt~VPpQe)(_O@&v|YXo zpgW5Uoy8+dYa)NM?p)wC6k|)icY*c9*yvJPbn#!x|Gk-(d>?&7q>r8(%Q?dWW;tFC zx1U~hI^?9$kIpMgB%-|Rj{R;DPn7PA%);nehnC5KU!I}`rf0&BHkpkO-G)Z+btuLg z0x*6;{l4rRhDx*9b+OW_x-E3w9)xpDUfI^ui+g|U$7+^UiofTdB83sZG-GII*g7Di zO&~nE75%2{csaXJn0czgx39{tGff=bE#xvzYb@Hv1tOc_zMFcc=2S7i|ESyf*_!Xk zuVC2~84O_vq=lMG{N6DlN30eE$nK4Q<#U`@7xbG_HV#RN~dt{sNgY&{EQ zX9{!5O3xB900zUL;@|^IhR3yh$(gfT-HYqYsaunY@*O7zB z?=~I3RBtK*3;3O-qmS~3QfA^v?CqnBQ;T}0+$Xraxe_wrU$8Nur7wNvk~g>1P;tZf za{Kn`5Z|NrmUzYzbM6hklMoSUQu3$VZa>I72W*|Afihd80aQZRC%oG*^mht1B>{N& zqD{MV_8tB-STe(st7<64BH3lHz8w!)k;|)MzoJzk7tHrm374&|LN)lo)OPhjuxq>e zacgkRk0$E=(Q*(=0?Xi9l&RDQfn~e()!||lmt*0F{M^>u%0DOX+$YA6c*7D1lOv*G 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 Nky5yqdFQG7{{c%u2zdYi From c3312c2da21c04b60b76299103dc3795f9b03956 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 20 Jul 2021 14:35:15 -0400 Subject: [PATCH 029/148] chore: remove the `TranslatedMessage` class and add the `i18n` field to the `Message` class Tests have been updated to reflect the removal of this class --- .../lib/src/core/api/responses.dart | 2 +- .../lib/src/core/api/responses.g.dart | 3 +- .../lib/src/core/models/message.dart | 41 +++++-------------- .../lib/src/core/models/message.g.dart | 17 ++------ .../lib/src/core/models/own_user.g.dart | 3 ++ .../test/src/api/channel_test.dart | 9 ++-- .../stream_chat/test/src/api/client_test.dart | 8 ++-- .../test/src/core/api/message_api_test.dart | 8 ++-- 8 files changed, 35 insertions(+), 56 deletions(-) diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index e06a213a..033e91b2 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -75,7 +75,7 @@ class QueryChannelsResponse extends _BaseResponse { @JsonSerializable(createToJson: false) class TranslateMessageResponse extends _BaseResponse { /// Translated message - late TranslatedMessage message; + late Message message; /// Create a new instance from a json static TranslateMessageResponse fromJson(Map json) => diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index deba55f0..15b64a05 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -47,8 +47,7 @@ TranslateMessageResponse _$TranslateMessageResponseFromJson( Map json) { return TranslateMessageResponse() ..duration = json['duration'] as String? - ..message = - TranslatedMessage.fromJson(json['message'] as Map); + ..message = Message.fromJson(json['message'] as Map); } QueryMembersResponse _$QueryMembersResponseFromJson(Map json) { diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index d394dbdd..0d2094cb 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -2,8 +2,8 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/attachment.dart'; import 'package:stream_chat/src/core/models/reaction.dart'; -import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:uuid/uuid.dart'; part 'message.g.dart'; @@ -73,6 +73,7 @@ class Message extends Equatable { this.extraData = const {}, this.deletedAt, this.status = MessageSendingStatus.sent, + this.i18n, }) : id = id ?? const Uuid().v4(), pinExpires = pinExpires?.toUtc(), createdAt = createdAt ?? DateTime.now(), @@ -218,6 +219,9 @@ class Message extends Equatable { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime? deletedAt; + /// A Map of + final Map? i18n; + /// Known top level fields. /// Useful for [Serializer] methods. static const topLevelFields = [ @@ -248,6 +252,7 @@ class Message extends Equatable { 'pinned_at', 'pin_expires', 'pinned_by', + 'i18n', ]; /// Serialize to json @@ -285,6 +290,7 @@ class Message extends Equatable { User? pinnedBy, Map? extraData, MessageSendingStatus? status, + Map? i18n, }) { assert(() { if (pinExpires is! DateTime && @@ -324,6 +330,7 @@ class Message extends Equatable { pinnedBy: pinnedBy ?? this.pinnedBy, pinExpires: pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?, + i18n: i18n ?? this.i18n, ); } @@ -358,6 +365,7 @@ class Message extends Equatable { pinnedAt: other.pinnedAt, pinExpires: other.pinExpires, pinnedBy: other.pinnedBy, + i18n: other.i18n, ); @override @@ -390,35 +398,6 @@ class Message extends Equatable { pinnedBy, extraData, status, + i18n, ]; } - -/// A translated message -/// It has an additional property called [i18n] -@JsonSerializable() -class TranslatedMessage extends Message { - /// Constructor used for json serialization - TranslatedMessage(this.i18n) : super(); - - /// Create a new instance from a json - factory TranslatedMessage.fromJson(Map json) => - _$TranslatedMessageFromJson( - Serializer.moveToExtraDataFromRoot(json, topLevelFields), - ); - - /// A Map of - final Map? i18n; - - /// Known top level fields. - /// Useful for [Serializer] methods. - static final topLevelFields = [ - 'i18n', - ...Message.topLevelFields, - ]; - - /// Serialize to json - @override - Map toJson() => Serializer.moveFromExtraDataToRoot( - _$TranslatedMessageToJson(this), - ); -} diff --git a/packages/stream_chat/lib/src/core/models/message.g.dart b/packages/stream_chat/lib/src/core/models/message.g.dart index c49e2a6e..8ad1b3e1 100644 --- a/packages/stream_chat/lib/src/core/models/message.g.dart +++ b/packages/stream_chat/lib/src/core/models/message.g.dart @@ -67,6 +67,9 @@ Message _$MessageFromJson(Map json) { deletedAt: json['deleted_at'] == null ? null : DateTime.parse(json['deleted_at'] as String), + i18n: (json['i18n'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ), ); } @@ -107,18 +110,6 @@ Map _$MessageToJson(Message instance) { val['pinned_by'] = readonly(instance.pinnedBy); val['extra_data'] = instance.extraData; writeNotNull('deleted_at', readonly(instance.deletedAt)); + val['i18n'] = instance.i18n; return val; } - -TranslatedMessage _$TranslatedMessageFromJson(Map json) { - return TranslatedMessage( - (json['i18n'] as Map?)?.map( - (k, e) => MapEntry(k, e as String), - ), - ); -} - -Map _$TranslatedMessageToJson(TranslatedMessage instance) => - { - 'i18n': instance.i18n, - }; diff --git a/packages/stream_chat/lib/src/core/models/own_user.g.dart b/packages/stream_chat/lib/src/core/models/own_user.g.dart index 26e4786e..465faff2 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.g.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.g.dart @@ -36,5 +36,8 @@ OwnUser _$OwnUserFromJson(Map json) { online: json['online'] as bool? ?? false, extraData: json['extra_data'] as Map? ?? {}, banned: json['banned'] as bool? ?? false, + teams: + (json['teams'] as List?)?.map((e) => e as String).toList() ?? + [], ); } diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 39003fb7..5be55491 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -1610,9 +1610,12 @@ void main() { const messageId = 'test-message-id'; const language = 'hi'; // Hindi const translatedMessageText = 'नमस्ते'; - final translatedMessage = TranslatedMessage(const { - language: translatedMessageText, - }); + + final translatedMessage = Message( + i18n: const { + language: translatedMessageText, + }, + ); when(() => client.translateMessage(messageId, language)).thenAnswer( (_) async => TranslateMessageResponse()..message = translatedMessage, diff --git a/packages/stream_chat/test/src/api/client_test.dart b/packages/stream_chat/test/src/api/client_test.dart index 423445ec..5df7406b 100644 --- a/packages/stream_chat/test/src/api/client_test.dart +++ b/packages/stream_chat/test/src/api/client_test.dart @@ -2109,9 +2109,11 @@ void main() { const messageId = 'test-message-id'; const language = 'hi'; // Hindi const translatedMessageText = 'नमस्ते'; - final translatedMessage = TranslatedMessage(const { - language: translatedMessageText, - }); + final translatedMessage = Message( + i18n: const { + language: translatedMessageText, + }, + ); when(() => api.message.translateMessage(messageId, language)).thenAnswer( (_) async => TranslateMessageResponse()..message = translatedMessage, diff --git a/packages/stream_chat/test/src/core/api/message_api_test.dart b/packages/stream_chat/test/src/core/api/message_api_test.dart index 1aacb6d2..e89491a2 100644 --- a/packages/stream_chat/test/src/core/api/message_api_test.dart +++ b/packages/stream_chat/test/src/core/api/message_api_test.dart @@ -371,9 +371,11 @@ void main() { final path = '/messages/${message.id}/translate'; const translatedMessageText = 'नमस्ते'; - final translatedMessage = TranslatedMessage(const { - language: translatedMessageText, - }); + final translatedMessage = Message( + i18n: const { + language: translatedMessageText, + }, + ); when(() => client.post( path, From 62b058e675bd7c44d3b20f528a9c10bb1538750a Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 20 Jul 2021 15:07:30 -0400 Subject: [PATCH 030/148] chore: add 'language' field to user.dart --- packages/stream_chat/lib/src/core/models/user.dart | 8 ++++++++ packages/stream_chat/lib/src/core/models/user.g.dart | 2 ++ 2 files changed, 10 insertions(+) diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index 1360d14e..87b4f9aa 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -18,6 +18,7 @@ class User extends Equatable { this.extraData = const {}, this.banned = false, this.teams = const [], + this.language = 'en', }) : createdAt = createdAt ?? DateTime.now(), updatedAt = updatedAt ?? DateTime.now(); @@ -82,6 +83,13 @@ class User extends Equatable { ) final Map extraData; + /// The language this user prefers. + /// + /// Defaults to 'en'. + @JsonKey( + includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 'en') + final String? language; + @override int get hashCode => id.hashCode; diff --git a/packages/stream_chat/lib/src/core/models/user.g.dart b/packages/stream_chat/lib/src/core/models/user.g.dart index befcac03..7b193f04 100644 --- a/packages/stream_chat/lib/src/core/models/user.g.dart +++ b/packages/stream_chat/lib/src/core/models/user.g.dart @@ -25,6 +25,7 @@ User _$UserFromJson(Map json) { teams: (json['teams'] as List?)?.map((e) => e as String).toList() ?? [], + language: json['language'] as String? ?? 'en', ); } @@ -47,5 +48,6 @@ Map _$UserToJson(User instance) { writeNotNull('online', readonly(instance.online)); writeNotNull('banned', readonly(instance.banned)); val['extra_data'] = instance.extraData; + writeNotNull('language', readonly(instance.language)); return val; } From c6dd3b0cd7aa254fbdd353e0aaed8ddc60830717 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 20 Jul 2021 15:13:58 -0400 Subject: [PATCH 031/148] test: add 'language' field test to user_test.dart --- packages/stream_chat/test/src/core/models/user_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/stream_chat/test/src/core/models/user_test.dart b/packages/stream_chat/test/src/core/models/user_test.dart index ce488cab..5319c02b 100644 --- a/packages/stream_chat/test/src/core/models/user_test.dart +++ b/packages/stream_chat/test/src/core/models/user_test.dart @@ -29,6 +29,7 @@ void main() { expect(newUser.id, user.id); expect(newUser.role, user.role); expect(newUser.name, user.name); + expect(newUser.language, user.language); newUser = user.copyWith( id: 'test', @@ -41,6 +42,7 @@ void main() { expect(newUser.id, 'test'); expect(newUser.role, 'test'); expect(newUser.name, 'test'); + expect(newUser.language, 'en'); }); }); } From 0943980b51ec265fbf4ef5de342295ace8eadda7 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 20 Jul 2021 15:15:09 -0400 Subject: [PATCH 032/148] test: add 'i18n' field test to message_test.dart --- packages/stream_chat/test/src/core/models/message_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat/test/src/core/models/message_test.dart b/packages/stream_chat/test/src/core/models/message_test.dart index a5571430..b5e6ddcc 100644 --- a/packages/stream_chat/test/src/core/models/message_test.dart +++ b/packages/stream_chat/test/src/core/models/message_test.dart @@ -28,6 +28,7 @@ void main() { expect(message.pinnedAt, null); expect(message.pinExpires, null); expect(message.pinnedBy, null); + expect(message.i18n, null); }); test('should serialize to json correctly', () { From 0210e2d49f5ce6309b2f02b8a5cababb776ac253 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 20 Jul 2021 15:19:37 -0400 Subject: [PATCH 033/148] chore: fill out message.i18n docs more --- packages/stream_chat/lib/src/core/models/message.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 0d2094cb..7244f617 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -219,7 +219,7 @@ class Message extends Equatable { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime? deletedAt; - /// A Map of + /// A Map of translations. final Map? i18n; /// Known top level fields. From 247efb4e8f49b580036fd68a4a55c83011200cca Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 20 Jul 2021 15:22:15 -0400 Subject: [PATCH 034/148] chore: add missing JsonKey to Message.i18n --- packages/stream_chat/lib/src/core/models/message.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 7244f617..d5435222 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -220,6 +220,7 @@ class Message extends Equatable { final DateTime? deletedAt; /// A Map of translations. + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final Map? i18n; /// Known top level fields. From 25aa6f2a905f60a54b893864772d7aec82329d9c Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 20 Jul 2021 15:23:25 -0400 Subject: [PATCH 035/148] chore: update CHANGELOG.md --- packages/stream_chat/CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index e05599e1..f4a6a136 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,13 @@ +## Upcoming + +🛑️ Removed +- The `MessageTranslation` class has been removed. Use the new `i18n` field in the `Message` class instead. + +✅ Added + +- The `Message` class now has an `i18n` field for translations +- The `User` class now has a `language` field for the user's language preference. + ## 2.0.0 🛑️ Breaking Changes from `1.5.3` From f8911bb6e3087898e91f686ded96a3db874d3eee Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 21 Jul 2021 15:02:57 +0530 Subject: [PATCH 036/148] feat: Added tests and minor fixes --- .../lib/src/gradient_avatar.dart | 6 +- .../test/src/goldens/gradient_avatar_0.png | Bin 0 -> 35488 bytes .../test/src/goldens/gradient_avatar_1.png | Bin 0 -> 38197 bytes .../test/src/gradient_avatar_test.dart | 82 ++++++++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 packages/stream_chat_flutter/test/src/goldens/gradient_avatar_0.png create mode 100644 packages/stream_chat_flutter/test/src/goldens/gradient_avatar_1.png create mode 100644 packages/stream_chat_flutter/test/src/gradient_avatar_test.dart diff --git a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart index 0cb8c41c..dc97fb98 100644 --- a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart @@ -47,7 +47,7 @@ class _GradientAvatarState extends State { var result = ''; for (var i = 0; i < parts.length; i++) { - result = result + parts[i][0]; + result = result + parts[i][0].toUpperCase(); } return result; @@ -112,8 +112,8 @@ class DemoPainter extends CustomPainter { final textSize = smallerSide / 3; - final dxShift = (username.length == 2 ? 1.45 : 0.75) * textSize / 2; - final dyShift = (username.length == 2 ? 1.0 : 1.8) * textSize / 2; + final dxShift = (username.length == 2 ? 1.45 : 0.9) * textSize / 2; + final dyShift = (username.length == 2 ? 1.0 : 1.65) * textSize / 2; final fontSize = username.length == 2 ? textSize : textSize * 1.5; diff --git a/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_0.png b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_0.png new file mode 100644 index 0000000000000000000000000000000000000000..0906f11aca5a82f0fd4a2f6d9fd4cc712b5696cc GIT binary patch literal 35488 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≫B3&Iwv02P3D1z%;8>US?8W2DYwXeQ_HO8|V9D+*iH2-55n{y>uHEPx>TC zhl1pm;mwiku|uV-JvT9X485#IQ9kC7+XV}VaX>eZgN}w7Aa%`MaaLrKg?^kBspoXL zEuN7!69{H}VHF35c4P_7rz-K^j_y$hNr>F)i)oWn2w;{BsX`JA`{6BX+Ahm_6s5^@ z&Mx`Zu8q&DA{Vx+(WHzpf@YVP1v$P}W5rV-^tvm1u9fwr884i3GJYK$M(7d{1*3Be z&8HxMGYX9CYfI*QqFnPy@vzNw#%3+ke=X!+vAZxt(Nx0d6etI-W=ibrWp<$!DOTY? zIkGUVd|$i1z4@CcXohTD&GPN?27Fmb1(4h{J7?9Ywr^;EZ1e&+bsX_&XXMIIn`gAL z%bDOlGUSq5NXlZpk;Tu2%NcUC%BOCjZgjYXkV&UM){{#FegJ*UGOWD0quz!G%#)$y zy5gzTvP?UAJkHJBCyJD%ll~Mq&ch1|YnF!!BjJQOd)>x7rakXy=r`|T|Ic>t3>Tl~ zNNVe-17R!%?aQlJwtmRi*#3^vB<$iyVyuqrQ(&B*J!dg&l|R`bc!+@CK`ob^v6&s~ zsv<$T=sufRA$L`$*MViBp$;U8*$Job!!C3Qj;v)r8XMZawNpT$7p)qXusYS_3%YY% zgyFli6$Rx%qtv%Rt{tF2ndp!PcQ zg)eB;fs~CV!ps#DW}o8%b~(ThxJI7AdoDq2(c)^xpTWLr^&t>?|)Wmlr z48y>1FYvO5S&@eqSH$9@m6Mhf@( z5o$QnBoEM}eQSXaeKemGs@3B+9M;BmnD|fgVuX(ZLKBfWy@EYdYMfkeMV?%K_%UeS zby(1>t51-zaeIkARYtIAshtZx?AP3e6hJhrRHk*>*5^tgGdNkrk^8JpcTV(R&PH?F z-vfdqE|VyqfprJWcXA}Z$sdgM0Ady-IS<-00u)e~I>&Ji;A+8HK?5n_JfiR^MKjQ> z2*qAB)~hA)i47$ugwt)P?z)aJ&shQto8)l&{g%f(!9lC7F&A>V{Bucy{`bN?e#7U* zsfd+azs_ME{<=9I@r+T<%?hy^t%z|Pt-sN?ON0YfU6a!cUWNgA7RHxS^B7 z<*vRHbfz0vlF3wt-veA0JClN=j&;FB&x~v|SBGmm3gi}c14h~0^{6Memp;{7GM#~c zmN+g8aB`st${*3eYiNXT<*RJlApykp)F3R1&LDOTWfqq zK6=|Ms0k+WntZRIxYcMve<`JSkrbyC6Uh-jz_qFHFz85oMXm4Y?#97tLhbjJGI_1a z#F{sP2cNzW)qWN<#9~XIj`K?VG51BGZR5;k|4wm8Qkwm0vT1|eE_Hz07t|oUyu9n| zl_adrsJN`xZa*(^8$$Rax^;s`HV(iXs(>+K@z^CyH|-JtN_)9q)D3nbv9eNc^?@*h z7QOCDu06UWx)@tKs95{LVoe^=1$-^n7QO$n*3ck&|2}ICUPTvkYfq7g-WMr}KtuHY z31F>MF1pwzAttu1C^4~ZixT_4$uE|V1&oWH8oT?VhI=$X%24Eo!v7LPhqm8y>geaRU{$y|EM{{b$%c_07) literal 0 HcmV?d00001 diff --git a/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_1.png b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_1.png new file mode 100644 index 0000000000000000000000000000000000000000..d288abf084fc623592103f82517695b17398972d GIT binary patch literal 38197 zcmeFaXIPWx8#hc_tF+b;1yK;t&>~9^l_9e#0kr}JOacl-KtP!yhLwcaN(I6ye?Tl# zK~xeT4A~}e0I7_ig)j|a2+=@5hzXF82uoqPQ55dW*4 z&9?6qzn77b*>?7fm7|Qzcb8>kw#dqE27XdU7i+>Cdd1S9lqV|`FY=<@_>JCQy5K;iNRW^jLqLc ziN3PO45jr4ggFD0m%rZVr-X&E#XT_Psp%T?@prm`0o@%OPK|H**IhMR)}DGEQdUOn zy%av({du&x=5fxT-VT*+R&8*t`EB`~m%m!u%LS~{V0Y`=)o*`(9AEz7=vRBzHpxBo zvVgGZU&^0(+xq3KJ^lj0(w2{wy?(UBmZRVj!{wvRZ#Y^8*UCAt zMS8R#(7&=dVsbWy{>SpZ${Y6mr~`s!F7IHpVTV!(xI}k(2fnRORlmoe+8XEX+pg=H z1)tP5OLq{WP&^T%k=L7IS@w&P5;7Fk$T?eh@Zam-iZXf0{soQfwwz`VDCp0rvV@a% zJJ*>0vpFXD6KjS{5RpiC;kg}j%cU15UfwCiER4ir15i?1TmHKFBG^5$$Q>=>Mctfn z7k#F~k#!V=9<$D&tGc@8@!=~CH8AOSu|gs5RVJcm2NUho8^y=Lnd|o&mIdwc-Ego& zaplhGGF#V3Bp-a~30mik@o%yTzUvHpu4(0*WBsrAWI7ML9Q|?qY3EzA31jP|E}sPJ zo@)}6aOHXJf^r$i%Z2-EPUR3XS>azRxD3fde>eF@TjpeufA2tEb@}U?7dsL4j$2SD z_m?KWtrLcQxEjOjM9s6Cd))~NWv(APPP}}5Wpw|D=K8hEJ#ExVC`{5NsR-wilt}UA zkNo|?CA-(TdjBi?JT*CwZMWBVRs_FPR9QEl<7Z+Fbk^B(^`5N1@j8h&0Jn~8*zPC5 zdUyY8B0nbp?m&H=d3WQ>1;4BdT=~FD_Mvr3H)CW88{EX3gW!_rb#3{X8dokjy zFWFl*EKZjt2sfO|1F+Q%2cZIdW&jzQBqPvJ&D0D=zGdpbNMpQ`(51-#MdqzA`hCH&)v*(n8yLlZQg7Fu(1xtKRjY4zIfi@a&FtWc$Y`+#>MAjkQN&j*FUgw z)^jDhVue#x)@nNIQobG#gIr89!mFpPc{jI47~pG@$T2_t%U~Hi`=jh+Es^5hNtrum z`=|?ew&Y`I%sPQ#Y_hd8GkO-Hyd?0~>jW9fd#p3<7IlAk@~<&Svvsnc