diff --git a/.github/workflows/pr_title.yml b/.github/workflows/pr_title.yml index 2a53261b..eddb2ac1 100644 --- a/.github/workflows/pr_title.yml +++ b/.github/workflows/pr_title.yml @@ -12,13 +12,16 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@v2.1.0 + - uses: amannn/action-semantic-pull-request@v3.4.0 with: scopes: | llc persistence core ui + doc + repo + localization requireScope: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 293f60f6..6758b189 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,9 @@ This package provides business logic to fetch common things required for integra ### [stream_chat_flutter](https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat_flutter) This library includes both a low-level chat SDK and a set of reusable and customizable UI components. +### [stream_chat_localizations](https://github.com/GetStream/stream-chat-flutter/tree/master/packages/stream_chat_localizations) +This library includes a set of localization files for the Flutter UI components. + ## Flutter Chat Tutorial The best place to start is the [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/). diff --git a/melos.yaml b/melos.yaml index 6232c3fa..b645deb0 100644 --- a/melos.yaml +++ b/melos.yaml @@ -13,7 +13,7 @@ scripts: analyze: run: | - melos exec -c 4 --ignore="*example*" -- \ + melos exec -c 5 --ignore="*example*" -- \ dart analyze --fatal-infos . description: | Run `dart analyze` in all packages. @@ -26,7 +26,7 @@ scripts: lint:pub: run: | - melos exec -c 4 --no-private --ignore="*example*" -- \ + melos exec -c 5 --no-private --ignore="*example*" -- \ pub publish --dry-run description: | Run `pub publish --dry-run` in all packages. @@ -56,7 +56,7 @@ scripts: dir-exists: test test:flutter: - run: melos exec -c 3 --fail-fast -- "flutter test --coverage" + run: melos exec -c 4 --fail-fast -- "flutter test --coverage" description: Run Flutter tests for a specific package in this project. select-package: flutter: true @@ -64,7 +64,7 @@ scripts: coverage:ignore-file: run: | - melos exec -c 4 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from-coverage.sh" + melos exec -c 5 --fail-fast -- "\$MELOS_ROOT_PATH/.github/workflows/scripts/remove-from-coverage.sh" description: Removes all the ignored files from the coverage report. select-package: dir-exists: coverage diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index e05599e1..84f12d05 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,24 @@ +## 2.1.0 + +ЁЯЫСя╕П 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. + +ЁЯФД Changed + +- `client.user` is now deprecated in favor of `client.currentUser`. +- `client.userStream` is now deprecated in favor of `client.currentUserStream`. + +ЁЯРЮ Fixed + +- [#563](https://github.com/GetStream/stream-chat-flutter/issues/563): `Channel.stopWatching()` not working +- [#575](https://github.com/GetStream/stream-chat-flutter/issues/575): Wrong `OwnUser.*` + ## 2.0.0 ЁЯЫСя╕П Breaking Changes from `1.5.3` @@ -619,4 +640,4 @@ ## 0.0.2 -- first beta version \ No newline at end of file +- first beta version diff --git a/packages/stream_chat/example/lib/main.dart b/packages/stream_chat/example/lib/main.dart index 167f5f6b..46030341 100644 --- a/packages/stream_chat/example/lib/main.dart +++ b/packages/stream_chat/example/lib/main.dart @@ -245,5 +245,5 @@ class _MessageViewState extends State { /// Helper extension for quickly retrieving /// the current user id from a [StreamChatClient]. extension on StreamChatClient { - String get uid => state.user!.id; + String get uid => state.currentUser!.id; } diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 94a07f34..a05587ab 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -65,12 +65,12 @@ class Channel { /// Returns true if the channel is muted bool get isMuted => - _client.state.user?.channelMutes + _client.state.currentUser?.channelMutes .any((element) => element.channel.cid == cid) == true; /// Returns true if the channel is muted as a stream - Stream? get isMutedStream => _client.state.userStream + Stream? get isMutedStream => _client.state.currentUserStream .map((event) => event!.channelMutes.any((element) => element.channel.cid == cid) == true) @@ -382,7 +382,7 @@ class Channel { // ignore: parameter_assignments message = message.copyWith( createdAt: message.createdAt, - user: _client.state.user, + user: _client.state.currentUser, quotedMessage: quotedMessage, status: MessageSendingStatus.sending, attachments: message.attachments.map( @@ -693,7 +693,7 @@ class Channel { _checkInitialized(); final messageId = message.id; final now = DateTime.now(); - final user = _client.state.user; + final user = _client.state.currentUser; final latestReactions = [...message.latestReactions ?? []]; if (enforceUnique) { @@ -750,7 +750,7 @@ class Channel { Future deleteReaction( Message message, Reaction reaction) async { final type = reaction.type; - final user = _client.state.user; + final user = _client.state.currentUser; final reactionCounts = {...message.reactionCounts ?? {}}; if (reactionCounts.containsKey(type)) { @@ -1314,7 +1314,7 @@ class ChannelClientState { void _computeInitialUnread() { final userRead = channelState.read.firstWhereOrNull( - (r) => r.user.id == _channel._client.state.user?.id, + (r) => r.user.id == _channel._client.state.currentUser?.id, ); if (userRead != null) { unreadCount = userRead.unreadMessages; @@ -1431,7 +1431,7 @@ class ChannelClientState { void _listenReactionDeleted() { _subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) { - final userId = _channel.client.state.user!.id; + final userId = _channel.client.state.currentUser!.id; final message = event.message!.copyWith( ownReactions: [...event.message!.latestReactions!] ..removeWhere((it) => it.userId != userId), @@ -1442,7 +1442,7 @@ class ChannelClientState { void _listenReactions() { _subscriptions.add(_channel.on(EventType.reactionNew).listen((event) { - final userId = _channel.client.state.user!.id; + final userId = _channel.client.state.currentUser!.id; final message = event.message!.copyWith( ownReactions: [...event.message!.latestReactions!] ..removeWhere((it) => it.userId != userId), @@ -1458,7 +1458,7 @@ class ChannelClientState { EventType.reactionUpdated, ) .listen((event) { - final userId = _channel.client.state.user!.id; + final userId = _channel.client.state.currentUser!.id; final message = event.message!.copyWith( ownReactions: [...event.message!.latestReactions!] ..removeWhere((it) => it.userId != userId), @@ -1552,7 +1552,7 @@ class ChannelClientState { if (userReadIndex != null && userReadIndex != -1) { final userRead = readList.removeAt(userReadIndex); - if (userRead.user.id == _channel._client.state.user!.id) { + if (userRead.user.id == _channel._client.state.currentUser!.id) { unreadCount = 0; } readList.add(Read( @@ -1642,11 +1642,12 @@ class ChannelClientState { int get unreadCount => _unreadCountController.value; bool _countMessageAsUnread(Message message) { - final userId = _channel.client.state.user?.id; - final userIsMuted = _channel.client.state.user?.mutes.firstWhereOrNull( - (m) => m.user.id == message.user?.id, - ) != - null; + final userId = _channel.client.state.currentUser?.id; + final userIsMuted = + _channel.client.state.currentUser?.mutes.firstWhereOrNull( + (m) => m.user.id == message.user?.id, + ) != + null; return message.silent != true && message.shadowed != true && message.user?.id != userId && @@ -1795,7 +1796,7 @@ class ChannelClientState { (event) { if (event.user != null) { final user = event.user!; - if (user.id != _channel.client.state.user?.id) { + if (user.id != _channel.client.state.currentUser?.id) { _typings[user] = event; _typingEventsController.add(_typings); } @@ -1808,7 +1809,7 @@ class ChannelClientState { (event) { if (event.user != null) { final user = event.user!; - if (user.id != _channel.client.state.user?.id) { + if (user.id != _channel.client.state.currentUser?.id) { _typings.remove(event.user); _typingEventsController.add(_typings); } diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 050d36f1..b25ebe88 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -25,12 +25,14 @@ import 'package:stream_chat/src/core/models/member.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/own_user.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; import 'package:stream_chat/src/core/util/utils.dart'; import 'package:stream_chat/src/db/chat_persistence_client.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/location.dart'; import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/src/ws/websocket.dart'; +import 'package:stream_chat/version.dart'; /// Handler function used for logging records. Function requires a single /// [LogRecord] as the only parameter. @@ -42,6 +44,10 @@ final _levelEmojiMapper = { Level.SEVERE: 'ЁЯЪи', }; +final _userAgent = 'stream-chat-dart-client-' + '${CurrentPlatform.name}-' + '${PACKAGE_VERSION.split('+')[0]}'; + /// The official Dart client for Stream Chat, /// a service for building chat applications. /// This library can be used on any Dart project and on both mobile and web apps @@ -80,6 +86,7 @@ class StreamChatClient { location: location, connectTimeout: connectTimeout, receiveTimeout: receiveTimeout, + headers: {'X-Stream-Client': _userAgent}, ); _chatApi = chatApi ?? @@ -99,6 +106,7 @@ class StreamChatClient { tokenManager: _tokenManager, handler: handleEvent, logger: detachedLogger('ЁЯФМ'), + queryParameters: {'X-Stream-Client': _userAgent}, ); _retryPolicy = retryPolicy ?? @@ -308,23 +316,21 @@ class StreamChatClient { ); final ownUser = OwnUser.fromUser(user); - state.user = ownUser; + state.currentUser = ownUser; - if (!connectWebSocket) { - return ownUser; - } + if (!connectWebSocket) return ownUser; try { if (_originalChatPersistenceClient != null) { _chatPersistenceClient = _originalChatPersistenceClient; await _chatPersistenceClient!.connect(ownUser.id); } - final res = await openConnection(); - return res; + final connectedUser = await openConnection(); + return state.currentUser = connectedUser; } catch (e, stk) { if (e is StreamWebSocketError && e.isRetriable) { final event = await _chatPersistenceClient?.getConnectionInfo(); - if (event != null) return event.me?.merge(ownUser) ?? ownUser; + if (event != null) return ownUser.merge(event.me); } logger.severe('error connecting user : ${ownUser.id}', e, stk); rethrow; @@ -334,12 +340,12 @@ class StreamChatClient { /// Creates a new WebSocket connection with the current user. Future openConnection() async { assert( - state.user != null, + state.currentUser != null, 'User is not set on client, ' 'use `connectUser` or `connectAnonymousUser` instead', ); - final user = state.user!; + final user = state.currentUser!; logger.info('Opening web-socket connection for ${user.id}'); @@ -363,7 +369,7 @@ class StreamChatClient { try { final event = await _ws.connect(user); - return event.me?.merge(user) ?? user; + return user.merge(event.me); } catch (e, stk) { logger.severe('error connecting ws', e, stk); rethrow; @@ -378,7 +384,7 @@ class StreamChatClient { void closeConnection() { if (wsConnectionStatus == ConnectionStatus.disconnected) return; - logger.info('Closing web-socket connection for ${state.user?.id}'); + logger.info('Closing web-socket connection for ${state.currentUser?.id}'); _wsConnectionStatus = ConnectionStatus.disconnected; _connectionStatusSubscription?.cancel(); @@ -388,9 +394,6 @@ class StreamChatClient { } void _handleHealthCheckEvent(Event event) { - final user = event.me; - if (user != null) state.user = user; - final connectionId = event.connectionId; if (connectionId != null) { _connectionIdManager.setConnectionId(connectionId); @@ -1290,7 +1293,7 @@ class StreamChatClient { /// If [flushChatPersistence] is true the client deletes all offline /// user's data. Future disconnectUser({bool flushChatPersistence = false}) async { - logger.info('Disconnecting user : ${state.user?.id}'); + logger.info('Disconnecting user : ${state.currentUser?.id}'); // resetting state state.dispose(); @@ -1331,22 +1334,6 @@ class ClientState { /// Creates a new instance listening to events and updating the state ClientState(this._client) { _subscriptions.addAll([ - _client - .on() - .where((event) => event.me != null) - .map((e) => e.me) - .listen((user) { - _userController.add(user); - final totalUnreadCount = user?.totalUnreadCount; - if (totalUnreadCount != null) { - _totalUnreadCountController.add(totalUnreadCount); - } - - final unreadChannels = user?.unreadChannels; - if (unreadChannels != null) { - _unreadChannelsController.add(unreadChannels); - } - }), _client .on() .map((event) => event.unreadChannels) @@ -1386,8 +1373,8 @@ class ClientState { void _listenUserUpdated() { _subscriptions.add(_client.on(EventType.userUpdated).listen((event) { - if (event.user!.id == user!.id) { - user = OwnUser.fromJson(event.user!.toJson()); + if (event.user!.id == currentUser!.id) { + currentUser = OwnUser.fromJson(event.user!.toJson()); } updateUser(event.user); })); @@ -1409,9 +1396,10 @@ class ClientState { final StreamChatClient _client; - /// Update user information - set user(OwnUser? user) { - _userController.add(user); + /// Sets the user currently interacting with the client + /// note: this fully overrides the [currentUser] + set currentUser(OwnUser? user) { + _currentUserController.add(user); } /// Update all the [users] with the provided [userList] @@ -1428,10 +1416,24 @@ class ClientState { void updateUser(User? user) => updateUsers([user]); /// The current user - OwnUser? get user => _userController.valueOrNull; + OwnUser? get currentUser => _currentUserController.valueOrNull; /// The current user as a stream - Stream get userStream => _userController.stream; + Stream get currentUserStream => _currentUserController.stream; + + // coverage:ignore-start + + /// The current user + @Deprecated('Use `.currentUser` instead, Will be removed in future releases') + OwnUser? get user => _currentUserController.valueOrNull; + + /// The current user as a stream + @Deprecated( + 'Use `.currentUserStream` instead, Will be removed in future releases', + ) + Stream get userStream => _currentUserController.stream; + + // coverage:ignore-end /// The current user Map get users => _usersController.value; @@ -1463,7 +1465,7 @@ class ClientState { } final _channelsController = BehaviorSubject>.seeded({}); - final _userController = BehaviorSubject(); + final _currentUserController = BehaviorSubject(); final _usersController = BehaviorSubject>.seeded({}); final _unreadChannelsController = BehaviorSubject.seeded(0); final _totalUnreadCountController = BehaviorSubject.seeded(0); @@ -1471,7 +1473,7 @@ class ClientState { /// Call this method to dispose this object void dispose() { _subscriptions.forEach((s) => s.cancel()); - _userController.close(); + _currentUserController.close(); _unreadChannelsController.close(); _totalUnreadCountController.close(); channels.values.forEach((c) => c.dispose()); diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 68f6c4f8..1ba7f333 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -289,6 +289,7 @@ class ChannelApi { ) async { final response = await _client.post( '${_getChannelUrl(channelId, channelType)}/stop-watching', + data: {}, ); return EmptyResponse.fromJson(response.data); } 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/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index 406138c3..c19cdb8a 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -10,9 +10,7 @@ import 'package:stream_chat/src/core/http/interceptor/connection_id_interceptor. import 'package:stream_chat/src/core/http/interceptor/logging_interceptor.dart'; import 'package:stream_chat/src/core/http/stream_chat_dio_error.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; -import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; import 'package:stream_chat/src/location.dart'; -import 'package:stream_chat/version.dart'; part 'stream_http_client_options.dart'; @@ -33,11 +31,14 @@ class StreamHttpClient { ..options.baseUrl = _options.baseUrl ..options.receiveTimeout = _options.receiveTimeout.inMilliseconds ..options.connectTimeout = _options.connectTimeout.inMilliseconds - ..options.queryParameters = {'api_key': apiKey} + ..options.queryParameters = { + 'api_key': apiKey, + ..._options.queryParameters, + } ..options.headers = { 'Content-Type': 'application/json', - 'X-Stream-Client': _options.userAgent, 'Content-Encoding': 'application/gzip', + ..._options.headers, } ..interceptors.addAll([ if (tokenManager != null) AuthInterceptor(this, tokenManager), diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart b/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart index faad46a1..01cdb12e 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart @@ -10,6 +10,8 @@ class StreamHttpClientOptions { this.location, this.connectTimeout = const Duration(seconds: 6), this.receiveTimeout = const Duration(seconds: 6), + this.queryParameters = const {}, + this.headers = const {}, }) : _baseUrl = baseUrl ?? _defaultBaseURL; final String _baseUrl; @@ -32,8 +34,20 @@ class StreamHttpClientOptions { /// received timeout, default to 6s final Duration receiveTimeout; - /// Get the current user agent - String get userAgent => 'stream-chat-dart-client-' - '${CurrentPlatform.name}-' - '${PACKAGE_VERSION.split('+')[0]}'; + /// Common query parameters. + /// + /// List values use the default [ListFormat.multiCompatible]. + /// + /// The value can be overridden per parameter by adding a [MultiParam] + /// object wrapping the actual List value and the desired format. + final Map queryParameters; + + /// Http request headers. + /// The keys of initial headers will be converted to lowercase, + /// for example 'Content-Type' will be converted to 'content-type'. + /// + /// The key of Header Map is case-insensitive + /// eg: content-type and Content-Type are + /// regard as the same key. + final Map headers; } diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index d394dbdd..6ad8721c 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,10 @@ class Message extends Equatable { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime? deletedAt; + /// A Map of translations. + @JsonKey(includeIfNull: false) + final Map? i18n; + /// Known top level fields. /// Useful for [Serializer] methods. static const topLevelFields = [ @@ -248,6 +253,7 @@ class Message extends Equatable { 'pinned_at', 'pin_expires', 'pinned_by', + 'i18n', ]; /// Serialize to json @@ -285,6 +291,7 @@ class Message extends Equatable { User? pinnedBy, Map? extraData, MessageSendingStatus? status, + Map? i18n, }) { assert(() { if (pinExpires is! DateTime && @@ -324,6 +331,7 @@ class Message extends Equatable { pinnedBy: pinnedBy ?? this.pinnedBy, pinExpires: pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?, + i18n: i18n ?? this.i18n, ); } @@ -358,6 +366,7 @@ class Message extends Equatable { pinnedAt: other.pinnedAt, pinExpires: other.pinExpires, pinnedBy: other.pinnedBy, + i18n: other.i18n, ); @override @@ -390,35 +399,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..25254e2c 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)); + writeNotNull('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.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index 679abb47..fb5b1caf 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -27,6 +27,7 @@ class OwnUser extends User { Map extraData = const {}, bool banned = false, List teams = const [], + String? language, }) : super( id: id, role: role, @@ -37,6 +38,7 @@ class OwnUser extends User { extraData: extraData, banned: banned, teams: teams, + language: language, ); /// Create a new instance from a json @@ -54,6 +56,7 @@ class OwnUser extends User { banned: user.banned, extraData: user.extraData, teams: user.teams, + language: user.language, ); /// Creates a copy of [OwnUser] with specified attributes overridden. @@ -73,6 +76,7 @@ class OwnUser extends User { List? mutes, int? totalUnreadCount, int? unreadChannels, + String? language, }) => OwnUser( id: id ?? this.id, @@ -89,15 +93,13 @@ class OwnUser extends User { mutes: mutes ?? this.mutes, totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, unreadChannels: unreadChannels ?? this.unreadChannels, + language: language ?? this.language, ); /// Returns a new [OwnUser] that is a combination of this ownUser /// and the given [other] ownUser. OwnUser merge(OwnUser? other) { - if (other == null) { - return this; - } - + if (other == null) return this; return copyWith( banned: other.banned, channelMutes: other.channelMutes, @@ -113,6 +115,7 @@ class OwnUser extends User { totalUnreadCount: other.totalUnreadCount, unreadChannels: other.unreadChannels, updatedAt: other.updatedAt, + language: other.language, ); } 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..ca4acdea 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,9 @@ 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() ?? + [], + language: json['language'] as String?, ); } diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index 1360d14e..a7e7a51e 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, }) : createdAt = createdAt ?? DateTime.now(), updatedAt = updatedAt ?? DateTime.now(); @@ -36,6 +37,7 @@ class User extends Equatable { 'online', 'banned', 'teams', + 'language', ]; /// User id @@ -82,8 +84,11 @@ class User extends Equatable { ) final Map extraData; - @override - int get hashCode => id.hashCode; + /// The language this user prefers. + /// + /// Defaults to 'en'. + @JsonKey(includeIfNull: false) + final String? language; /// Shortcut for user name String get name { @@ -98,11 +103,6 @@ class User extends Equatable { static List? toIds(List? users) => users?.map((u) => u.id).toList(); - @override - bool operator ==(Object other) => - identical(this, other) || - other is User && runtimeType == other.runtimeType && id == other.id; - /// Serialize to json Map toJson() => Serializer.moveFromExtraDataToRoot( _$UserToJson(this), @@ -119,6 +119,7 @@ class User extends Equatable { Map? extraData, bool? banned, List? teams, + String? language, }) => User( id: id ?? this.id, @@ -130,18 +131,9 @@ class User extends Equatable { extraData: extraData ?? this.extraData, banned: banned ?? this.banned, teams: teams ?? this.teams, + language: language ?? this.language, ); @override - List get props => [ - id, - role, - teams, - createdAt, - updatedAt, - lastActive, - online, - banned, - extraData, - ]; + List get props => [id]; } 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..dd3183d5 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?, ); } @@ -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', instance.language); return val; } diff --git a/packages/stream_chat/lib/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart index f9d8a210..b26d811e 100644 --- a/packages/stream_chat/lib/src/ws/websocket.dart +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -41,11 +41,15 @@ class WebSocket with TimerHelper { this.reconnectionMonitorInterval = 10, this.healthCheckInterval = 20, this.reconnectionMonitorTimeout = 40, + this.queryParameters = const {}, }) : _logger = logger; /// final String apiKey; + /// Additional query parameters to be added to the websocket url + final Map queryParameters; + /// WS base url final String baseUrl; @@ -156,6 +160,7 @@ class WebSocket with TimerHelper { 'api_key': apiKey, 'authorization': token.rawValue, 'stream-auth-type': token.authType.raw, + ...queryParameters, }; final scheme = baseUrl.startsWith('https') ? 'wss' : 'ws'; final host = baseUrl.replaceAll(RegExp(r'(^\w+:|^)\/\/'), ''); diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index d09a2ccc..aba200a5 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '2.0.0'; +const PACKAGE_VERSION = '2.1.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 1f145992..78f5380b 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 2.0.0 +version: 2.1.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat/test/fixtures/user.json b/packages/stream_chat/test/fixtures/user.json index 49b972a3..b22c8553 100644 --- a/packages/stream_chat/test/fixtures/user.json +++ b/packages/stream_chat/test/fixtures/user.json @@ -1,5 +1,6 @@ { "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", "role": "test-role", - "name": "John" + "name": "John", + "language": "en" } \ No newline at end of file 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..a1a3fc96 100644 --- a/packages/stream_chat/test/src/api/client_test.dart +++ b/packages/stream_chat/test/src/api/client_test.dart @@ -155,7 +155,7 @@ void main() { group('`.openConnection`', () { test('should throw if state does not contain user', () async { - expect(client.state.user, isNull); + expect(client.state.currentUser, isNull); try { await client.openConnection(); } catch (e) { @@ -164,7 +164,7 @@ void main() { }); test('should throw if connection is already in progress', () async { - expect(client.state.user, isNull); + expect(client.state.currentUser, isNull); try { await client.connectAnonymousUser(); await client.openConnection(); @@ -179,7 +179,7 @@ void main() { }); test('should throw if connection is already available', () async { - expect(client.state.user, isNull); + expect(client.state.currentUser, isNull); try { await client.connectAnonymousUser(); // waiting 300ms for `wsConnectionStatusStream` to emit @@ -799,7 +799,7 @@ void main() { }); test('`.disconnectUser` should reset state and user', () async { - expect(client.state.user, isNotNull); + expect(client.state.currentUser, isNotNull); expect(client.wsConnectionStatus, ConnectionStatus.connected); expectLater( @@ -810,7 +810,7 @@ void main() { await client.disconnectUser(); - expect(client.state.user, isNull); + expect(client.state.currentUser, isNull); expect(client.wsConnectionStatus, ConnectionStatus.disconnected); }); }); @@ -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/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index ab530ff1..c43f161f 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -595,14 +595,14 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/stop-watching'; - when(() => client.post(path)).thenAnswer( + when(() => client.post(path, data: {})).thenAnswer( (_) async => successResponse(path, data: {})); final res = await channelApi.stopWatching(channelId, channelType); expect(res, isNotNull); - verify(() => client.post(path)).called(1); + verify(() => client.post(path, data: {})).called(1); verifyNoMoreInteractions(client); }); } 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, diff --git a/packages/stream_chat/test/src/core/http/stream_http_client_options_test.dart b/packages/stream_chat/test/src/core/http/stream_http_client_options_test.dart index a03434fa..02cfc07a 100644 --- a/packages/stream_chat/test/src/core/http/stream_http_client_options_test.dart +++ b/packages/stream_chat/test/src/core/http/stream_http_client_options_test.dart @@ -9,6 +9,8 @@ void main() { expect(options.baseUrl, 'https://chat-us-east-1.stream-io-api.com'); expect(options.connectTimeout, const Duration(seconds: 6)); expect(options.receiveTimeout, const Duration(seconds: 6)); + expect(options.queryParameters, const {}); + expect(options.headers, const {}); }); test('should override all the default set params', () { @@ -16,11 +18,15 @@ void main() { baseUrl: 'base-url', connectTimeout: Duration(seconds: 3), receiveTimeout: Duration(seconds: 3), + headers: {'test': 'test'}, + queryParameters: {'123': '123'}, ); expect(options.location, isNull); expect(options.baseUrl, 'base-url'); expect(options.connectTimeout, const Duration(seconds: 3)); expect(options.receiveTimeout, const Duration(seconds: 3)); + expect(options.headers, {'test': 'test'}); + expect(options.queryParameters, {'123': '123'}); }); group('should create baseUrl according to provided location', () { diff --git a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart index ab4c4984..7860a0d6 100644 --- a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart +++ b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart @@ -96,7 +96,7 @@ void main() { await client.get('path'); } catch (_) {} - verify(() => logger.info(any())).called(16); + verify(() => logger.info(any())).called(greaterThan(0)); }); test('loggingInterceptor should log error', () async { @@ -108,7 +108,7 @@ void main() { await client.get('path'); } catch (_) {} - verify(() => logger.severe(any())).called(8); + verify(() => logger.severe(any())).called(greaterThan(0)); }); test('`.lock` should lock the dio client', () async { 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', () { 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'); }); }); } diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart index 15d5991c..8d3f6d37 100644 --- a/packages/stream_chat/test/src/fakes.dart +++ b/packages/stream_chat/test/src/fakes.dart @@ -89,7 +89,7 @@ class FakeChatApi extends Fake implements StreamChatApi { class FakeClientState extends Fake implements ClientState { @override - OwnUser? get user => OwnUser(id: 'test-user-id'); + OwnUser? get currentUser => OwnUser(id: 'test-user-id'); @override int totalUnreadCount = 0; diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index e606d32d..487af25e 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,21 @@ +## 2.1.0 + +тЬЕ Added + +- Added `MessageListView.paginationLimit` +- `MessageText` renders message translation if available +- Allow the various ListView widgets to be themed via ThemeData classes +- Added `bottomRowBuilder` and `deletedBottomRowBuilder` that build a widget below a `MessageWidget` + +ЁЯФД Changed + +- `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`. +- `StreamChat.of(context).userStream` is now deprecated in favor of `StreamChat.of(context).currentUserStream`. + +ЁЯРЮ Fixed + +- Fix floating date divider not having a fixed size + ## 2.0.0 ЁЯЫСя╕П Breaking Changes from `1.5.4` @@ -663,4 +681,4 @@ The property showVideoFullScreen was added mainly because of this issue brianega ## 0.0.1 -- First release \ No newline at end of file +- First release 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/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index 89362d81..c2663a6a 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -1,11 +1,8 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_persistence/stream_chat_persistence.dart'; - -/// A chat-persisted StreamChatClient -final chatPersistentClient = StreamChatPersistenceClient( - logLevel: Level.INFO, -); +import 'package:stream_chat_localizations/stream_chat_localizations.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -15,7 +12,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 @@ -72,6 +69,13 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) => MaterialApp( theme: ThemeData.light(), darkTheme: ThemeData.dark(), + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('it'), + ], + localizationsDelegates: GlobalStreamChatLocalizations.delegates, builder: (context, widget) => StreamChat( client: client, child: widget, diff --git a/packages/stream_chat_flutter/example/lib/split_view.dart b/packages/stream_chat_flutter/example/lib/split_view.dart index 454071a6..52c0e885 100644 --- a/packages/stream_chat_flutter/example/lib/split_view.dart +++ b/packages/stream_chat_flutter/example/lib/split_view.dart @@ -103,7 +103,7 @@ class ChannelListPage extends StatelessWidget { : null, filter: Filter.in_( 'members', - [StreamChat.of(context).user!.id], + [StreamChat.of(context).currentUser!.id], ), sort: const [SortOption('last_message_at')], pagination: const PaginationParams( diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 79f8674c..5fa9cfdb 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -80,7 +80,7 @@ class ChannelListPage extends StatelessWidget { child: ChannelListView( filter: Filter.in_( 'members', - [StreamChat.of(context).user!.id], + [StreamChat.of(context).currentUser!.id], ), sort: const [SortOption('last_message_at')], pagination: const PaginationParams( diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index 78081c85..9658eec4 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -81,7 +81,7 @@ class ChannelListPage extends StatelessWidget { child: ChannelListView( filter: Filter.in_( 'members', - [StreamChat.of(context).user!.id], + [StreamChat.of(context).currentUser!.id], ), channelPreviewBuilder: _channelPreviewBuilder, // sort: [SortOption('last_message_at')], diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index 07942302..df94cf06 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -66,7 +66,7 @@ class ChannelListPage extends StatelessWidget { child: ChannelListView( filter: Filter.in_( 'members', - [StreamChat.of(context).user!.id], + [StreamChat.of(context).currentUser!.id], ), sort: const [SortOption('last_message_at')], pagination: const PaginationParams( diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index dbbf31a0..b9089d6a 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -72,7 +72,7 @@ class ChannelListPage extends StatelessWidget { child: ChannelListView( filter: Filter.in_( 'members', - [StreamChat.of(context).user!.id], + [StreamChat.of(context).currentUser!.id], ), sort: const [SortOption('last_message_at')], pagination: const PaginationParams( @@ -115,7 +115,8 @@ class ChannelPage extends StatelessWidget { MessageWidget _, ) { final message = details.message; - final isCurrentUser = StreamChat.of(context).user!.id == message.user!.id; + final isCurrentUser = + StreamChat.of(context).currentUser!.id == message.user!.id; final textAlign = isCurrentUser ? TextAlign.right : TextAlign.left; final color = isCurrentUser ? Colors.blueGrey : Colors.blue; diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index fcf86b5c..cf94b568 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -99,7 +99,7 @@ class ChannelListPage extends StatelessWidget { child: ChannelListView( filter: Filter.in_( 'members', - [StreamChat.of(context).user!.id], + [StreamChat.of(context).currentUser!.id], ), sort: const [SortOption('last_message_at')], pagination: const PaginationParams( diff --git a/packages/stream_chat_flutter/example/macos/.gitignore b/packages/stream_chat_flutter/example/macos/.gitignore new file mode 100644 index 00000000..d2fd3772 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/.gitignore @@ -0,0 +1,6 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/xcuserdata/ diff --git a/packages/stream_chat_flutter/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/stream_chat_flutter/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/stream_chat_flutter/example/macos/Flutter/Flutter-Release.xcconfig b/packages/stream_chat_flutter/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/stream_chat_flutter/example/macos/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_flutter/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..f8c105bb --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,635 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 7A465D4E5940248C04D2D4E3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5ED1F4FA50EB1433A201473C /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 2BCA7399119839DE435DACD6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 2C2B248A2BB89C8B353A7D81 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 5ED1F4FA50EB1433A201473C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + C4CD72858CD59598795BB48E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7A465D4E5940248C04D2D4E3 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 35E4E72D48C70FBCFEDFB30C /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 35E4E72D48C70FBCFEDFB30C /* Pods */ = { + isa = PBXGroup; + children = ( + 2C2B248A2BB89C8B353A7D81 /* Pods-Runner.debug.xcconfig */, + C4CD72858CD59598795BB48E /* Pods-Runner.release.xcconfig */, + 2BCA7399119839DE435DACD6 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5ED1F4FA50EB1433A201473C /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 72C2655B408A295073A1CEB6 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + F8C1BBEE8C9F4830ECBA88A2 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0930; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 72C2655B408A295073A1CEB6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F8C1BBEE8C9F4830ECBA88A2 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/stream_chat_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/stream_chat_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/stream_chat_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/stream_chat_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..ae8ff59d --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_flutter/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/stream_chat_flutter/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/stream_chat_flutter/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/stream_chat_flutter/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/stream_chat_flutter/example/macos/Runner/AppDelegate.swift b/packages/stream_chat_flutter/example/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..d53ef643 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..3c4935a7 Binary files /dev/null and b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..ed4cc164 Binary files /dev/null and b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..483be613 Binary files /dev/null and b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bcbf36df Binary files /dev/null and b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..9c0a6528 Binary files /dev/null and b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..e71a7261 Binary files /dev/null and b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..8a31fe2d Binary files /dev/null and b/packages/stream_chat_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/stream_chat_flutter/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/stream_chat_flutter/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..537341ab --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_flutter/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/stream_chat_flutter/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..cf9be60c --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright ┬й 2021 com.example. All rights reserved. diff --git a/packages/stream_chat_flutter/example/macos/Runner/Configs/Debug.xcconfig b/packages/stream_chat_flutter/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/stream_chat_flutter/example/macos/Runner/Configs/Release.xcconfig b/packages/stream_chat_flutter/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/stream_chat_flutter/example/macos/Runner/Configs/Warnings.xcconfig b/packages/stream_chat_flutter/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/stream_chat_flutter/example/macos/Runner/DebugProfile.entitlements b/packages/stream_chat_flutter/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..e585d0e0 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/packages/stream_chat_flutter/example/macos/Runner/Info.plist b/packages/stream_chat_flutter/example/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/stream_chat_flutter/example/macos/Runner/MainFlutterWindow.swift b/packages/stream_chat_flutter/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..2722837e --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/stream_chat_flutter/example/macos/Runner/Release.entitlements b/packages/stream_chat_flutter/example/macos/Runner/Release.entitlements new file mode 100644 index 00000000..852fa1a4 --- /dev/null +++ b/packages/stream_chat_flutter/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index d8b29d3f..d3fec032 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -33,6 +33,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/attachment/attachment_upload_state_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart index b1b29c4c..af46515e 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.barsBg, ), 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 99c3ec40..f57e1ee6 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 { @@ -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, @@ -286,7 +286,10 @@ class FileAttachment extends AttachmentWidget { progressIndicatorColor: theme.colorTheme.accentPrimary, ), 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 f14abffc..7eb2606e 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -2,8 +2,10 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; +import 'package:stream_chat_flutter/src/visible_footnote.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 +73,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 +136,7 @@ class GiphyAttachment extends AttachmentWidget { }); }, child: Text( - 'Cancel', + context.translations.cancelLabel.toLowerCase(), style: StreamChatTheme.of(context) .textTheme .bodyBold @@ -166,7 +168,7 @@ class GiphyAttachment extends AttachmentWidget { }); }, child: Text( - 'Shuffle', + context.translations.shuffleLabel, style: StreamChatTheme.of(context) .textTheme .bodyBold @@ -199,7 +201,7 @@ class GiphyAttachment extends AttachmentWidget { }); }, child: Text( - 'Send', + context.translations.sendLabel, style: TextStyle( color: StreamChatTheme.of(context) .colorTheme @@ -216,36 +218,11 @@ class GiphyAttachment extends AttachmentWidget { ), ), const SizedBox(height: 4), - Align( + const Align( alignment: Alignment.centerRight, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - StreamSvgIcon.eye( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), - size: 16, - ), - const SizedBox( - width: 8, - ), - Text( - 'Only visible to you', - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5)), - ), - ], - ), + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: VisibleFootnote(), ), ), ], @@ -339,7 +316,7 @@ class GiphyAttachment extends AttachmentWidget { size: 16, ), Text( - 'GIPHY', + context.translations.giphyLabel.toUpperCase(), style: TextStyle( color: StreamChatTheme.of(context).colorTheme.barsBg, 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 ea189b91..b5a3cc87 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.textLowEmphasis, @@ -80,7 +80,7 @@ class AttachmentActionsModal extends StatelessWidget { ), _buildButton( context, - 'Show in Chat', + context.translations.showInChatLabel, StreamSvgIcon.eye( size: 24, color: theme.colorTheme.textHighEmphasis, @@ -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.textLowEmphasis, @@ -138,10 +139,11 @@ class AttachmentActionsModal extends StatelessWidget { ); }, ), - if (StreamChat.of(context).user?.id == message.user?.id) + if (StreamChat.of(context).currentUser?.id == + message.user?.id) _buildButton( context, - 'Delete', + context.translations.deleteLabel.capitalize(), StreamSvgIcon.delete( size: 24, color: theme.colorTheme.accentError, diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index 4ab6ba31..551fd278 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -141,7 +141,7 @@ class ChannelAvatar extends StatelessWidget { return child; } - final currentUser = streamChat.user!; + final currentUser = streamChat.currentUser!; final otherMembers = channel.state!.members .where((it) => it.userId != currentUser.id) .toList(growable: false); @@ -149,7 +149,7 @@ class ChannelAvatar extends StatelessWidget { // our own space, no other members if (otherMembers.isEmpty) { return BetterStreamBuilder( - stream: streamChat.client.state.userStream.map((it) => it!), + stream: streamChat.client.state.currentUserStream.map((it) => it!), initialData: currentUser, builder: (context, user) => UserAvatar( borderRadius: borderRadius ?? previewTheme?.borderRadius, 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 368bfbd8..a06da0a8 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 { @@ -27,8 +28,8 @@ class _ChannelBottomSheetState extends State { final members = channel.state?.members ?? []; - final userAsMember = - members.firstWhere((e) => e.user?.id == _streamChatState.user?.id); + final userAsMember = members + .firstWhere((e) => e.user?.id == _streamChatState.currentUser?.id); final isOwner = userAsMember.role == 'owner'; return Material( @@ -149,7 +150,7 @@ class _ChannelBottomSheetState extends State { color: _streamChatThemeData.colorTheme.textLowEmphasis, ), ), - title: 'View Info', + title: context.translations.viewInfoLabel, onTap: widget.onViewInfoTap, ), if (!channel.isDistinct) @@ -160,7 +161,7 @@ class _ChannelBottomSheetState extends State { color: _streamChatThemeData.colorTheme.textLowEmphasis, ), ), - title: 'Leave Group', + title: context.translations.leaveGroupLabel, onTap: () async { setState(() { _showActions = false; @@ -179,7 +180,7 @@ class _ChannelBottomSheetState extends State { color: _streamChatThemeData.colorTheme.accentError, ), ), - title: 'Delete Conversation', + title: context.translations.deleteConversationLabel, titleColor: _streamChatThemeData.colorTheme.accentError, onTap: () async { setState(() { @@ -198,7 +199,7 @@ class _ChannelBottomSheetState extends State { color: _streamChatThemeData.colorTheme.textLowEmphasis, ), ), - 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.accentError, ), @@ -237,17 +238,17 @@ 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.accentError, ), ); if (res == true) { final channel = _streamChannelState.channel; - final user = _streamChatState.user; + final user = _streamChatState.currentUser; if (user != null) { await channel.removeMembers([user.id]); } diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 8f586516..5f6275a8 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 3be26ec8..67a59782 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) @@ -67,7 +71,7 @@ class ChannelInfo extends StatelessWidget { .subtitle, ); } else { - final userId = StreamChat.of(context).user?.id; + final userId = StreamChat.of(context).currentUser?.id; final otherMember = members?.firstWhereOrNull( (element) => element.userId != userId, ); @@ -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.searchingForNetworkText, 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.accentPrimary, ), 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 0db31f80..835f51a2 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( @@ -95,7 +96,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { final _client = client ?? StreamChat.of(context).client; - final user = _client.state.user; + final user = _client.state.currentUser; return ConnectionStatusBuilder( statusBuilder: (context, status) { var statusString = ''; @@ -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.textHighEmphasis, ), @@ -226,7 +226,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { ), const SizedBox(width: 10), Text( - 'Searching for Network', + context.translations.searchingForNetworkText, 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 83f196df..42c7420b 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?); @@ -231,10 +232,21 @@ class _ChannelListViewState extends State { ); } - return LazyLoadScrollView( + child = LazyLoadScrollView( onEndOfPage: () => _channelListController.paginateData!(), child: child, ); + + final backgroundColor = ChannelListViewTheme.of(context).backgroundColor; + + if (backgroundColor != null) { + return ColoredBox( + color: backgroundColor, + child: child, + ); + } + + return child; } Widget _buildListView(BuildContext context, List channels) { @@ -290,7 +302,7 @@ class _ChannelListViewState extends State { Padding( padding: const EdgeInsets.all(8), child: Text( - 'LetтАЩs start chatting!', + context.translations.letsStartChattingLabel, style: chatThemeData.textTheme.headline, ), ), @@ -300,7 +312,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.textLowEmphasis, @@ -319,7 +331,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.accentPrimary, ), @@ -455,9 +467,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, @@ -465,14 +477,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), ), ], ), @@ -541,7 +553,7 @@ class _ChannelListViewState extends State { 'owner', ].contains(channel.state!.members .firstWhereOrNull( - (m) => m.userId == channel.client.state.user?.id) + (m) => m.userId == channel.client.state.currentUser?.id) ?.role)) IconSlideAction( color: backgroundColor, @@ -550,17 +562,16 @@ class _ChannelListViewState extends State { ), onTap: widget.onDeletePressed != null ? () { - widget.onDeletePressed!(channel); + widget.onDeletePressed?.call(channel); } : () async { final res = await showConfirmationDialog( context, - title: 'Delete Conversation', - okText: 'DELETE', + title: context.translations.deleteConversationLabel, question: - // ignore: lines_longer_than_80_chars - 'Are you sure you want to delete this conversation?', - cancelText: 'CANCEL', + context.translations.deleteConversationQuestion, + okText: context.translations.deleteLabel, + cancelText: context.translations.cancelLabel, icon: StreamSvgIcon.delete( color: chatThemeData.colorTheme.accentError, ), @@ -571,18 +582,13 @@ class _ChannelListViewState extends State { }, ), ], - child: DecoratedBox( - decoration: BoxDecoration( - color: chatThemeData.colorTheme.appBg, - ), - child: widget.channelPreviewBuilder?.call(context, channel) ?? - ChannelPreview( - onLongPress: widget.onChannelLongPress, - channel: channel, - onImageTap: () => widget.onImageTap?.call(channel), - onTap: (channel) => onTap(channel, widget.channelWidget), - ), - ), + child: widget.channelPreviewBuilder?.call(context, channel) ?? + ChannelPreview( + onLongPress: widget.onChannelLongPress, + channel: channel, + onImageTap: () => widget.onImageTap?.call(channel), + onTap: (channel) => onTap(channel, widget.channelWidget), + ), ), ); } @@ -662,7 +668,7 @@ class _ChannelListViewState extends State { child: Padding( padding: const EdgeInsets.all(16), child: Text( - 'Error loading channels', + context.translations.loadingChannelsError, style: theme.textTheme.body.copyWith( color: Colors.white, ), diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index 4819ecc7..4ae4f935 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,10 +45,12 @@ class ChannelName extends StatelessWidget { ) => LayoutBuilder( builder: (context, constraints) { - var title = 'No title'; - if (extraData['name'] == null) { - final otherMembers = - members?.where((member) => member.userId != client.user!.id); + var title = context.translations.noTitleText; + if (extraData['name'] != null) { + title = extraData['name']; + } else { + final otherMembers = members + ?.where((member) => member.userId != client.currentUser!.id); if (otherMembers?.length == 1) { if (otherMembers!.first.user != null) { title = otherMembers.first.user!.name; @@ -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 bf615248..7fc59c7f 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) @@ -101,7 +102,7 @@ class ChannelPreview extends StatelessWidget { if (members?.isEmpty == true || members?.any((Member e) => e.user!.id == - channel.client.state.user?.id) != + channel.client.state.currentUser?.id) != true) { return const SizedBox(); } @@ -124,7 +125,7 @@ class ChannelPreview extends StatelessWidget { (m) => !m.isDeleted && m.shadowed != true, ); if (lastMessage?.user?.id == - streamChatState.user?.id) { + streamChatState.currentUser?.id) { return Padding( padding: const EdgeInsets.only(right: 4), child: SendingIndicator( @@ -133,7 +134,8 @@ class ChannelPreview extends StatelessWidget { isMessageRead: channel.state!.read ?.where((element) => element.user.id != - channel.client.state.user!.id) + channel + .client.state.currentUser!.id) .where((element) => element.lastRead .isAfter(lastMessage.createdAt)) .isNotEmpty == @@ -172,7 +174,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 { @@ -197,7 +199,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 8fff2ade..3d85299c 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 011b0c72..0a6c337a 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 5161c8e8..b975941e 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(); @@ -9,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. /// @@ -103,6 +105,11 @@ extension BuildContextX on BuildContext { // ignore: public_member_api_docs double get textScaleFactor => MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0; + + /// Retrieves current translations according to locale + /// Defaults to [DefaultTranslations] + 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 0f6101b9..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,12 +4,12 @@ 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'; 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 +182,10 @@ class _FullScreenMediaState extends State children: [ GalleryHeader( 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')}', + sentAt: context.translations.sentAtText( + date: widget.message.createdAt, + time: widget.message.createdAt, + ), onBackPressed: () { Navigator.of(context).pop(); }, @@ -197,7 +198,7 @@ class _FullScreenMediaState extends State ); }, ), - if (widget.message.type != 'ephemeral') + if (!widget.message.isEphemeral) GalleryFooter( currentPage: _currentPage, totalPages: widget.mediaAttachments.length, @@ -222,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/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index c26c8dbc..03d575ae 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_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 GalleryFooter extends StatefulWidget implements PreferredSizeWidget { @@ -135,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, ), ], @@ -191,7 +194,7 @@ class _GalleryFooterState extends State { child: Padding( padding: const EdgeInsets.all(16), child: Text( - 'Photos', + context.translations.photosLabel, style: galleryFooterThemeData.bottomSheetPhotosTextStyle, ), 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/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart new file mode 100644 index 00000000..3a6aba10 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart @@ -0,0 +1,258 @@ +import 'dart:math'; +import 'dart:ui'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +/// Fallback user avatar with a polygon gradient overlayed with text +class GradientAvatar extends StatefulWidget { + /// 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) => Center( + child: RepaintBoundary( + child: CustomPaint( + painter: DemoPainter( + widget.userId, + getShortenedName(widget.name), + DefaultTextStyle.of(context).style.fontFamily ?? 'Roboto', + ), + child: const SizedBox.expand(), + ), + ), + ); + + String getShortenedName(String name) { + var parts = name.split(' ')..removeWhere((e) => e == ''); + + if (parts.length > 2) { + parts = parts.take(2).toList(); + } + + var result = ''; + + for (var i = 0; i < parts.length; i++) { + result = result + parts[i][0].toUpperCase(); + } + + return result; + } +} + +/// Painter for bg polygon gradient +class DemoPainter extends CustomPainter { + /// Constructor for [DemoPainter] + DemoPainter( + this.userId, + this.username, + this.fontFamily, + ); + + /// 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; + + /// User name to display + String username; + + /// Font family to use + String fontFamily; + + @override + void paint(Canvas canvas, Size size) { + final rowUnit = size.width / columnCount; + final columnUnit = size.height / rowCount; + final rand = Random(userId.length); + + final squares = []; + final points = {}; + final gradient = colorGradients[rand.nextInt(colorGradients.length)]; + + 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]); + + 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)); + } + } + + 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.9) * textSize / 2; + final dyShift = (username.length == 2 ? 1.0 : 1.65) * textSize / 2; + + final fontSize = username.length == 2 ? textSize : textSize * 1.5; + + TextPainter( + text: TextSpan( + text: username, + style: TextStyle( + fontFamily: fontFamily, + 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 + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; + + /// Transforms initial grid into a polygon grid + List transformPoints(Set points, Size size) { + final transformedList = []; + final orgList = points.toList(); + final rand = Random(userId.length); + + for (var i = 0; i < points.length; i++) { + final orgDx = orgList[i].dx; + final orgDy = orgList[i].dy; + + if (orgDx == 0 || + orgDy == 0 || + orgDx == size.width || + orgDy == size.height) { + transformedList.add(Offset(orgDx, orgDy)); + continue; + } + + final sign1 = rand.nextInt(2) == 1 ? 1 : -1; + final sign2 = rand.nextInt(2) == 1 ? 1 : -1; + + 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)); + } + + return transformedList; + } +} + +/// 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; + + /// Draw the polygon on canvas + void draw(Canvas canvas, List points) { + final 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); + } +} + +/// Gradient list for polygons +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/localization/stream_chat_localizations.dart b/packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart new file mode 100644 index 00000000..e9f8cf4b --- /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 stream chat 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..d6e5add0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -0,0 +1,667 @@ +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat_flutter/src/connection_status_builder.dart'; +import 'package:stream_chat_flutter/src/message_input.dart'; +import 'package:stream_chat_flutter/src/message_list_view.dart'; +import 'package:stream_chat_flutter/src/message_search_list_view.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart' + show User; + +/// Translation strings for the stream chat widgets +abstract class Translations { + /// The error shown when [launchURL] fails + String get launchUrlError; + + /// The error shown when loading users fails + String get loadingUsersError; + + /// The label for "retry" button + String get retryLabel; + + /// The label for showing no users + String get noUsersLabel; + + /// The text for showing user is online + String get userOnlineText; + + /// The text for showing the last online of the user + String get userLastOnlineText; + + /// The text shown when [users] starts typing + String userTypingText(Iterable users); + + /// The label for "thread reply" + String get threadReplyLabel; + + /// The text for showing if the message is only visible to you + String get onlyVisibleToYouText; + + /// The text for showing the thread reply count + String threadReplyCountText(int count); + + /// The text for showing the attachments upload progress + String attachmentsUploadProgressText({ + required int remaining, + required int total, + }); + + /// The text for showing who pinned the message + String pinnedByUserText({ + required User pinnedBy, + required User currentUser, + }); + + /// The text for showing there are empty messages + String get emptyMessagesText; + + /// The text for showing generic error + String get genericErrorText; + + /// The error shown when loading messages fails + String get loadingMessagesError; + + /// The text for showing the result count in [MessageSearchListView] + String resultCountText(int count); + + /// The text for showing the message is deleted + String get messageDeletedText; + + /// The label for message deleted + String get messageDeletedLabel; + + /// The label for message reactions + String get messageReactionsLabel; + + /// The text for showing there are no chats + String get emptyChatMessagesText; + + /// The text for showing the thread separator in case [MessageListView] + /// contains a parent message + String threadSeparatorText(int replyCount); + + /// The label for "connected" in [ConnectionStatusBuilder] + String get connectedLabel; + + /// The label for "disconnected" in [ConnectionStatusBuilder] + String get disconnectedLabel; + + /// The label for "reconnecting" in [ConnectionStatusBuilder] + String get reconnectingLabel; + + /// The label for also send as direct message "checkbox"" in [MessageInput] + String get alsoSendAsDirectMessageLabel; + + /// The label for search Gif + String get searchGifLabel; + + /// The label for add a comment or send in case of + /// attachments inside [MessageInput] + String get addACommentOrSendLabel; + + /// The label for write a message in [MessageInput] + String get writeAMessageLabel; + + /// The label for instant commands in [MessageInput] + String get instantCommandsLabel; + + /// The error shown in case the fi"le is too large even after compression + /// while uploading via [MessageInput] + String fileTooLargeAfterCompressionError(double limitInMB); + + /// The error shown in case the file is too large + /// while uploading via [MessageInput] + String fileTooLargeError(double limitInMB); + + /// The text for showing the query while searching for emojis + String emojiMatchingQueryText(String query); + + /// The label for "add a file" + String get addAFileLabel; + + /// The label for "upload a photo" + String get uploadAPhotoLabel; + + /// The label for "upload a video" + String get uploadAVideoLabel; + + /// The label for "photo from camera" + String get photoFromCameraLabel; + + /// The label for "video from camera" + String get videoFromCameraLabel; + + /// The label for "upload a file" + String get uploadAFileLabel; + + /// The error shown when something went wrong + String get somethingWentWrongError; + + /// The label for "OK" + String get okLabel; + + /// The label for "add more files" + String get addMoreFilesLabel; + + /// The message shown for asking photo and video access permission + String get enablePhotoAndVideoAccessMessage; + + /// The message shown for asking gallery access permission + String get allowGalleryAccessMessage; + + /// The label for "flag message" + String get flagMessageLabel; + + /// The question asked while showing flag message dialog + String get flagMessageQuestion; + + /// The label for "Flag" + String get flagLabel; + + /// The label for "Cancel" + String get cancelLabel; + + /// The label for successful message flag + String get flagMessageSuccessfulLabel; + + /// The text for showing the message if successfully flagged + String get flagMessageSuccessfulText; + + /// The label for "delete message" + String get deleteMessageLabel; + + /// The question asked while showing delete message dialog + String get deleteMessageQuestion; + + /// The label for "Delete" + String get deleteLabel; + + /// The text for showing the operation could not be completed + String get operationCouldNotBeCompletedText; + + /// The label for "Reply" + String get replyLabel; + + /// The text for showing pin/un-pin functionality in [MessageWidget] + /// based on [pinned] + String togglePinUnpinText({required bool pinned}); + + /// The text for showing delete/retry-delete based on [isDeleteFailed] + String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}); + + /// The label for "copy message" + String get copyMessageLabel; + + /// The label for "edit message" + String get editMessageLabel; + + /// The text for showing resend/resend-edited message + /// based on [isUpdateFailed] + String toggleResendOrResendEditedMessage({required bool isUpdateFailed}); + + /// The label for "Photos" + String get photosLabel; + + /// The text for showing on which [date] and [time] the message was sent + String sentAtText({required DateTime date, required DateTime time}); + + /// The label for "Today" + String get todayLabel; + + /// The label for "Yesterday" + String get yesterdayLabel; + + /// The text for showing the channel is muted + String get channelIsMutedText; + + /// The text for showing there is no title + String get noTitleText; + + /// The label for "let's start chatting" + String get letsStartChattingLabel; + + /// The label for sending the first message + String get sendingFirstMessageLabel; + + /// The label for "start a chat" + String get startAChatLabel; + + /// The error shown when loading channel fails + String get loadingChannelsError; + + /// The label for "Delete conversation" + String get deleteConversationLabel; + + /// The question asked while showing delete conversation dialog + String get deleteConversationQuestion; + + /// The label for "Stream Chat" + String get streamChatLabel; + + /// The text for showing searching for network + String get searchingForNetworkText; + + /// The label for "Offline" + String get offlineLabel; + + /// The label for "Try again" + String get tryAgainLabel; + + /// The text for showing the members count based on [count] + String membersCountText(int count); + + /// The text for showing the watchers count based on [count] + String watchersCountText(int count); + + /// The label for "View Info" + String get viewInfoLabel; + + /// The label for "Leave Group" + String get leaveGroupLabel; + + /// The label for "Leave" + String get leaveLabel; + + /// The label for "Leave conversation" + String get leaveConversationLabel; + + /// The question asked while showing leave conversation dialog + String get leaveConversationQuestion; + + /// The label for "Show in chat" + String get showInChatLabel; + + /// The label for "Save Image" + String get saveImageLabel; + + /// The label for "Save Video" + String get saveVideoLabel; + + /// The label for "Upload Error" + String get uploadErrorLabel; + + /// The label for "Giphy" + String get giphyLabel; + + /// The label for "Shuffle" + String get shuffleLabel; + + /// The label for "Send" + String get sendLabel; + + /// The label for "With" + String get withText; + + /// The text shown for "In" + String get inText; + + /// The text shown for "You" + String get youText; + + /// The text shown for "Of" + String get ofText; + + /// The text shown for "File" + String get fileText; + + /// The label for "Reply to message" + String get replyToMessageLabel; +} + +/// Default implementation of Translation strings for the stream chat widgets +class DefaultTranslations implements Translations { + const DefaultTranslations._(); + + /// Singleton instance of [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 is deleted.'; + + @override + String get messageDeletedLabel => 'Message deleted'; + + @override + String get messageReactionsLabel => '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 fileTooLargeAfterCompressionError(double limitInMB) => + 'The file is too large to upload. ' + 'The file size limit is $limitInMB MB. ' + 'We tried compressing it, but it was not enough.'; + + @override + String fileTooLargeError(double limitInMB) => + 'The file is too large to upload. The file size limit is $limitInMB MB.'; + + @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 somethingWentWrongError => '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'; + + String _getDay(DateTime dateTime) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = DateTime(now.year, now.month, now.day - 1); + + final date = DateTime(dateTime.year, dateTime.month, dateTime.day); + + if (date == today) { + return 'today'; + } else if (date == yesterday) { + return 'yesterday'; + } else { + return 'on ${Jiffy(date).MMMd}'; + } + } + + @override + String sentAtText({required DateTime date, required DateTime time}) => + 'Sent ${_getDay(date)} at ${Jiffy(time.toLocal()).format('HH:mm')}'; + + @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 searchingForNetworkText => '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'; + + @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'; + + @override + String get replyToMessageLabel => 'Reply to Message'; +} 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 3e653187..d392c5c5 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -101,7 +101,7 @@ class _MessageActionsModalState extends State { Widget _showMessageOptionsModal() { final mediaQueryData = MediaQuery.of(context); final size = mediaQueryData.size; - final user = StreamChat.of(context).user; + final user = StreamChat.of(context).currentUser; final roughMaxSize = 2 * size.width / 3; var messageTextLength = widget.message.text!.length; @@ -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.accentError, 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.flagLabel, + cancelText: context.translations.cancelLabel, ); final theme = streamChatThemeData; @@ -290,9 +288,9 @@ class _MessageActionsModalState extends State { color: theme.colorTheme.accentError, 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.accentError, 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.accentError, 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.accentError, size: 24, ), - details: 'The operation couldn\'t be completed.', - title: 'Something went wrong', - okText: 'OK', + details: context.translations.operationCouldNotBeCompletedText, + title: context.translations.somethingWentWrongError, + 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,9 +592,9 @@ class _MessageActionsModalState extends State { color: streamChatThemeData.colorTheme.disabled, ), ), - const Text( - 'Edit Message', - style: TextStyle(fontWeight: FontWeight.bold), + Text( + context.translations.editMessageLabel, + style: const TextStyle(fontWeight: FontWeight.bold), ), IconButton( visualDensity: VisualDensity.compact, @@ -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 5a4f6822..feb5bd6d 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -357,9 +357,9 @@ class MessageInputState extends State { color: _streamChatTheme.colorTheme.disabled, ), ), - const Text( - 'Reply to Message', - style: TextStyle(fontWeight: FontWeight.bold), + Text( + context.translations.replyToMessageLabel, + style: const TextStyle(fontWeight: FontWeight.bold), ), IconButton( visualDensity: VisualDensity.compact, @@ -461,7 +461,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.textHighEmphasis .withOpacity(0.5), @@ -586,7 +586,7 @@ class MessageInputState extends State { style: _streamChatTheme.messageInputTheme.inputTextStyle, autofocus: widget.autofocus, textAlignVertical: TextAlignVertical.center, - decoration: _getInputDecoration(), + decoration: _getInputDecoration(context), textCapitalization: TextCapitalization.sentences, ), ) @@ -598,11 +598,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.textLowEmphasis, ), @@ -751,14 +751,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) { @@ -881,7 +881,7 @@ class MessageInputState extends State { ), ), Text( - 'Instant Commands', + context.translations.instantCommandsLabel, style: TextStyle( color: _streamChatTheme.colorTheme.textHighEmphasis .withOpacity(.5), @@ -1144,8 +1144,9 @@ 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( + widget.maxAttachmentSize / (1024 * 1024), + ), ); return; } @@ -1156,9 +1157,9 @@ 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( + widget.maxAttachmentSize / (1024 * 1024), + )); return; } } @@ -1426,7 +1427,9 @@ class MessageInputState extends State { ), Flexible( child: Text( - 'Emoji matching "$query"', + context.translations.emojiMatchingQueryText( + query, + ), style: TextStyle( color: _streamChatTheme .colorTheme.textHighEmphasis @@ -1776,17 +1779,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); @@ -1794,7 +1797,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); @@ -1803,7 +1806,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); @@ -1812,7 +1815,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); @@ -1820,7 +1823,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); @@ -1923,8 +1926,9 @@ 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( + widget.maxAttachmentSize / (1024 * 1024), + ), ); return; } @@ -1935,9 +1939,9 @@ 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( + widget.maxAttachmentSize / (1024 * 1024), + )); return; } } @@ -2117,7 +2121,7 @@ class MessageInputState extends State { height: 26, ), Text( - 'Something went wrong', + context.translations.somethingWentWrongError, style: _streamChatTheme.textTheme.headlineBold, ), const SizedBox( @@ -2146,7 +2150,7 @@ class MessageInputState extends State { Navigator.of(context).pop(); }, child: Text( - 'OK', + context.translations.okLabel, style: _streamChatTheme.textTheme.bodyBold.copyWith( color: _streamChatTheme.colorTheme.accentPrimary), ), @@ -2257,10 +2261,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 @@ -2292,7 +2296,7 @@ class __PickerWidgetState extends State<_PickerWidget> { color: widget.streamChatTheme.colorTheme.inputBg, alignment: Alignment.center, child: Text( - 'Add more files', + context.translations.addMoreFilesLabel, style: TextStyle( color: widget.streamChatTheme.colorTheme.accentPrimary, fontWeight: FontWeight.bold, @@ -2324,8 +2328,7 @@ class __PickerWidgetState extends State<_PickerWidget> { color: widget.streamChatTheme.colorTheme.disabled, ), 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.textLowEmphasis), @@ -2334,7 +2337,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.accentPrimary, ), 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..5ae6f9ea 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,23 @@ class MessageListView extends StatefulWidget { this.showFloatingDateDivider = true, this.threadSeparatorBuilder, this.messageListController, + this.reverse = true, + this.paginationLimit = 20, }) : 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; + + /// Limit used during pagination + final int paginationLimit; + /// Function used to build a custom system message widget final SystemMessageBuilder? systemMessageBuilder; @@ -323,11 +335,13 @@ class _MessageListViewState extends State { bool _inBetweenList = false; late final _defaultController = MessageListController(); + MessageListController get _messageListController => widget.messageListController ?? _defaultController; @override Widget build(BuildContext context) => MessageListCore( + paginationLimit: widget.paginationLimit, messageFilter: widget.messageFilter, loadingBuilder: widget.loadingBuilder ?? (context) => const Center( @@ -336,7 +350,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.textHighEmphasis .withOpacity(.5)), @@ -349,7 +363,7 @@ class _MessageListViewState extends State { errorBuilder: widget.errorBuilder ?? (BuildContext context, Object error) => Center( child: Text( - 'Something went wrong', + context.translations.genericErrorText, style: _streamTheme.textTheme.footnote.copyWith( color: _streamTheme.colorTheme.textHighEmphasis .withOpacity(.5)), @@ -386,7 +400,7 @@ class _MessageListViewState extends State { 1 // parent message ; - return Stack( + final child = Stack( alignment: Alignment.center, children: [ ConnectionStatusBuilder( @@ -395,14 +409,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; } @@ -445,7 +459,7 @@ class _MessageListViewState extends State { initialAlignment: initialAlignment ?? 0, physics: widget.scrollPhysics, itemScrollController: _scrollController, - reverse: true, + reverse: widget.reverse, addAutomaticKeepAlives: false, itemCount: itemCount, @@ -528,7 +542,9 @@ class _MessageListViewState extends State { }, itemBuilder: (context, i) { if (i == itemCount - 1) { - if (widget.parentMessage == null) return const Offstage(); + if (widget.parentMessage == null) { + return const Offstage(); + } return buildParentMessage(widget.parentMessage!); } @@ -584,6 +600,17 @@ class _MessageListViewState extends State { _buildFloatingDateDivider(itemCount), ], ); + + final backgroundColor = MessageListViewTheme.of(context).backgroundColor; + + if (backgroundColor != null) { + return ColoredBox( + color: backgroundColor, + child: child, + ); + } + + return child; } Widget _buildThreadSeparator() { @@ -591,7 +618,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, @@ -599,7 +626,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, ), @@ -608,7 +635,10 @@ class _MessageListViewState extends State { } Positioned _buildFloatingDateDivider(int itemCount) => Positioned( - top: 20, + top: widget.reverse ? 20 : null, + bottom: widget.reverse ? null : 20, + left: 0, + right: 0, child: BetterStreamBuilder>( initialData: _itemPositionListener.itemPositions.value, stream: _itemPositionStream, @@ -640,7 +670,9 @@ class _MessageListViewState extends State { ); Future _paginateData( - StreamChannelState? channel, QueryDirection direction) => + StreamChannelState? channel, + QueryDirection direction, + ) => _messageListController.paginateData!(direction: direction); int? _getTopElementIndex(Iterable values) { @@ -672,7 +704,8 @@ class _MessageListViewState extends State { final unreadCount = snapshot.data!.item2; final showUnreadCount = unreadCount > 0 && streamChannel!.channel.state!.members.any((e) => - e.userId == streamChannel!.channel.client.state.user!.id); + e.userId == + streamChannel!.channel.client.state.currentUser!.id); return Positioned( bottom: 8, right: 8, @@ -700,9 +733,13 @@ class _MessageListViewState extends State { ); } }, - 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( @@ -774,9 +811,10 @@ class _MessageListViewState extends State { Widget buildParentMessage( Message message, ) { - final isMyMessage = message.user!.id == StreamChat.of(context).user!.id; + final isMyMessage = + message.user!.id == StreamChat.of(context).currentUser!.id; final isOnlyEmoji = message.text!.isOnlyEmoji; - final currentUser = StreamChat.of(context).user; + final currentUser = StreamChat.of(context).currentUser; final members = StreamChannel.of(context).channel.state?.members ?? []; final currentUserMember = members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); @@ -861,7 +899,7 @@ class _MessageListViewState extends State { ); } - final userId = StreamChat.of(context).user!.id; + final userId = StreamChat.of(context).currentUser!.id; final isMyMessage = message.user!.id == userId; final nextMessage = index - 1 >= 0 ? messages[index - 1] : null; final isNextUserSame = @@ -924,7 +962,7 @@ class _MessageListViewState extends State { ? BorderSide.none : null; - final currentUser = StreamChat.of(context).user; + final currentUser = StreamChat.of(context).currentUser; final members = StreamChannel.of(context).channel.state?.members ?? []; final currentUserMember = members.firstWhere((e) => e.user!.id == currentUser!.id); @@ -1130,7 +1168,7 @@ class _MessageListViewState extends State { _topPaginationActive = false; } if (event.message!.user!.id == - streamChannel!.channel.client.state.user!.id) { + streamChannel!.channel.client.state.currentUser!.id) { WidgetsBinding.instance!.addPostFrameCallback((_) { _scrollController?.jumpTo( index: 0, @@ -1212,8 +1250,8 @@ class _LoadingIndicator extends StatelessWidget { initialData: false, errorBuilder: (context, error) => Container( color: streamTheme.colorTheme.accentError.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 3d9a907e..b7e37bad 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 { @@ -42,7 +43,7 @@ class MessageReactionsModal extends StatelessWidget { @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; - final user = StreamChat.of(context).user; + final user = StreamChat.of(context).currentUser; final roughMaxSize = 2 * size.width / 3; var messageTextLength = message.text!.length; @@ -154,7 +155,7 @@ class MessageReactionsModal extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - 'Message Reactions', + context.translations.messageReactionsLabel, 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..31c53d5a 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. /// @@ -49,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).currentUser?.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, ), @@ -98,7 +101,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 f364d0f2..1eae16cd 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,62 +141,77 @@ class MessageSearchListView extends StatefulWidget { class _MessageSearchListViewState extends State { late final _defaultController = MessageSearchListController(); + MessageSearchListController get _messageSearchListController => widget.messageSearchListController ?? _defaultController; @override - Widget build(BuildContext context) => MessageSearchListCore( - filters: widget.filters, - sortOptions: widget.sortOptions, - messageQuery: widget.messageQuery, - paginationParams: widget.paginationParams, - messageFilters: widget.messageFilters, - messageSearchListController: _messageSearchListController, - emptyBuilder: widget.emptyBuilder ?? - (context) => LayoutBuilder( - builder: (context, viewportConstraints) => - SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: const Center( - child: Text('There are no messages currently'), - ), + Widget build(BuildContext context) { + final messageSearchListCore = MessageSearchListCore( + filters: widget.filters, + sortOptions: widget.sortOptions, + messageQuery: widget.messageQuery, + paginationParams: widget.paginationParams, + messageFilters: widget.messageFilters, + messageSearchListController: _messageSearchListController, + emptyBuilder: widget.emptyBuilder ?? + (context) => LayoutBuilder( + builder: (context, viewportConstraints) => + SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: Center( + child: Text(context.translations.emptyMessagesText), ), ), ), - errorBuilder: widget.errorBuilder ?? - (BuildContext context, dynamic error) { - if (error is Error) { - print(error.stackTrace); - } - return InfoTile( - showMessage: widget.showErrorTile, - tileAnchor: Alignment.topCenter, - childAnchor: Alignment.topCenter, - message: 'An error occurred.', - child: Container(), - ); - }, - loadingBuilder: widget.loadingBuilder ?? - (context) => LayoutBuilder( - builder: (context, viewportConstraints) => - SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: const Center( - child: CircularProgressIndicator(), - ), + ), + errorBuilder: widget.errorBuilder ?? + (BuildContext context, dynamic error) { + if (error is Error) { + print(error.stackTrace); + } + return InfoTile( + showMessage: widget.showErrorTile, + tileAnchor: Alignment.topCenter, + childAnchor: Alignment.topCenter, + message: context.translations.genericErrorText, + child: Container(), + ); + }, + loadingBuilder: widget.loadingBuilder ?? + (context) => LayoutBuilder( + builder: (context, viewportConstraints) => + SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: viewportConstraints.maxHeight, + ), + child: const Center( + child: CircularProgressIndicator(), ), ), ), - childBuilder: widget.childBuilder ?? _buildListView, + ), + childBuilder: widget.childBuilder ?? _buildListView, + ); + + final backgroundColor = + MessageSearchListViewTheme.of(context).backgroundColor; + + if (backgroundColor != null) { + return ColoredBox( + color: backgroundColor, + child: messageSearchListCore, ); + } + + return messageSearchListCore; + } Widget _separatorBuilder(BuildContext context, int index) => Container( height: 1, @@ -226,10 +242,10 @@ class _MessageSearchListViewState extends State { .colorTheme .accentError .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 +308,7 @@ class _MessageSearchListViewState extends State { horizontal: 8, ), child: Text( - '${items.length} results', + context.translations.resultCountText(items.length), style: TextStyle( color: chatThemeData.colorTheme.textLowEmphasis, ), diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index 6d006085..345b0096 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -29,60 +29,66 @@ class MessageText extends StatelessWidget { @override Widget build(BuildContext context) { - final text = _replaceMentions(message.text ?? '').replaceAll('\n', '\n\n'); + final streamChat = StreamChat.of(context); + assert(streamChat.currentUser != null, ''); + return BetterStreamBuilder( + stream: streamChat.currentUserStream.map((it) => it!.language ?? 'en'), + initialData: streamChat.currentUser!.language ?? 'en', + builder: (context, language) { + final translatedText = + message.i18n?['${language}_text'] ?? message.text; + final messageText = + _replaceMentions(translatedText ?? '').replaceAll('\n', '\n\n'); + final themeData = Theme.of(context); + return MarkdownBody( + data: messageText, + onTapLink: ( + String link, + String? href, + String title, + ) { + if (link.startsWith('@')) { + final mentionedUser = message.mentionedUsers.firstWhereOrNull( + (u) => '@${u.name}' == link, + ); - final themeData = Theme.of(context); - return MarkdownBody( - data: text, - onTapLink: ( - String link, - String? href, - String title, - ) { - if (link.startsWith('@')) { - final mentionedUser = message.mentionedUsers.firstWhereOrNull( - (u) => '@${u.name}' == link, - ); - if (mentionedUser == null) { - return; - } + if (mentionedUser == null) return; - if (onMentionTap != null) { - onMentionTap!(mentionedUser); - } else { - print('tap on ${mentionedUser.name}'); - } - } else { - if (onLinkTap != null) { - onLinkTap!(link); - } else { - launchURL(context, link); - } - } - }, - styleSheet: MarkdownStyleSheet.fromTheme( - themeData.copyWith( - textTheme: themeData.textTheme.apply( - bodyColor: messageTheme.messageText?.color, - decoration: messageTheme.messageText?.decoration, - decorationColor: messageTheme.messageText?.decorationColor, - decorationStyle: messageTheme.messageText?.decorationStyle, - fontFamily: messageTheme.messageText?.fontFamily, + onMentionTap?.call(mentionedUser); + } else { + if (onLinkTap != null) { + onLinkTap!(link); + } else { + launchURL(context, link); + } + } + }, + styleSheet: MarkdownStyleSheet.fromTheme( + themeData.copyWith( + textTheme: themeData.textTheme.apply( + bodyColor: messageTheme.messageText?.color, + decoration: messageTheme.messageText?.decoration, + decorationColor: messageTheme.messageText?.decorationColor, + decorationStyle: messageTheme.messageText?.decorationStyle, + fontFamily: messageTheme.messageText?.fontFamily, + ), + ), + ).copyWith( + a: messageTheme.messageLinks, + p: messageTheme.messageText, ), - ), - ).copyWith( - a: messageTheme.messageLinks, - p: messageTheme.messageText, - ), + ); + }, ); } String _replaceMentions(String text) { - message.mentionedUsers.map((u) => u.name).toSet().forEach((userName) { - // ignore: parameter_assignments - text = text.replaceAll( + var messageTextToRender = text; + for (final user in message.mentionedUsers.toSet()) { + final userName = user.name; + messageTextToRender = messageTextToRender.replaceAll( '@$userName', '[@$userName](@${userName.replaceAll(' ', '')})'); - }); - return text; + } + return messageTextToRender; } } diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index c9dfd9d4..6982d0a0 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -92,6 +92,8 @@ class MessageWidget extends StatefulWidget { this.userAvatarBuilder, this.editMessageInputBuilder, this.textBuilder, + this.bottomRowBuilder, + this.deletedBottomRowBuilder, this.onReturnAction, Map? customAttachmentBuilders, this.readList, @@ -275,6 +277,12 @@ class MessageWidget extends StatefulWidget { /// Function called on long press final void Function(BuildContext, Message)? onMessageActions; + /// Widget builder for building a bottom row below the message + final Widget Function(BuildContext, Message)? bottomRowBuilder; + + /// Widget builder for building a bottom row below a deleted message + final Widget Function(BuildContext, Message)? deletedBottomRowBuilder; + /// Widget builder for building user avatar final Widget Function(BuildContext, User)? userAvatarBuilder; @@ -410,6 +418,8 @@ class MessageWidget extends StatefulWidget { Widget Function(BuildContext, Message)? editMessageInputBuilder, Widget Function(BuildContext, Message)? textBuilder, Widget Function(BuildContext, Message)? usernameBuilder, + Widget Function(BuildContext, Message)? bottomRowBuilder, + Widget Function(BuildContext, Message)? deletedBottomRowBuilder, void Function(BuildContext, Message)? onMessageActions, Message? message, MessageTheme? messageTheme, @@ -463,6 +473,9 @@ class MessageWidget extends StatefulWidget { editMessageInputBuilder ?? this.editMessageInputBuilder, textBuilder: textBuilder ?? this.textBuilder, usernameBuilder: usernameBuilder ?? this.usernameBuilder, + bottomRowBuilder: bottomRowBuilder ?? this.bottomRowBuilder, + deletedBottomRowBuilder: + deletedBottomRowBuilder ?? this.deletedBottomRowBuilder, onMessageActions: onMessageActions ?? this.onMessageActions, message: message ?? this.message, messageTheme: messageTheme ?? this.messageTheme, @@ -782,7 +795,11 @@ class _MessageWidgetState extends State bottom: isPinned && widget.showPinHighlight ? 6.0 : 0.0, ), - child: _bottomRow, + child: widget.bottomRowBuilder?.call( + context, + widget.message, + ) ?? + _bottomRow, ), if (isFailedState) Positioned( @@ -810,7 +827,7 @@ class _MessageWidgetState extends State } Widget _buildQuotedMessage() { - final isMyMessage = widget.message.user?.id == _streamChat.user?.id; + final isMyMessage = widget.message.user?.id == _streamChat.currentUser?.id; final onTap = widget.message.quotedMessage?.isDeleted != true && widget.onQuotedMessageTap != null ? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId) @@ -830,22 +847,11 @@ class _MessageWidgetState extends State Widget get _bottomRow { if (isDeleted) { - final chatThemeData = _streamChatTheme; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - StreamSvgIcon.eye( - color: chatThemeData.colorTheme.textLowEmphasis, - size: 16, - ), - const SizedBox(width: 8), - Text( - 'Only visible to you', - style: chatThemeData.textTheme.footnote - .copyWith(color: chatThemeData.colorTheme.textLowEmphasis), - ), - ], - ); + return widget.deletedBottomRowBuilder?.call( + context, + widget.message, + ) ?? + const Offstage(); } final children = []; @@ -854,9 +860,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 @@ -993,7 +999,7 @@ class _MessageWidgetState extends State Widget _buildReactionIndicator( BuildContext context, ) { - final ownId = _streamChat.user!.id; + final ownId = _streamChat.currentUser!.id; final reactionsMap = {}; widget.message.latestReactions?.forEach((element) { if (!reactionsMap.containsKey(element.type) || @@ -1054,10 +1060,10 @@ class _MessageWidgetState extends State showReactionPickerIndicator: widget.showReactions && (widget.message.status == MessageSendingStatus.sent), showPinHighlight: false, - showUserAvatar: - widget.message.user!.id == channel.client.state.user!.id - ? DisplayWidget.gone - : DisplayWidget.show, + showUserAvatar: widget.message.user!.id == + channel.client.state.currentUser!.id + ? DisplayWidget.gone + : DisplayWidget.show, ), onCopyTap: (message) => Clipboard.setData(ClipboardData(text: message.text)), @@ -1118,7 +1124,7 @@ class _MessageWidgetState extends State (widget.message.status == MessageSendingStatus.sent), showPinHighlight: false, showUserAvatar: - widget.message.user!.id == channel.client.state.user!.id + widget.message.user!.id == channel.client.state.currentUser!.id ? DisplayWidget.gone : DisplayWidget.show, ), @@ -1201,7 +1207,10 @@ class _MessageWidgetState extends State ); } return Text( - 'Uploading $uploadRemaining/$totalAttachments ...', + context.translations.attachmentsUploadProgressText( + remaining: uploadRemaining, + total: totalAttachments, + ), style: style, ); } @@ -1275,8 +1284,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.currentUser!; return Padding( padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8), @@ -1290,7 +1299,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.textLowEmphasis, fontSize: 13, diff --git a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart index 9e279344..ecc2b27a 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart @@ -123,7 +123,7 @@ class ReactionBubble extends StatelessWidget { ); final chatThemeData = StreamChatTheme.of(context); - final userId = StreamChat.of(context).user?.id; + final userId = StreamChat.of(context).currentUser?.id; return Padding( padding: const EdgeInsets.symmetric( horizontal: 4, diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index c050d8cb..6eae0fcc 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -102,7 +102,6 @@ class StreamChatState extends State { data: materialTheme.copyWith( primaryIconTheme: streamTheme.primaryIconTheme, accentColor: streamTheme.colorTheme.accentPrimary, - scaffoldBackgroundColor: streamTheme.colorTheme.barsBg, ), child: StreamChatCore( client: client, @@ -127,21 +126,34 @@ class StreamChatState extends State { return defaultTheme.merge(themeData); } + // coverage:ignore-start + /// The current user - User? get user => widget.client.state.user; + @Deprecated('Use `.currentUser` instead, Will be removed in future releases') + User? get user => widget.client.state.currentUser; /// The current user as a stream - Stream get userStream => widget.client.state.userStream; + @Deprecated( + 'Use `.currentUserStream` instead, Will be removed in future releases', + ) + Stream get userStream => widget.client.state.currentUserStream; - @override - void initState() { - super.initState(); - } + // coverage:ignore-end + + /// The current user + User? get currentUser => widget.client.state.currentUser; + + /// The current user as a stream + Stream get currentUserStream => widget.client.state.currentUserStream; @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(); } } 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..9d2df266 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -1,12 +1,11 @@ -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'; 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'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -60,6 +59,10 @@ class StreamChatThemeData { List? reactionIcons, GalleryHeaderThemeData? imageHeaderTheme, GalleryFooterThemeData? imageFooterTheme, + MessageListViewThemeData? messageListViewTheme, + ChannelListViewThemeData? channelListViewTheme, + UserListViewThemeData? userListViewTheme, + MessageSearchListViewThemeData? messageSearchListViewTheme, }) { brightness ??= colorTheme?.brightness ?? Brightness.light; final isDark = brightness == Brightness.dark; @@ -83,6 +86,10 @@ class StreamChatThemeData { reactionIcons: reactionIcons, galleryHeaderTheme: imageHeaderTheme, galleryFooterTheme: imageFooterTheme, + messageListViewTheme: messageListViewTheme, + channelListViewTheme: channelListViewTheme, + userListViewTheme: userListViewTheme, + messageSearchListViewTheme: messageSearchListViewTheme, ); return defaultData.merge(customizedData); @@ -111,6 +118,10 @@ class StreamChatThemeData { required this.reactionIcons, required this.galleryHeaderTheme, required this.galleryFooterTheme, + required this.messageListViewTheme, + required this.channelListViewTheme, + required this.userListViewTheme, + required this.messageSearchListViewTheme, }); /// Create a theme from a Material [Theme] @@ -166,6 +177,18 @@ class StreamChatThemeData { /// Assets used for rendering reactions final List reactionIcons; + /// Theme configuration for the [MessageListView] widget. + final MessageListViewThemeData messageListViewTheme; + + /// Theme configuration for the [ChannelListView] widget. + final ChannelListViewThemeData channelListViewTheme; + + /// Theme configuration for the [UserListView] widget. + final UserListViewThemeData userListViewTheme; + + /// Theme configuration for the [] widget. + final MessageSearchListViewThemeData messageSearchListViewTheme; + /// Creates a copy of [StreamChatThemeData] with specified attributes /// overridden. StreamChatThemeData copyWith({ @@ -182,6 +205,10 @@ class StreamChatThemeData { List? reactionIcons, GalleryHeaderThemeData? galleryHeaderTheme, GalleryFooterThemeData? galleryFooterTheme, + MessageListViewThemeData? messageListViewTheme, + ChannelListViewThemeData? channelListViewTheme, + UserListViewThemeData? userListViewTheme, + MessageSearchListViewThemeData? messageSearchListViewTheme, }) => StreamChatThemeData.raw( channelListHeaderTheme: @@ -199,6 +226,11 @@ class StreamChatThemeData { reactionIcons: reactionIcons ?? this.reactionIcons, galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme, galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme, + messageListViewTheme: messageListViewTheme ?? this.messageListViewTheme, + channelListViewTheme: channelListViewTheme ?? this.channelListViewTheme, + userListViewTheme: userListViewTheme ?? this.userListViewTheme, + messageSearchListViewTheme: + messageSearchListViewTheme ?? this.messageSearchListViewTheme, ); /// Merge themes @@ -219,6 +251,13 @@ class StreamChatThemeData { reactionIcons: other.reactionIcons, galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme), galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme), + messageListViewTheme: + messageListViewTheme.merge(other.messageListViewTheme), + channelListViewTheme: + channelListViewTheme.merge(other.channelListViewTheme), + userListViewTheme: userListViewTheme.merge(other.userListViewTheme), + messageSearchListViewTheme: + messageSearchListViewTheme.merge(other.messageSearchListViewTheme), ); } @@ -270,10 +309,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, @@ -438,6 +476,18 @@ class StreamChatThemeData { bottomSheetPhotosTextStyle: textTheme.headlineBold, bottomSheetCloseIconColor: colorTheme.textHighEmphasis, ), + messageListViewTheme: MessageListViewThemeData( + backgroundColor: colorTheme.barsBg, + ), + channelListViewTheme: ChannelListViewThemeData( + backgroundColor: colorTheme.appBg, + ), + userListViewTheme: UserListViewThemeData( + backgroundColor: colorTheme.appBg, + ), + messageSearchListViewTheme: MessageSearchListViewThemeData( + backgroundColor: colorTheme.appBg, + ), ); } } @@ -1651,7 +1701,7 @@ class GalleryFooterThemeData with Diagnosticable { a.bottomSheetCloseIconColor, b.bottomSheetCloseIconColor, t), ); - /// Merges one [GalleryFooterThemeData] with the another + /// Merges one [GalleryFooterThemeData] with another. GalleryFooterThemeData merge(GalleryFooterThemeData? other) { if (other == null) return this; return copyWith( @@ -1708,3 +1758,436 @@ class GalleryFooterThemeData with Diagnosticable { 'bottomSheetCloseIconColor', bottomSheetCloseIconColor)); } } + +/// Overrides the default style of [MessageListView] descendants. +/// +/// See also: +/// +/// * [MessageListViewThemeData], which is used to configure this theme. +class MessageListViewTheme extends InheritedTheme { + /// Creates a [MessageListViewTheme]. + /// + /// The [data] parameter must not be null. + const MessageListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final MessageListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [MessageListViewTheme] widget, then + /// [StreamChatThemeData.messageListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// MessageListViewTheme theme = MessageListViewTheme.of(context); + /// ``` + static MessageListViewThemeData of(BuildContext context) { + final messageListViewTheme = + context.dependOnInheritedWidgetOfExactType(); + return messageListViewTheme?.data ?? + StreamChatTheme.of(context).messageListViewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + MessageListViewTheme(data: data, child: child); + + @override + bool updateShouldNotify(MessageListViewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [MessageListView]s when +/// used with [MessageListViewTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.messageListViewTheme]. +/// +/// See also: +/// +/// * [MessageListViewTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.messageListViewTheme], which can be used to override +/// the default style for [MessageListView]s below the overall +/// [StreamChatTheme]. +class MessageListViewThemeData with Diagnosticable { + /// Creates a [MessageListViewThemeData]. + const MessageListViewThemeData({ + this.backgroundColor, + }); + + /// The color of the [MessageListView] background. + final Color? backgroundColor; + + /// Copies this [MessageListViewThemeData] to another. + MessageListViewThemeData copyWith({ + Color? backgroundColor, + }) => + MessageListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [MessageListView] themes. + /// + /// All the properties must be non-null. + MessageListViewThemeData lerp( + MessageListViewThemeData a, + MessageListViewThemeData b, + double t, + ) => + MessageListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + ); + + /// Merges one [MessageListViewThemeData] with another. + MessageListViewThemeData merge(MessageListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} + +/// Overrides the default style of [ChannelListView] descendants. +/// +/// See also: +/// +/// * [ChannelListViewThemeData], which is used to configure this theme. +class ChannelListViewTheme extends InheritedTheme { + /// Creates a [ChannelListViewTheme]. + /// + /// The [data] parameter must not be null. + const ChannelListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final ChannelListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [ChannelListViewTheme] widget, then + /// [StreamChatThemeData.channelListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ChannelListViewTheme theme = ChannelListViewTheme.of(context); + /// ``` + static ChannelListViewThemeData of(BuildContext context) { + final channelListViewTheme = + context.dependOnInheritedWidgetOfExactType(); + return channelListViewTheme?.data ?? + StreamChatTheme.of(context).channelListViewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + ChannelListViewTheme(data: data, child: child); + + @override + bool updateShouldNotify(ChannelListViewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [ChannelListView]s when +/// used with [ChannelListViewTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.channelListViewTheme]. +/// +/// See also: +/// +/// * [ChannelListViewTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.channelListViewTheme], which can be used to override +/// the default style for [ChannelListView]s below the overall +/// [StreamChatTheme]. +class ChannelListViewThemeData with Diagnosticable { + /// Creates a [ChannelListViewThemeData]. + const ChannelListViewThemeData({ + this.backgroundColor, + }); + + /// The color of the [ChannelListView] background. + final Color? backgroundColor; + + /// Copies this [ChannelListViewThemeData] to another. + ChannelListViewThemeData copyWith({ + Color? backgroundColor, + }) => + ChannelListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [ChannelListViewThemeData] themes. + /// + /// All the properties must be non-null. + ChannelListViewThemeData lerp( + ChannelListViewThemeData a, + ChannelListViewThemeData b, + double t, + ) => + ChannelListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + ); + + /// Merges one [ChannelListViewThemeData] with another. + ChannelListViewThemeData merge(ChannelListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChannelListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} + +/// Overrides the default style of [UserListView] descendants. +/// +/// See also: +/// +/// * [UserListViewThemeData], which is used to configure this theme. +class UserListViewTheme extends InheritedTheme { + /// Creates a [UserListViewTheme]. + /// + /// The [data] parameter must not be null. + const UserListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final UserListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [UserListViewTheme] widget, then + /// [StreamChatThemeData.userListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// UserListViewTheme theme = UserListViewTheme.of(context); + /// ``` + static UserListViewThemeData of(BuildContext context) { + final userListViewTheme = + context.dependOnInheritedWidgetOfExactType(); + return userListViewTheme?.data ?? + StreamChatTheme.of(context).userListViewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + UserListViewTheme(data: data, child: child); + + @override + bool updateShouldNotify(UserListViewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [UserListView]s when +/// used with [UserListViewTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.userListViewTheme]. +/// +/// See also: +/// +/// * [UserListViewTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.userListViewTheme], which can be used to override +/// the default style for [UserListView]s below the overall +/// [StreamChatTheme]. +class UserListViewThemeData with Diagnosticable { + /// Creates a [UserListViewThemeData]. + const UserListViewThemeData({ + this.backgroundColor, + }); + + /// The color of the [ChannelListView] background. + final Color? backgroundColor; + + /// Copies this [ChannelListViewThemeData] to another. + UserListViewThemeData copyWith({ + Color? backgroundColor, + }) => + UserListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [UserListViewThemeData] themes. + /// + /// All the properties must be non-null. + UserListViewThemeData lerp( + UserListViewThemeData a, + UserListViewThemeData b, + double t, + ) => + UserListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + ); + + /// Merges one [UserListViewThemeData] with another. + UserListViewThemeData merge(UserListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} + +/// Overrides the default style of [MessageSearchListView] descendants. +/// +/// See also: +/// +/// * [UserListViewThemeData], which is used to configure this theme. +class MessageSearchListViewTheme extends InheritedTheme { + /// Creates a [UserListViewTheme]. + /// + /// The [data] parameter must not be null. + const MessageSearchListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final MessageSearchListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [MessageSearchListView] widget, then + /// [StreamChatThemeData.messageSearchListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// MessageSearchListViewTheme theme = MessageSearchListViewTheme.of(context); + /// ``` + static MessageSearchListViewThemeData of(BuildContext context) { + final messageSearchListViewTheme = context + .dependOnInheritedWidgetOfExactType(); + return messageSearchListViewTheme?.data ?? + StreamChatTheme.of(context).messageSearchListViewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + MessageSearchListViewTheme(data: data, child: child); + + @override + bool updateShouldNotify(MessageSearchListViewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [MessageSearchListView]s +/// when used with [MessageSearchListView] or with the overall +/// [StreamChatTheme]'s [StreamChatThemeData.messageSearchListViewTheme]. +/// +/// See also: +/// +/// * [MessageSearchListViewTheme], the theme which is configured with this +/// class. +/// * [StreamChatThemeData.messageSearchListViewTheme], which can be used to +/// override the default style for [UserListView]s below the overall +/// [StreamChatTheme]. +class MessageSearchListViewThemeData with Diagnosticable { + /// Creates a [MessageSearchListViewThemeData]. + const MessageSearchListViewThemeData({ + this.backgroundColor, + }); + + /// The color of the [MessageSearchListView] background. + final Color? backgroundColor; + + /// Copies this [MessageSearchListViewThemeData] to another. + MessageSearchListViewThemeData copyWith({ + Color? backgroundColor, + }) => + MessageSearchListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [UserListViewThemeData] themes. + /// + /// All the properties must be non-null. + MessageSearchListViewThemeData lerp( + MessageSearchListViewThemeData a, + MessageSearchListViewThemeData b, + double t, + ) => + MessageSearchListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + ); + + /// Merges one [MessageSearchListViewThemeData] with another. + MessageSearchListViewThemeData merge(MessageSearchListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageSearchListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} 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/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 bfde9a4c..a5eb62db 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) @@ -111,7 +112,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'with ', + '${context.translations.withText} ', style: chatThemeData.channelTheme.channelHeaderTheme.subtitle, ), Flexible( @@ -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 a13b544e..a55693aa 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.textHighEmphasis.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 923e0334..9481e831 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,12 +156,13 @@ class _UserListViewState extends State bool get _isListView => widget.crossAxisCount == 1; late final _defaultController = UserListController(); + UserListController get _userListController => widget.userListController ?? _defaultController; @override Widget build(BuildContext context) { - final child = UserListCore( + final userListCore = UserListCore( errorBuilder: widget.errorBuilder ?? (BuildContext context, Object err) => _buildError(err), emptyBuilder: widget.emptyBuilder ?? (context) => _buildEmpty(), @@ -189,6 +191,19 @@ class _UserListViewState extends State userListController: _userListController, ); + final backgroundColor = UserListViewTheme.of(context).backgroundColor; + + Widget child; + + if (backgroundColor != null) { + child = ColoredBox( + color: backgroundColor, + child: userListCore, + ); + } else { + child = userListCore; + } + if (!widget.pullToRefresh) { return child; } else { @@ -207,9 +222,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 +232,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 +252,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 +402,10 @@ class _UserListViewState extends State .colorTheme .accentError .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 aee46800..314a788a 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/src/visible_footnote.dart b/packages/stream_chat_flutter/lib/src/visible_footnote.dart new file mode 100644 index 00000000..8d16ce41 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/visible_footnote.dart @@ -0,0 +1,30 @@ +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/src/extension.dart'; + +/// Widget for displaying a footnote +class VisibleFootnote extends StatelessWidget { + /// Constructor for creating a [VisibleFootnote] + const VisibleFootnote({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + StreamSvgIcon.eye( + color: chatThemeData.colorTheme.textLowEmphasis, + size: 16, + ), + const SizedBox(width: 8), + Text( + context.translations.onlyVisibleToYouText, + style: chatThemeData.textTheme.footnote + .copyWith(color: chatThemeData.colorTheme.textLowEmphasis), + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 39aabb85..373a61d2 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'; @@ -14,7 +15,10 @@ export 'src/deleted_message.dart'; export 'src/full_screen_media.dart'; export 'src/gallery_footer.dart'; export 'src/gallery_header.dart'; +export 'src/gradient_avatar.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'; @@ -41,3 +45,4 @@ export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_list_view.dart'; export 'src/utils.dart'; +export 'src/visible_footnote.dart'; 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..60e6888f --- /dev/null +++ b/packages/stream_chat_flutter/lib/svgs/Icon_up.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 590ed055..f651573f 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,12 +1,13 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 2.0.0 +version: 2.1.0 repository: https://github.com/GetStream/stream-chat-flutter 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 @@ -30,13 +31,13 @@ dependencies: lottie: ^1.0.1 meta: ^1.3.0 path_provider: ^2.0.1 - photo_manager: ^1.1.6 - photo_view: ^0.11.1 + photo_manager: ^1.2.6+1 + photo_view: ^0.12.0 rxdart: ^0.27.0 scrollable_positioned_list: ^0.2.0-nullsafety.0 share_plus: ^2.0.3 shimmer: ^2.0.0 - stream_chat_flutter_core: ^2.0.0 + stream_chat_flutter_core: ^2.1.0 substring_highlight: ^1.0.26 synchronized: ^3.0.0 url_launcher: ^6.0.3 @@ -59,4 +60,5 @@ dev_dependencies: golden_toolkit: ^0.9.0 mocktail: ^0.1.2 pedantic: ^1.11.0 + path: ^1.8.0 diff --git a/packages/stream_chat_flutter/test/flutter_test_config.dart b/packages/stream_chat_flutter/test/flutter_test_config.dart index c09db700..bfda23d0 100644 --- a/packages/stream_chat_flutter/test/flutter_test_config.dart +++ b/packages/stream_chat_flutter/test/flutter_test_config.dart @@ -1,9 +1,35 @@ import 'dart:async'; +import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:golden_toolkit/golden_toolkit.dart'; Future testExecutable(FutureOr Function() testMain) async { await loadAppFonts(); + goldenFileComparator = + CustomGoldenFileComparator(Uri.parse('test/src/goldens')); return testMain(); } + +class CustomGoldenFileComparator extends LocalFileComparator { + CustomGoldenFileComparator(Uri testFile) : super(testFile); + + @override + Future compare(Uint8List imageBytes, Uri golden) async { + final result = await GoldenFileComparator.compareLists( + imageBytes, + await getGoldenBytes(golden), + ); + + if (!result.passed && result.diffPercent > 0.05) { + final error = await generateFailureOutput(result, golden, basedir); + throw FlutterError(error); + } + return true; + } + + @override + Future update(Uri golden, Uint8List imageBytes) => + super.update(golden, imageBytes); +} 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..c8832f6f 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 @@ -35,7 +35,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -77,7 +77,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id2')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id2')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -119,7 +119,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -160,7 +160,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -207,7 +207,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -254,7 +254,7 @@ void main() { when(() => mockChannel.updateMessage(any())) .thenAnswer((_) async => UpdateMessageResponse()); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final message = Message( text: 'test', @@ -308,7 +308,7 @@ void main() { when(() => mockChannel.updateMessage(any())) .thenAnswer((_) async => UpdateMessageResponse()); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final message = Message( text: 'test', @@ -357,7 +357,7 @@ void main() { when(() => mockChannel.deleteMessage(any())) .thenAnswer((_) async => EmptyResponse()); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final message = Message( user: User( @@ -399,7 +399,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final imageDownloader = MockAttachmentDownloader(); @@ -454,7 +454,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final fileDownloader = MockAttachmentDownloader(); diff --git a/packages/stream_chat_flutter/test/src/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel_header_test.dart index 211a7132..f7157871 100644 --- a/packages/stream_chat_flutter/test/src/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_header_test.dart @@ -18,8 +18,9 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(user); - when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUser).thenReturn(user); + when(() => clientState.currentUserStream) + .thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -82,8 +83,9 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(user); - when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUser).thenReturn(user); + when(() => clientState.currentUserStream) + .thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -149,8 +151,9 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(user); - when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUser).thenReturn(user); + when(() => clientState.currentUserStream) + .thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -217,8 +220,9 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(user); - when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUser).thenReturn(user); + when(() => clientState.currentUserStream) + .thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -293,8 +297,9 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(user); - when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUser).thenReturn(user); + when(() => clientState.currentUserStream) + .thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -360,8 +365,9 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(user); - when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUser).thenReturn(user); + when(() => clientState.currentUserStream) + .thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); diff --git a/packages/stream_chat_flutter/test/src/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel_image_test.dart index 7c620774..c9293e79 100644 --- a/packages/stream_chat_flutter/test/src/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_image_test.dart @@ -17,7 +17,7 @@ void main() { final channelState = MockChannelState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ @@ -56,7 +56,7 @@ void main() { final channelState = MockChannelState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ @@ -135,7 +135,7 @@ void main() { final currentUser = OwnUser(id: 'user-id'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(currentUser); + when(() => clientState.currentUser).thenReturn(currentUser); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ @@ -207,7 +207,7 @@ void main() { final channelState = MockChannelState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ diff --git a/packages/stream_chat_flutter/test/src/channel_list_header_test.dart b/packages/stream_chat_flutter/test/src/channel_list_header_test.dart index f2f054d4..a2783067 100644 --- a/packages/stream_chat_flutter/test/src/channel_list_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_list_header_test.dart @@ -13,7 +13,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connected)); @@ -30,7 +30,7 @@ void main() { await tester.pumpAndSettle(); final userAvatar = tester.widget(find.byType(UserAvatar)); - expect(userAvatar.user, clientState.user); + expect(userAvatar.user, clientState.currentUser); expect(find.byType(StreamNeumorphicButton), findsOneWidget); expect(find.text('Stream Chat'), findsOneWidget); }, @@ -43,7 +43,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); @@ -72,7 +72,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); @@ -101,7 +101,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); @@ -139,7 +139,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); @@ -173,7 +173,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => client.wsConnectionStatusStream) .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); diff --git a/packages/stream_chat_flutter/test/src/channel_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/channel_list_view_theme_test.dart new file mode 100644 index 00000000..80a21d25 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/channel_list_view_theme_test.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + test('ChannelListViewThemeData copyWith, ==, hashCode basics', () { + expect(const ChannelListViewThemeData(), + const ChannelListViewThemeData().copyWith()); + }); + + test( + '''Light ChannelListViewThemeData lerps completely to dark ChannelListViewThemeData''', + () { + expect( + const ChannelListViewThemeData().lerp(_channelListViewThemeDataControl, + _channelListViewThemeDataControlDark, 1), + _channelListViewThemeDataControlDark); + }); + + test( + '''Light ChannelListViewThemeData lerps halfway to dark ChannelListViewThemeData''', + () { + expect( + const ChannelListViewThemeData().lerp(_channelListViewThemeDataControl, + _channelListViewThemeDataControlDark, 0.5), + _channelListViewThemeDataControlHalfLerp); + }); + + test( + '''Dark ChannelListViewThemeData lerps completely to light ChannelListViewThemeData''', + () { + expect( + const ChannelListViewThemeData().lerp( + _channelListViewThemeDataControlDark, + _channelListViewThemeDataControl, + 1), + _channelListViewThemeDataControl); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _channelListViewThemeDataControl + .merge(_channelListViewThemeDataControlDark), + _channelListViewThemeDataControlDark); + }); + + testWidgets( + 'Passing no ChannelListViewThemeData returns default light theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + child: child, + ), + home: Builder( + builder: (BuildContext context) { + _context = context; + return Scaffold( + body: StreamChannel( + channel: MockChannel(), + child: const ChannelListView(), + ), + ); + }, + ), + ), + ); + + final channelListViewTheme = ChannelListViewTheme.of(_context); + expect(channelListViewTheme.backgroundColor, + _channelListViewThemeDataControl.backgroundColor); + }); + + testWidgets( + 'Passing no ChannelListViewThemeData returns default dark theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + streamChatThemeData: StreamChatThemeData.dark(), + child: child, + ), + home: Builder( + builder: (BuildContext context) { + _context = context; + return Scaffold( + body: StreamChannel( + channel: MockChannel(), + child: const MessageListView(), + ), + ); + }, + ), + ), + ); + + final channelListViewTheme = ChannelListViewTheme.of(_context); + expect(channelListViewTheme.backgroundColor, + _channelListViewThemeDataControlDark.backgroundColor); + }); +} + +final _channelListViewThemeDataControl = ChannelListViewThemeData( + backgroundColor: ColorTheme.light().appBg, +); + +const _channelListViewThemeDataControlHalfLerp = ChannelListViewThemeData( + backgroundColor: Color(0xff818384), +); + +final _channelListViewThemeDataControlDark = ChannelListViewThemeData( + backgroundColor: ColorTheme.dark().appBg, +); diff --git a/packages/stream_chat_flutter/test/src/channel_name_test.dart b/packages/stream_chat_flutter/test/src/channel_name_test.dart index c8d3df9b..fc868eb0 100644 --- a/packages/stream_chat_flutter/test/src/channel_name_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_name_test.dart @@ -16,7 +16,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel_preview_test.dart index 6add0c60..7d0c549e 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -18,8 +18,9 @@ void main() { when(() => channel.cid).thenReturn('cid'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(user); - when(() => clientState.userStream).thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUser).thenReturn(user); + when(() => clientState.currentUserStream) + .thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); diff --git a/packages/stream_chat_flutter/test/src/date_divider_test.dart b/packages/stream_chat_flutter/test/src/date_divider_test.dart index eb5e9b70..f84d4dce 100644 --- a/packages/stream_chat_flutter/test/src/date_divider_test.dart +++ b/packages/stream_chat_flutter/test/src/date_divider_test.dart @@ -13,7 +13,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); await tester.pumpWidget(MaterialApp( home: StreamChat( diff --git a/packages/stream_chat_flutter/test/src/default_translations_test.dart b/packages/stream_chat_flutter/test/src/default_translations_test.dart new file mode 100644 index 00000000..e78c0115 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/default_translations_test.dart @@ -0,0 +1,174 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + test('Default translations should exist', () { + const translations = DefaultTranslations.instance; + expect(translations.launchUrlError, isNotNull); + expect(translations.loadingUsersError, isNotNull); + expect(translations.noUsersLabel, isNotNull); + expect(translations.retryLabel, isNotNull); + expect(translations.userLastOnlineText, isNotNull); + expect(translations.userOnlineText, isNotNull); + expect(translations.userOnlineText, isNotNull); + // no users + expect(translations.userTypingText([]), isNotNull); + // single user + expect(translations.userTypingText([User(id: 'test-id')]), isNotNull); + // multiple users + expect( + translations.userTypingText([ + User(id: 'test-id-1'), + User(id: 'test-id-2'), + ]), + isNotNull, + ); + expect(translations.threadReplyLabel, isNotNull); + expect(translations.onlyVisibleToYouText, isNotNull); + expect(translations.threadReplyCountText(3), isNotNull); + expect( + translations.attachmentsUploadProgressText(remaining: 3, total: 10), + isNotNull, + ); + expect( + translations.pinnedByUserText( + pinnedBy: User(id: 'pinned-by-user-id'), + currentUser: OwnUser(id: 'current-user-id'), + ), + isNotNull, + ); + expect(translations.emptyMessagesText, isNotNull); + expect(translations.genericErrorText, isNotNull); + expect(translations.loadingMessagesError, isNotNull); + expect(translations.resultCountText(3), isNotNull); + expect(translations.messageDeletedText, isNotNull); + expect(translations.messageDeletedLabel, isNotNull); + expect(translations.messageReactionsLabel, isNotNull); + expect(translations.emptyChatMessagesText, isNotNull); + expect(translations.threadSeparatorText(3), isNotNull); + expect(translations.connectedLabel, isNotNull); + expect(translations.disconnectedLabel, isNotNull); + expect(translations.reconnectingLabel, isNotNull); + expect(translations.alsoSendAsDirectMessageLabel, isNotNull); + expect(translations.addACommentOrSendLabel, isNotNull); + expect(translations.searchGifLabel, isNotNull); + expect(translations.writeAMessageLabel, isNotNull); + expect(translations.instantCommandsLabel, isNotNull); + expect(translations.fileTooLargeAfterCompressionError(33), isNotNull); + expect(translations.fileTooLargeError(33), isNotNull); + expect(translations.emojiMatchingQueryText('sahil'), isNotNull); + expect(translations.addAFileLabel, isNotNull); + expect(translations.photoFromCameraLabel, isNotNull); + expect(translations.uploadAFileLabel, isNotNull); + expect(translations.uploadAPhotoLabel, isNotNull); + expect(translations.uploadAVideoLabel, isNotNull); + expect(translations.videoFromCameraLabel, isNotNull); + expect(translations.okLabel, isNotNull); + expect(translations.somethingWentWrongError, isNotNull); + expect(translations.addMoreFilesLabel, isNotNull); + expect(translations.enablePhotoAndVideoAccessMessage, isNotNull); + expect(translations.allowGalleryAccessMessage, isNotNull); + expect(translations.flagMessageLabel, isNotNull); + expect(translations.flagMessageQuestion, isNotNull); + expect(translations.flagLabel, isNotNull); + expect(translations.cancelLabel, isNotNull); + expect(translations.flagMessageSuccessfulLabel, isNotNull); + expect(translations.flagMessageSuccessfulText, isNotNull); + expect(translations.deleteLabel, isNotNull); + expect(translations.deleteMessageLabel, isNotNull); + expect(translations.deleteMessageQuestion, isNotNull); + expect(translations.operationCouldNotBeCompletedText, isNotNull); + expect(translations.replyLabel, isNotNull); + // pinned + expect(translations.togglePinUnpinText(pinned: true), isNotNull); + // un-pinned + expect(translations.togglePinUnpinText(pinned: false), isNotNull); + // delete-failed + expect( + translations.toggleDeleteRetryDeleteMessageText(isDeleteFailed: true), + isNotNull, + ); + // first-delete + expect( + translations.toggleDeleteRetryDeleteMessageText(isDeleteFailed: false), + isNotNull, + ); + expect(translations.copyMessageLabel, isNotNull); + expect(translations.editMessageLabel, isNotNull); + // resend-failed + expect( + translations.toggleResendOrResendEditedMessage(isUpdateFailed: true), + isNotNull, + ); + // first resend + expect( + translations.toggleResendOrResendEditedMessage(isUpdateFailed: false), + isNotNull, + ); + expect(translations.photosLabel, isNotNull); + // today + expect( + translations.sentAtText( + date: DateTime.now(), + time: DateTime.now(), + ), + isNotNull, + ); + // yesterday + expect( + translations.sentAtText( + date: DateTime.now().subtract(const Duration(days: 1)), + time: DateTime.now(), + ), + isNotNull, + ); + // any other day + expect( + translations.sentAtText( + date: DateTime.now().subtract(const Duration(days: 3)), + time: DateTime.now(), + ), + isNotNull, + ); + expect(translations.todayLabel, isNotNull); + expect(translations.yesterdayLabel, isNotNull); + expect(translations.channelIsMutedText, isNotNull); + expect(translations.noTitleText, isNotNull); + expect(translations.letsStartChattingLabel, isNotNull); + expect(translations.sendingFirstMessageLabel, isNotNull); + expect(translations.startAChatLabel, isNotNull); + expect(translations.loadingChannelsError, isNotNull); + expect(translations.deleteConversationLabel, isNotNull); + expect(translations.deleteConversationQuestion, isNotNull); + expect(translations.streamChatLabel, isNotNull); + expect(translations.searchingForNetworkText, isNotNull); + expect(translations.offlineLabel, isNotNull); + expect(translations.tryAgainLabel, isNotNull); + // 1 member + expect(translations.membersCountText(1), isNotNull); + // 3 members + expect(translations.membersCountText(3), isNotNull); + // 1 member + expect(translations.watchersCountText(1), isNotNull); + // 3 members + expect(translations.watchersCountText(3), isNotNull); + expect(translations.viewInfoLabel, isNotNull); + expect(translations.leaveGroupLabel, isNotNull); + expect(translations.leaveLabel, isNotNull); + expect(translations.leaveConversationLabel, isNotNull); + expect(translations.leaveConversationQuestion, isNotNull); + expect(translations.showInChatLabel, isNotNull); + expect(translations.saveImageLabel, isNotNull); + expect(translations.saveVideoLabel, isNotNull); + expect(translations.uploadErrorLabel, isNotNull); + expect(translations.giphyLabel, isNotNull); + expect(translations.shuffleLabel, isNotNull); + expect(translations.sendLabel, isNotNull); + expect(translations.withText, isNotNull); + expect(translations.inText, isNotNull); + expect(translations.youText, isNotNull); + expect(translations.ofText, isNotNull); + expect(translations.fileText, isNotNull); + expect(translations.replyToMessageLabel, isNotNull); + }); +} diff --git a/packages/stream_chat_flutter/test/src/deleted_message_test.dart b/packages/stream_chat_flutter/test/src/deleted_message_test.dart index 0e30d2b3..ac953ee9 100644 --- a/packages/stream_chat_flutter/test/src/deleted_message_test.dart +++ b/packages/stream_chat_flutter/test/src/deleted_message_test.dart @@ -14,7 +14,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); await tester.pumpWidget(MaterialApp( home: StreamChat( @@ -46,7 +46,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -99,7 +99,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -152,7 +152,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); diff --git a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart index 03f6ea65..2d271090 100644 --- a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart +++ b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart @@ -17,7 +17,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); diff --git a/packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart b/packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart index cbcc07a6..aefbb319 100644 --- a/packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart @@ -47,13 +47,6 @@ void main() { _galleryHeaderThemeDataDarkControl); }); - test('Merging dark and light themes results in a dark theme', () { - expect( - _galleryHeaderThemeDataDarkControl - .merge(_galleryHeaderThemeDataControl), - _galleryHeaderThemeDataControl); - }); - testWidgets( 'Passing no GalleryHeaderThemeData returns default light theme values', (WidgetTester tester) async { 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 00000000..bf15f6af Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_0.png differ 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 00000000..bd466f79 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_1.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_2.png b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_2.png new file mode 100644 index 00000000..d7d671c4 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_2.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_3.png b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_3.png new file mode 100644 index 00000000..5381c0f0 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/goldens/gradient_avatar_3.png differ diff --git a/packages/stream_chat_flutter/test/src/goldens/message_text.png b/packages/stream_chat_flutter/test/src/goldens/message_text.png index ecd93721..dda158b2 100644 Binary files a/packages/stream_chat_flutter/test/src/goldens/message_text.png and b/packages/stream_chat_flutter/test/src/goldens/message_text.png differ diff --git a/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart b/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart new file mode 100644 index 00000000..6f7ebe2e --- /dev/null +++ b/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/src/gradient_avatar.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + testWidgets( + 'control test', + (WidgetTester tester) async { + final client = MockClient(); + final clientState = MockClientState(); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: const Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GradientAvatar(name: 'demo user', userId: 'demo123'), + ), + ), + ), + ), + ), + ); + + expect(find.byType(GradientAvatar), findsOneWidget); + }, + ); + + testGoldens( + 'golden test for the name "demo user"', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GradientAvatar(name: 'demo user', userId: 'demo123'), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'gradient_avatar_0'); + }, + ); + + testGoldens( + 'golden test for the name "demo"', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GradientAvatar(name: 'demo', userId: 'demo1'), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'gradient_avatar_1'); + }, + ); + + testGoldens( + 'control special character test', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GradientAvatar( + name: 'd123@/d de:\$as', + userId: 'demo123', + ), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'gradient_avatar_2'); + }, + ); + + testGoldens( + 'control special character test 2', + (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 100, + height: 100, + child: GradientAvatar(name: '123@/d \$as', userId: 'demo123'), + ), + ), + ), + ), + ); + + await screenMatchesGolden(tester, 'gradient_avatar_3'); + }, + ); +} diff --git a/packages/stream_chat_flutter/test/src/image_footer_test.dart b/packages/stream_chat_flutter/test/src/image_footer_test.dart index 0fe3349a..434a78d4 100644 --- a/packages/stream_chat_flutter/test/src/image_footer_test.dart +++ b/packages/stream_chat_flutter/test/src/image_footer_test.dart @@ -16,7 +16,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); diff --git a/packages/stream_chat_flutter/test/src/info_tile_test.dart b/packages/stream_chat_flutter/test/src/info_tile_test.dart index 02387edc..f175ca9a 100644 --- a/packages/stream_chat_flutter/test/src/info_tile_test.dart +++ b/packages/stream_chat_flutter/test/src/info_tile_test.dart @@ -14,7 +14,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); await tester.pumpWidget(MaterialApp( home: StreamChat( @@ -44,7 +44,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); await tester.pumpWidget(MaterialApp( home: StreamChat( 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..38518b19 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 @@ -20,7 +20,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -66,7 +66,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -117,7 +117,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -172,7 +172,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -218,7 +218,7 @@ void main() { final clientState = MockClientState(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -265,7 +265,7 @@ void main() { final channel = MockChannel(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -314,7 +314,7 @@ void main() { final channel = MockChannel(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -364,7 +364,7 @@ void main() { final channel = MockChannel(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -414,7 +414,7 @@ void main() { final channel = MockChannel(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.sendMessage(any())) .thenAnswer((_) async => SendMessageResponse()); @@ -464,7 +464,7 @@ void main() { final channel = MockChannel(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.updateMessage(any())) .thenAnswer((_) async => UpdateMessageResponse()); @@ -514,7 +514,7 @@ void main() { final channel = MockChannel(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -568,7 +568,7 @@ void main() { final channel = MockChannel(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => client.flagMessage(any())) .thenThrow(StreamChatNetworkError(ChatErrorCode.internalSystemError)); @@ -624,7 +624,7 @@ void main() { final channel = MockChannel(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => client.flagMessage(any())) .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); @@ -680,7 +680,7 @@ void main() { final channel = MockChannel(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -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(); @@ -734,7 +734,7 @@ void main() { final channel = MockChannel(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.deleteMessage(any())) .thenThrow(StreamChatNetworkError(ChatErrorCode.internalSystemError)); @@ -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(); diff --git a/packages/stream_chat_flutter/test/src/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input_test.dart index f07f518d..679fb7f3 100644 --- a/packages/stream_chat_flutter/test/src/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input_test.dart @@ -16,7 +16,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); diff --git a/packages/stream_chat_flutter/test/src/message_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/message_list_view_theme_test.dart new file mode 100644 index 00000000..ffe54dd0 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_list_view_theme_test.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +class MockStreamChatClient extends Mock implements StreamChatClient {} + +void main() { + test('MessageListViewThemeData copyWith, ==, hashCode basics', () { + expect(const MessageListViewThemeData(), + const MessageListViewThemeData().copyWith()); + expect(const MessageListViewThemeData().hashCode, + const MessageListViewThemeData().copyWith().hashCode); + }); + + test( + '''Light MessageListViewThemeData lerps completely to dark MessageListViewThemeData''', + () { + expect( + const MessageListViewThemeData().lerp(_messageListViewThemeDataControl, + _messageListViewThemeDataControlDark, 1), + _messageListViewThemeDataControlDark); + }); + + test( + '''Light MessageListViewThemeData lerps halfway to dark MessageListViewThemeData''', + () { + expect( + const MessageListViewThemeData().lerp(_messageListViewThemeDataControl, + _messageListViewThemeDataControlDark, 0.5), + _messageListViewThemeDataControlHalfLerp); + }); + + test( + '''Dark MessageListViewThemeData lerps completely to light MessageListViewThemeData''', + () { + expect( + const MessageListViewThemeData().lerp( + _messageListViewThemeDataControlDark, + _messageListViewThemeDataControl, + 1), + _messageListViewThemeDataControl); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _messageListViewThemeDataControl + .merge(_messageListViewThemeDataControlDark), + _messageListViewThemeDataControlDark); + }); + + testWidgets( + 'Passing no MessageListViewThemeData returns default light theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockStreamChatClient(), + child: child, + ), + home: Builder( + builder: (BuildContext context) { + _context = context; + return Scaffold( + body: StreamChannel( + channel: MockChannel(), + child: const MessageListView(), + ), + ); + }, + ), + ), + ); + + final messageListViewTheme = MessageListViewTheme.of(_context); + expect(messageListViewTheme.backgroundColor, + _messageListViewThemeDataControl.backgroundColor); + }); + + testWidgets( + 'Passing no MessageListViewThemeData returns default dark theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockStreamChatClient(), + streamChatThemeData: StreamChatThemeData.dark(), + child: child, + ), + home: Builder( + builder: (BuildContext context) { + _context = context; + return Scaffold( + body: StreamChannel( + channel: MockChannel(), + child: const MessageListView(), + ), + ); + }, + ), + ), + ); + + final messageListViewTheme = MessageListViewTheme.of(_context); + expect(messageListViewTheme.backgroundColor, + _messageListViewThemeDataControlDark.backgroundColor); + }); +} + +final _messageListViewThemeDataControl = MessageListViewThemeData( + backgroundColor: ColorTheme.light().barsBg, +); + +const _messageListViewThemeDataControlHalfLerp = MessageListViewThemeData( + backgroundColor: Color(0xff87898b), +); + +final _messageListViewThemeDataControlDark = MessageListViewThemeData( + backgroundColor: ColorTheme.dark().barsBg, +); diff --git a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart index 9452e7e6..3732ca0d 100644 --- a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart @@ -16,7 +16,7 @@ void main() { final themeData = ThemeData(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final streamTheme = StreamChatThemeData.fromTheme(themeData); @@ -61,7 +61,7 @@ void main() { final themeData = ThemeData(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final streamTheme = StreamChatThemeData.fromTheme(themeData); diff --git a/packages/stream_chat_flutter/test/src/message_search_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/message_search_list_view_theme_test.dart new file mode 100644 index 00000000..e485e0d6 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_search_list_view_theme_test.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + test('MessageSearchListViewThemeData copyWith, ==, hashCode basics', () { + expect(const MessageSearchListViewThemeData(), + const MessageSearchListViewThemeData().copyWith()); + expect(const MessageSearchListViewThemeData().hashCode, + const MessageSearchListViewThemeData().copyWith().hashCode); + }); + + test( + '''Light MessageSearchListViewThemeData lerps completely to dark MessageSearchListViewThemeData''', + () { + expect( + const MessageSearchListViewThemeData().lerp( + _messageSearchListViewThemeDataControl, + _messageSearchListViewThemeDataControlDark, + 1), + _messageSearchListViewThemeDataControlDark); + }); + + test( + '''Light MessageSearchListViewThemeData lerps halfway to dark MessageSearchListViewThemeData''', + () { + expect( + const MessageSearchListViewThemeData().lerp( + _messageSearchListViewThemeDataControl, + _messageSearchListViewThemeDataControlDark, + 0.5), + _messageSearchListViewThemeDataControlHalfLerp); + }); + + test( + '''Dark MessageSearchListViewThemeData lerps completely to light MessageSearchListViewThemeData''', + () { + expect( + const MessageSearchListViewThemeData().lerp( + _messageSearchListViewThemeDataControlDark, + _messageSearchListViewThemeDataControl, + 1), + _messageSearchListViewThemeDataControl); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _messageSearchListViewThemeDataControl + .merge(_messageSearchListViewThemeDataControlDark), + _messageSearchListViewThemeDataControlDark); + }); + + testWidgets( + '''Passing no MessageSearchListViewThemeData returns default light theme values''', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + child: child, + ), + home: Builder( + builder: (BuildContext context) { + _context = context; + return Scaffold( + body: MessageSearchBloc( + child: MessageSearchListView( + filters: Filter.in_('members', const ['test_id']), + ), + ), + ); + }, + ), + ), + ); + + final messageSearchListViewTheme = MessageSearchListViewTheme.of(_context); + expect(messageSearchListViewTheme.backgroundColor, + _messageSearchListViewThemeDataControl.backgroundColor); + }); + + testWidgets( + '''Passing no MessageSearchListViewThemeData returns default dark theme values''', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + streamChatThemeData: StreamChatThemeData.dark(), + child: child, + ), + home: Builder( + builder: (BuildContext context) { + _context = context; + return Scaffold( + body: MessageSearchBloc( + child: MessageSearchListView( + filters: Filter.in_('members', const ['test_id']), + ), + ), + ); + }, + ), + ), + ); + + final messageSearchListViewTheme = MessageSearchListViewTheme.of(_context); + expect(messageSearchListViewTheme.backgroundColor, + _messageSearchListViewThemeDataControlDark.backgroundColor); + }); +} + +final _messageSearchListViewThemeDataControl = MessageSearchListViewThemeData( + backgroundColor: ColorTheme.light().appBg, +); + +const _messageSearchListViewThemeDataControlHalfLerp = + MessageSearchListViewThemeData( + backgroundColor: Color(0xff818384), +); + +final _messageSearchListViewThemeDataControlDark = + MessageSearchListViewThemeData( + backgroundColor: ColorTheme.dark().appBg, +); diff --git a/packages/stream_chat_flutter/test/src/message_text_test.dart b/packages/stream_chat_flutter/test/src/message_text_test.dart index f64e2bf1..5bfe0a34 100644 --- a/packages/stream_chat_flutter/test/src/message_text_test.dart +++ b/packages/stream_chat_flutter/test/src/message_text_test.dart @@ -8,10 +8,33 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'mocks.dart'; import 'simple_frame.dart'; +void expectTextStrings(Iterable widgets, List strings) { + var currentString = 0; + for (final widget in widgets) { + if (widget is RichText) { + final span = widget.text as TextSpan; + final text = _extractTextFromTextSpan(span); + expect(text, equals(strings[currentString])); + currentString += 1; + } + } +} + +String _extractTextFromTextSpan(TextSpan span) { + var text = span.text ?? ''; + if (span.children != null) { + for (final child in span.children! as Iterable) { + text += _extractTextFromTextSpan(child); + } + } + return text; +} + void main() { testWidgets( 'it should show correct message text', (WidgetTester tester) async { + final currentUser = OwnUser(id: 'user-id'); final client = MockClient(); final clientState = MockClientState(); final channel = MockChannel(); @@ -21,7 +44,9 @@ void main() { final streamTheme = StreamChatThemeData.fromTheme(themeData); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(currentUser); + when(() => clientState.currentUserStream) + .thenAnswer((_) => Stream.value(currentUser)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -54,9 +79,107 @@ void main() { }, ); + group('Message with i18n field', () { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + const messageTheme = MessageTheme(); + + final currentUser = OwnUser( + id: 'sahil', + language: 'hi', + ); + + setUp(() { + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(currentUser); + when(() => clientState.currentUserStream) + .thenAnswer((_) => Stream.value(currentUser)); + + when(() => channel.state).thenReturn(channelState); + when(() => channel.client).thenReturn(client); + when(() => channel.isMuted).thenReturn(false); + when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false)); + }); + + testWidgets( + 'should show correct translated message text as per user language', + (WidgetTester tester) async { + final message = Message( + text: 'Hello', + i18n: const { + 'en_text': 'Hello', + 'hi_text': 'рдирдорд╕реНрддреЗ', + 'language': 'en', + }, + ); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: MessageText( + message: message, + messageTheme: messageTheme, + ), + ), + ), + ), + ), + ); + + expect(find.byType(MarkdownBody), findsOneWidget); + + final widgets = tester.allWidgets; + expectTextStrings(widgets, ['рдирдорд╕реНрддреЗ']); + }, + ); + + testWidgets( + '''should show default text if i18n does not contain translations as per user language''', + (WidgetTester tester) async { + final message = Message( + text: 'Hello', + i18n: const { + 'en_text': 'Hello', + 'fr_text': 'Bonjour', + 'language': 'en', + }, + ); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: MessageText( + message: message, + messageTheme: messageTheme, + ), + ), + ), + ), + ), + ); + + expect(find.byType(MarkdownBody), findsOneWidget); + + final widgets = tester.allWidgets; + expectTextStrings(widgets, ['Hello']); + }, + ); + }); + testGoldens( 'control test', (WidgetTester tester) async { + final currentUser = OwnUser(id: 'user-id'); final client = MockClient(); final clientState = MockClientState(); final channel = MockChannel(); @@ -66,7 +189,9 @@ void main() { final streamTheme = StreamChatThemeData.fromTheme(themeData); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(currentUser); + when(() => clientState.currentUserStream) + .thenAnswer((_) => Stream.value(currentUser)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -80,7 +205,7 @@ void main() { }); const messageText = ''' - a message. +a message. with multiple lines and a list: - a. okasd @@ -90,14 +215,18 @@ cool.'''; await tester.pumpWidgetBuilder( materialAppWrapper()(SimpleFrame( - child: StreamChannel( - channel: channel, - child: Scaffold( - body: MessageText( - message: Message( - text: messageText, + child: StreamChat( + client: client, + connectivityStream: Stream.value(ConnectivityResult.wifi), + child: StreamChannel( + channel: channel, + child: Scaffold( + body: MessageText( + message: Message( + text: messageText, + ), + messageTheme: streamTheme.otherMessageTheme, ), - messageTheme: streamTheme.otherMessageTheme, ), ), ), diff --git a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart index 1e3c9f5e..6e823b43 100644 --- a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart +++ b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart @@ -6,32 +6,8 @@ import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'mocks.dart'; -import 'simple_frame.dart'; void main() { - testGoldens( - 'it should show no reactions', - (WidgetTester tester) async { - await tester.pumpWidgetBuilder( - SimpleFrame( - child: StreamChatTheme( - data: StreamChatThemeData(), - child: const SizedBox( - child: ReactionBubble( - reactions: [], - borderColor: Colors.black, - backgroundColor: Colors.white, - maskColor: Colors.white, - ), - ), - ), - ), - surfaceSize: const Size(100, 100), - ); - await screenMatchesGolden(tester, 'reaction_bubble_0'); - }, - ); - testGoldens( 'it should show a like - light theme', (WidgetTester tester) async { @@ -40,7 +16,7 @@ void main() { final themeData = ThemeData.light(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); final theme = StreamChatThemeData.fromTheme(themeData); await tester.pumpWidgetBuilder( @@ -77,7 +53,7 @@ void main() { final theme = StreamChatThemeData.fromTheme(themeData); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); await tester.pumpWidgetBuilder( StreamChat( @@ -114,7 +90,7 @@ void main() { final theme = StreamChatThemeData.fromTheme(themeData); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); await tester.pumpWidgetBuilder( StreamChat( @@ -159,7 +135,7 @@ void main() { final theme = StreamChatThemeData.fromTheme(themeData); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); await tester.pumpWidgetBuilder( StreamChat( @@ -203,7 +179,7 @@ void main() { final themeData = ThemeData(); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); await tester.pumpWidgetBuilder( StreamChat( diff --git a/packages/stream_chat_flutter/test/src/system_message_test.dart b/packages/stream_chat_flutter/test/src/system_message_test.dart index 2e221d57..24eccdf6 100644 --- a/packages/stream_chat_flutter/test/src/system_message_test.dart +++ b/packages/stream_chat_flutter/test/src/system_message_test.dart @@ -17,7 +17,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -68,7 +68,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -120,7 +120,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); diff --git a/packages/stream_chat_flutter/test/src/thread_header_test.dart b/packages/stream_chat_flutter/test/src/thread_header_test.dart index 2b649be3..1bc4c865 100644 --- a/packages/stream_chat_flutter/test/src/thread_header_test.dart +++ b/packages/stream_chat_flutter/test/src/thread_header_test.dart @@ -16,7 +16,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -81,7 +81,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); diff --git a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart index 6c8a3351..8d790266 100644 --- a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart @@ -16,7 +16,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); diff --git a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart b/packages/stream_chat_flutter/test/src/unread_indicator_test.dart index 96a6c678..00359089 100644 --- a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/unread_indicator_test.dart @@ -16,7 +16,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -58,7 +58,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => clientState.channels).thenReturn({ channel.cid!: channel, }); @@ -98,7 +98,7 @@ void main() { final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); when(() => client.state).thenReturn(clientState); - when(() => clientState.user).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => clientState.channels).thenReturn({ channel.cid!: channel, }); diff --git a/packages/stream_chat_flutter/test/src/user_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/user_list_view_theme_test.dart new file mode 100644 index 00000000..e9b7985a --- /dev/null +++ b/packages/stream_chat_flutter/test/src/user_list_view_theme_test.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + test('UserListViewThemeData copyWith, ==, hashCode basics', () { + expect(const UserListViewThemeData(), + const UserListViewThemeData().copyWith()); + }); + + test( + '''Light UserListViewThemeData lerps completely to dark UserListViewThemeData''', + () { + expect( + const UserListViewThemeData().lerp(_userListViewThemeDataControl, + _userListViewThemeDataControlDark, 1), + _userListViewThemeDataControlDark); + }); + + test( + '''Light UserListViewThemeData lerps halfway to dark UserListViewThemeData''', + () { + expect( + const UserListViewThemeData().lerp(_userListViewThemeDataControl, + _userListViewThemeDataControlDark, 0.5), + _userListViewThemeDataControlHalfLerp); + }); + + test( + '''Dark UserListViewThemeData lerps completely to light UserListViewThemeData''', + () { + expect( + const UserListViewThemeData().lerp(_userListViewThemeDataControlDark, + _userListViewThemeDataControl, 1), + _userListViewThemeDataControl); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _userListViewThemeDataControl.merge(_userListViewThemeDataControlDark), + _userListViewThemeDataControlDark); + }); + + testWidgets( + 'Passing no ChannelListViewThemeData returns default light theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + child: child, + ), + home: Builder( + builder: (BuildContext context) { + _context = context; + return const Scaffold( + body: UsersBloc( + child: UserListView(), + ), + ); + }, + ), + ), + ); + + final userListViewTheme = UserListViewTheme.of(_context); + expect(userListViewTheme.backgroundColor, + _userListViewThemeDataControl.backgroundColor); + }); + + testWidgets( + 'Passing no ChannelListViewThemeData returns default dark theme values', + (WidgetTester tester) async { + late BuildContext _context; + await tester.pumpWidget( + MaterialApp( + builder: (context, child) => StreamChat( + client: MockClient(), + streamChatThemeData: StreamChatThemeData.dark(), + child: child, + ), + home: Builder( + builder: (BuildContext context) { + _context = context; + return const Scaffold( + body: UsersBloc( + child: UserListView(), + ), + ); + }, + ), + ), + ); + + final userListViewTheme = UserListViewTheme.of(_context); + expect(userListViewTheme.backgroundColor, + _userListViewThemeDataControlDark.backgroundColor); + }); +} + +final _userListViewThemeDataControl = UserListViewThemeData( + backgroundColor: ColorTheme.light().appBg, +); + +const _userListViewThemeDataControlHalfLerp = UserListViewThemeData( + backgroundColor: Color(0xff818384), +); + +final _userListViewThemeDataControlDark = UserListViewThemeData( + backgroundColor: ColorTheme.dark().appBg, +); diff --git a/packages/stream_chat_flutter/test/utils/golden.dart b/packages/stream_chat_flutter/test/utils/golden.dart new file mode 100644 index 00000000..4fc0aff3 --- /dev/null +++ b/packages/stream_chat_flutter/test/utils/golden.dart @@ -0,0 +1,58 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:golden_toolkit/src/testing_tools.dart'; +import 'package:path/path.dart' as path; + +const double _kGoldenDiffTolerance = 0.05; + +/// Wrapper function for golden tests. +Future customExpectGoldenMatches( + WidgetTester tester, + String name, { + bool? autoHeight, + Finder? finder, + CustomPump? customPump, + @Deprecated(''' +This method level parameter will be removed in an upcoming release. This can be configured globally. If you have concerns, please file an issue with your use case.''') bool? skip, +}) { + final goldenPath = path.join('test/src/goldens'); + print('goldenPath: $goldenPath'); + goldenFileComparator = CustomGoldenFileComparator(Uri.parse(goldenPath)); + + return compareWithGolden( + tester, + name, + autoHeight: autoHeight, + finder: finder, + customPump: customPump, + skip: skip, + // This value is actually ignored. We are forced to pass it because the + // downstream API is structured poorly. This should be refactored. + device: Device.phone, + fileNameFactory: (String name, Device device) => + GoldenToolkit.configuration.fileNameFactory(name), + ); +} + +class CustomGoldenFileComparator extends LocalFileComparator { + CustomGoldenFileComparator(Uri testFile) : super(testFile); + + @override + Future compare(Uint8List imageBytes, Uri golden) async { + print('golden.toString(): ${golden.toString()}'); + final result = await GoldenFileComparator.compareLists( + imageBytes, + await getGoldenBytes(golden), + ); + + if (!result.passed && result.diffPercent > _kGoldenDiffTolerance) { + final error = await generateFailureOutput(result, golden, basedir); + throw FlutterError(error); + } + return result.passed; + } +} diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 2f7abfe2..770f6678 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,15 @@ +## 2.1.0 + +ЁЯЫСя╕П Breaking Changes from `2.0.0` +- Changed default message filter of `MessageListCore` + +тЬЕ Added +- Added `MessageListCore.paginationLimit` + +ЁЯФД Changed +- `StreamChatCore.of(context).user` is now deprecated in favor of `StreamChatCore.of(context).currentUser`. +- `StreamChatCore.of(context).userStream` is now deprecated in favor of `StreamChatCore.of(context).currentUserStream`. + ## 2.0.0 ЁЯЫСя╕П Breaking Changes from `1.5.3` diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index 4989f38f..0a7b0bce 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -87,7 +87,7 @@ class HomeScreen extends StatelessWidget { filter: Filter.and([ Filter.equal('type', 'messaging'), Filter.in_('members', [ - StreamChatCore.of(context).user!.id, + StreamChatCore.of(context).currentUser!.id, ]) ]), emptyBuilder: (BuildContext context) => const Center( @@ -336,7 +336,7 @@ class _MessageScreenState extends State { /// below, we add two simple extensions to the [StreamChatClient] and [Channel]. extension on StreamChatClient { /// Fetches the current user id. - String get uid => state.user!.id; + String get uid => state.currentUser!.id; } extension on Channel { 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..d3d69dfb 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. /// @@ -112,7 +116,7 @@ class MessageListCoreState extends State { bool get _isThreadConversation => widget.parentMessage != null; - OwnUser? get _currentUser => _streamChannel!.channel.client.state.user; + OwnUser? get _currentUser => _streamChannel!.channel.client.state.currentUser; var _messages = []; @@ -130,8 +134,7 @@ class MessageListCoreState extends State { bool defaultFilter(Message m) { final isMyMessage = m.user?.id == _currentUser?.id; - final isDeletedOrShadowed = m.isDeleted == true || m.shadowed == true; - if (isDeletedOrShadowed && !isMyMessage) return false; + if (m.shadowed && !isMyMessage) return false; return true; } @@ -163,13 +166,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 +189,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 +210,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 diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart index 6e39d5e3..3d07b3c7 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart @@ -97,11 +97,25 @@ class StreamChatCoreState extends State @override Widget build(BuildContext context) => widget.child; + // coverage:ignore-start + /// The current user - User? get user => client.state.user; + @Deprecated('Use `.currentUser` instead, Will be removed in future releases') + User? get user => client.state.currentUser; /// The current user as a stream - Stream get userStream => client.state.userStream; + @Deprecated( + 'Use `.currentUserStream` instead, Will be removed in future releases', + ) + Stream get userStream => client.state.currentUserStream; + + // coverage:ignore-end + + /// The current user + User? get currentUser => client.state.currentUser; + + /// The current user as a stream + Stream get currentUserStream => client.state.currentUserStream; StreamSubscription? _connectivitySubscription; @@ -126,7 +140,7 @@ class StreamChatCoreState extends State if (!_isInForeground) return; if (_isConnectionAvailable) { if (client.wsConnectionStatus == ConnectionStatus.disconnected && - user != null) { + currentUser != null) { client.openConnection(); } } else { @@ -163,7 +177,7 @@ class StreamChatCoreState extends State AppLifecycleState.resumed, AppLifecycleState.inactive, ].contains(state); - if (user != null) { + if (currentUser != null) { if (_isInForeground) { _onForeground(); } else { diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index cc59f9fe..8a83f4b8 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 2.0.0 +version: 2.1.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -16,7 +16,7 @@ dependencies: sdk: flutter meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^2.0.0 + stream_chat: ^2.1.0 dev_dependencies: fake_async: ^1.2.0 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..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,9 @@ void main() { (tester) async { const messageListCoreKey = Key('messageListCore'); final controller = MessageListController(); + const 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); }, diff --git a/packages/stream_chat_flutter_core/test/mocks.dart b/packages/stream_chat_flutter_core/test/mocks.dart index 73227e7c..7013d408 100644 --- a/packages/stream_chat_flutter_core/test/mocks.dart +++ b/packages/stream_chat_flutter_core/test/mocks.dart @@ -18,10 +18,10 @@ class MockClient extends Mock implements StreamChatClient { } class MockClientState extends Mock implements ClientState { - OwnUser? _user; + OwnUser? _currentUser; @override - OwnUser get user => _user ??= OwnUser( + OwnUser get currentUser => _currentUser ??= OwnUser( id: 'testUserId', role: 'admin', createdAt: DateTime.now(), diff --git a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart index ee9a86cb..addcab87 100644 --- a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart @@ -431,7 +431,7 @@ void main() { ); testWidgets( - 'streamChatCoreState.userStream should emit all the user events ' + 'streamChatCoreState.currentUserStream should emit all the user events ' 'provided by client', (tester) async { await tester.runAsync(() async { @@ -451,7 +451,7 @@ void main() { expect(find.byKey(streamChatCoreKey), findsOneWidget); expect(find.byKey(childKey), findsOneWidget); - when(() => mockClient.state.userStream) + when(() => mockClient.state.currentUserStream) .thenAnswer((_) => userController.stream); final streamChatCoreState = tester.state( @@ -462,7 +462,7 @@ void main() { userController.add(ownUser); await expectLater( - streamChatCoreState.userStream, + streamChatCoreState.currentUserStream, emits(ownUser), ); 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..5ca6d89e --- /dev/null +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +* First 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..49fd4442 --- /dev/null +++ b/packages/stream_chat_localizations/README.md @@ -0,0 +1,115 @@ +# Official Localizations for [Stream Chat Flutter](https://getstream.io/chat/sdk/flutter/) library. + +> The Official localizations for Stream Chat Flutter, a service for +> building chat applications. + +[![Pub](https://img.shields.io/pub/v/stream_chat_localizations.svg)](https://pub.dartlang.org/packages/stream_chat_localizations) +![](https://img.shields.io/badge/platform-flutter%20%7C%20flutter%20web-ff69b4.svg?style=flat-square) +![CI](https://github.com/GetStream/stream-chat-flutter/workflows/stream_flutter_workflow/badge.svg?branch=master) + + +**Quick Links** + +- [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/) + +This package provides localized strings for the stream chat widgets for many languages. + +### Changelog + +Check out the [changelog on pub.dev](https://pub.dev/packages/stream_chat_localizations/changelog) to see the latest changes in the package. + +## Supported languages + +At the moment we support the following languages: +- [English](https://pub.dev/documentation/stream_chat_localizations/latest/stream_chat_localizations/StreamChatLocalizationsEn-class.html) +- [Hindi](https://pub.dev/documentation/stream_chat_localizations/latest/stream_chat_localizations/StreamChatLocalizationsHi-class.html) +- [Italian](https://pub.dev/documentation/stream_chat_localizations/latest/stream_chat_localizations/StreamChatLocalizationsIt-class.html) +- [French](https://pub.dev/documentation/stream_chat_localizations/latest/stream_chat_localizations/StreamChatLocalizationsFr-class.html) + +More languages will be added in the future. Feel free to [contribute](https://github.com/GetStream/stream-chat-flutter/blob/master/CONTRIBUTING.md) to add more languages. + +## Add dependency + +Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_localizations.svg)](https://pub.dartlang.org/packages/stream_chat_localizations) +```yaml +dependencies: + stream_chat_localizations: ^latest_version +``` + +You should then run `flutter packages get` + +### Usage + +```dart +import 'package:flutter/material.dart'; +import 'package:stream_chat_localizations/stream_chat_localizations.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + // Add all the supported locales + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('it'), + ], + // Add GlobalStreamChatLocalizations.delegates + localizationsDelegates: GlobalStreamChatLocalizations.delegates, + builder: (context, widget) => StreamChat( + client: client, + child: widget, + ), + home: StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ); + } +} +``` + +### Adding a new language + +To add a new language, you need to create a new class extending `GlobalStreamChatLocalizations` and create a delegate for it adding it to the `delegates` array. + +Checkout [this example](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/example/lib/add_new_lang.dart) to see how to add a new language. + +### Override exisiting languages + +To override an existing language, you need to create a new class extending that particular language class and create a delegate for it adding it to the `delegates` array. + +Checkout [this example](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/example/lib/override_lang.dart) to see how to override an existing language. + +### тЪая╕П Note on **iOS** + +For translation to work on **iOS** you need to add supported locales to +`ios/Runner/Info.plist` as described [here](https://flutter.dev/docs/development/accessibility-and-localization/internationalization#specifying-supportedlocales). + +Example: + +```xml +CFBundleLocalizations + + en + nb + fr + it + +``` + +## Contributing + +We welcome code changes that improve this library or fix a problem, +please make sure to follow all best practices and add tests if applicable before submitting a Pull Request on Github. +We are pleased to merge your code into the official repository. +Make sure to sign our [Contributor License Agreement (CLA)](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) first. +See our license file for more details. diff --git a/packages/stream_chat_localizations/example/.gitignore b/packages/stream_chat_localizations/example/.gitignore new file mode 100644 index 00000000..9d532b18 --- /dev/null +++ b/packages/stream_chat_localizations/example/.gitignore @@ -0,0 +1,41 @@ +# 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/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/stream_chat_localizations/example/.metadata b/packages/stream_chat_localizations/example/.metadata new file mode 100644 index 00000000..182cccaf --- /dev/null +++ b/packages/stream_chat_localizations/example/.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: 78910062997c3a836feee883712c241a5fd22983 + channel: stable + +project_type: app diff --git a/packages/stream_chat_localizations/example/README.md b/packages/stream_chat_localizations/example/README.md new file mode 100644 index 00000000..07e5ac18 --- /dev/null +++ b/packages/stream_chat_localizations/example/README.md @@ -0,0 +1,2 @@ +# Stream Chat Persistence Example +Please see `lib/` for example code. \ No newline at end of file diff --git a/packages/stream_chat_localizations/example/android/.gitignore b/packages/stream_chat_localizations/example/android/.gitignore new file mode 100644 index 00000000..0a741cb4 --- /dev/null +++ b/packages/stream_chat_localizations/example/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/packages/stream_chat_localizations/example/android/app/build.gradle b/packages/stream_chat_localizations/example/android/app/build.gradle new file mode 100644 index 00000000..fbd6268e --- /dev/null +++ b/packages/stream_chat_localizations/example/android/app/build.gradle @@ -0,0 +1,64 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 30 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + checkReleaseBuilds false + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.example" + minSdkVersion 21 + targetSdkVersion 30 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/packages/stream_chat_localizations/example/android/app/src/debug/AndroidManifest.xml b/packages/stream_chat_localizations/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..c208884f --- /dev/null +++ b/packages/stream_chat_localizations/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/stream_chat_localizations/example/android/app/src/main/AndroidManifest.xml b/packages/stream_chat_localizations/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..55ca830c --- /dev/null +++ b/packages/stream_chat_localizations/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_localizations/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/stream_chat_localizations/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 00000000..e793a000 --- /dev/null +++ b/packages/stream_chat_localizations/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/packages/stream_chat_localizations/example/android/app/src/main/res/drawable/launch_background.xml b/packages/stream_chat_localizations/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/packages/stream_chat_localizations/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/packages/stream_chat_localizations/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/stream_chat_localizations/example/android/app/src/main/res/values/styles.xml b/packages/stream_chat_localizations/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..1f83a33f --- /dev/null +++ b/packages/stream_chat_localizations/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/stream_chat_localizations/example/android/app/src/profile/AndroidManifest.xml b/packages/stream_chat_localizations/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..c208884f --- /dev/null +++ b/packages/stream_chat_localizations/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/stream_chat_localizations/example/android/build.gradle b/packages/stream_chat_localizations/example/android/build.gradle new file mode 100644 index 00000000..3e0873de --- /dev/null +++ b/packages/stream_chat_localizations/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.5.20' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.2.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/stream_chat_localizations/example/android/gradle.properties b/packages/stream_chat_localizations/example/android/gradle.properties new file mode 100644 index 00000000..a6738207 --- /dev/null +++ b/packages/stream_chat_localizations/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true diff --git a/packages/stream_chat_localizations/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/stream_chat_localizations/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3df6b338 --- /dev/null +++ b/packages/stream_chat_localizations/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip diff --git a/packages/stream_chat_localizations/example/android/settings.gradle b/packages/stream_chat_localizations/example/android/settings.gradle new file mode 100644 index 00000000..44e62bcf --- /dev/null +++ b/packages/stream_chat_localizations/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/stream_chat_localizations/example/ios/.gitignore b/packages/stream_chat_localizations/example/ios/.gitignore new file mode 100644 index 00000000..e96ef602 --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..261aa5a8 --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,563 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0E9B23A7BA08E142FA5000CF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D76F8024ABE1070895D659BA /* Pods_Runner.framework */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3F6A054EEDAF06BCF649C130 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 6FE1ECA061EBB001F6BCE8B7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D76F8024ABE1070895D659BA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EC2A45E9198C1011BED23834 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0E9B23A7BA08E142FA5000CF /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 04AAB960E493BD92262BBF82 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D76F8024ABE1070895D659BA /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 8559384DCD98ED6067CEF8CB /* Pods */ = { + isa = PBXGroup; + children = ( + 3F6A054EEDAF06BCF649C130 /* Pods-Runner.debug.xcconfig */, + EC2A45E9198C1011BED23834 /* Pods-Runner.release.xcconfig */, + 6FE1ECA061EBB001F6BCE8B7 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 8559384DCD98ED6067CEF8CB /* Pods */, + 04AAB960E493BD92262BBF82 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + C1EE41B94EADE099F7AF3A1C /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + BD38DACC9A0AD429D9ED9939 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + BD38DACC9A0AD429D9ED9939 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + C1EE41B94EADE099F7AF3A1C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..a28140cf --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_localizations/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/stream_chat_localizations/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/stream_chat_localizations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/stream_chat_localizations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/stream_chat_localizations/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/stream_chat_localizations/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/stream_chat_localizations/example/ios/Runner/AppDelegate.swift b/packages/stream_chat_localizations/example/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..70693e4a --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..28c6bf03 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..f091b6b0 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cde1211 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..d0ef06e7 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..dcdc2306 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..2ccbfd96 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..c8f9ed8f Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..a6d6b860 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..75b2d164 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..c4df70d3 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..6a84f41e Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..d0e1f585 Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/stream_chat_localizations/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/stream_chat_localizations/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_localizations/example/ios/Runner/Base.lproj/Main.storyboard b/packages/stream_chat_localizations/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/stream_chat_localizations/example/ios/Runner/Info.plist b/packages/stream_chat_localizations/example/ios/Runner/Info.plist new file mode 100644 index 00000000..a060db61 --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/stream_chat_localizations/example/ios/Runner/Runner-Bridging-Header.h b/packages/stream_chat_localizations/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/packages/stream_chat_localizations/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart new file mode 100644 index 00000000..1412f4e0 --- /dev/null +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -0,0 +1,500 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_localizations/stream_chat_localizations.dart'; + +class _NnStreamChatLocalizationsDelegate + extends LocalizationsDelegate { + const _NnStreamChatLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) => locale.languageCode == 'nn'; + + @override + Future load(Locale locale) => + SynchronousFuture(const NnStreamChatLocalizations()); + + @override + bool shouldReload(_NnStreamChatLocalizationsDelegate old) => false; +} + +/// A custom set of localizations for the 'nn' locale. In this example, only +/// the value for launchUrlError was modified to use a custom message as +/// an example. Everything else uses the American English (en_US) messages +/// and formatting. +class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { + /// Create an instance of the translation bundle for English. + const NnStreamChatLocalizations({String localeName = 'nn'}) + : super(localeName: localeName); + + /// A [LocalizationsDelegate] for [NnStreamChatLocalizations]. + static const delegate = _NnStreamChatLocalizationsDelegate(); + + @override + String get launchUrlError => 'Custom error'; + + @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 is deleted.'; + + @override + String get messageDeletedLabel => 'Message deleted'; + + @override + String get messageReactionsLabel => '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 fileTooLargeAfterCompressionError(double limitInMB) => + 'The file is too large to upload. ' + 'The file size limit is $limitInMB MB. ' + 'We tried compressing it, but it was not enough.'; + + @override + String fileTooLargeError(double limitInMB) => + 'The file is too large to upload. The file size limit is $limitInMB MB.'; + + @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 somethingWentWrongError => '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'; + + String _getDay(DateTime dateTime) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = DateTime(now.year, now.month, now.day - 1); + + final date = DateTime(dateTime.year, dateTime.month, dateTime.day); + + if (date == today) { + return 'today'; + } else if (date == yesterday) { + return 'yesterday'; + } else { + return 'on ${Jiffy(date).MMMd}'; + } + } + + @override + String sentAtText({required DateTime date, required DateTime time}) => + 'Sent ${_getDay(date)} at ${Jiffy(time.toLocal()).format('HH:mm')}'; + + @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 searchingForNetworkText => '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'; + + @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'; + + @override + String get replyToMessageLabel => 'Reply to Message'; +} + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + /// Create a new instance of [StreamChatClient] passing the apikey obtained + /// from your project dashboard. + final client = StreamChatClient( + 's2dxdhpxd94g', + logLevel: Level.INFO, + ); + + /// 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. + /// + /// Please see the following for more information: + /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ + await client.connectUser( + User(id: 'super-band-9'), + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' + '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + ); + + final channel = client.channel('messaging', id: 'godevs'); + + await channel.watch(); + + runApp( + MyApp( + client: client, + channel: channel, + ), + ); +} + +/// Example application using Stream Chat Flutter widgets. +/// +/// Stream Chat Flutter is a set of Flutter widgets which provide full chat +/// functionalities for building Flutter applications using Stream. If you'd +/// prefer using minimal wrapper widgets for your app, please see our other +/// package, `stream_chat_flutter_core`. +class MyApp extends StatelessWidget { + /// Example using Stream's Flutter package. + /// + /// If you'd prefer using minimal wrapper widgets for your app, please see + /// our other package, `stream_chat_flutter_core`. + const MyApp({ + Key? key, + required this.client, + required this.channel, + }) : super(key: key); + + /// Instance of Stream Client. + /// + /// Stream's [StreamChatClient] can be used to connect to our servers and + /// set the default user for the application. Performing these actions + /// trigger a websocket connection allowing for real-time updates. + final StreamChatClient client; + + /// Instance of the Channel + final Channel channel; + + @override + Widget build(BuildContext context) => MaterialApp( + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + // Add all the supported locales + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('it'), + // Add support for additional 'nn' locale + Locale('nn'), + ], + // Add overridden "NnStreamChatLocalizations.delegate" along with + // "GlobalStreamChatLocalizations.delegates" + localizationsDelegates: const [ + NnStreamChatLocalizations.delegate, + ...GlobalStreamChatLocalizations.delegates, + ], + builder: (context, widget) => StreamChat( + client: client, + child: widget, + ), + home: StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ); +} + +/// A list of messages sent in the current channel. +/// +/// This is implemented using [MessageListView], a widget that provides query +/// functionalities fetching the messages from the api and showing them in a +/// listView. +class ChannelPage extends StatelessWidget { + /// Creates the page that shows the list of messages + const ChannelPage({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: const [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ); +} diff --git a/packages/stream_chat_localizations/example/lib/main.dart b/packages/stream_chat_localizations/example/lib/main.dart new file mode 100644 index 00000000..436553c1 --- /dev/null +++ b/packages/stream_chat_localizations/example/lib/main.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_localizations/stream_chat_localizations.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + /// Create a new instance of [StreamChatClient] passing the apikey obtained + /// from your project dashboard. + final client = StreamChatClient( + 's2dxdhpxd94g', + logLevel: Level.INFO, + ); + + /// 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. + /// + /// Please see the following for more information: + /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ + await client.connectUser( + User(id: 'super-band-9'), + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' + '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + ); + + final channel = client.channel('messaging', id: 'godevs'); + + await channel.watch(); + + runApp( + MyApp( + client: client, + channel: channel, + ), + ); +} + +/// Example application using Stream Chat Flutter widgets. +/// +/// Stream Chat Flutter is a set of Flutter widgets which provide full chat +/// functionalities for building Flutter applications using Stream. If you'd +/// prefer using minimal wrapper widgets for your app, please see our other +/// package, `stream_chat_flutter_core`. +class MyApp extends StatelessWidget { + /// Example using Stream's Flutter package. + /// + /// If you'd prefer using minimal wrapper widgets for your app, please see + /// our other package, `stream_chat_flutter_core`. + const MyApp({ + Key? key, + required this.client, + required this.channel, + }) : super(key: key); + + /// Instance of Stream Client. + /// + /// Stream's [StreamChatClient] can be used to connect to our servers and + /// set the default user for the application. Performing these actions + /// trigger a websocket connection allowing for real-time updates. + final StreamChatClient client; + + /// Instance of the Channel + final Channel channel; + + @override + Widget build(BuildContext context) => MaterialApp( + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + // Add all the supported locales + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('it'), + ], + // Add GlobalStreamChatLocalizations.delegates + localizationsDelegates: GlobalStreamChatLocalizations.delegates, + builder: (context, widget) => StreamChat( + client: client, + child: widget, + ), + home: StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ); +} + +/// A list of messages sent in the current channel. +/// +/// This is implemented using [MessageListView], a widget that provides query +/// functionalities fetching the messages from the api and showing them in a +/// listView. +class ChannelPage extends StatelessWidget { + /// Creates the page that shows the list of messages + const ChannelPage({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: const [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ); +} diff --git a/packages/stream_chat_localizations/example/lib/override_lang.dart b/packages/stream_chat_localizations/example/lib/override_lang.dart new file mode 100644 index 00000000..0e45019a --- /dev/null +++ b/packages/stream_chat_localizations/example/lib/override_lang.dart @@ -0,0 +1,142 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_localizations/stream_chat_localizations.dart'; + +class _CustomStreamChatLocalizationsDelegate + extends LocalizationsDelegate { + const _CustomStreamChatLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) => locale.languageCode == 'en'; + + @override + Future load(Locale locale) => + SynchronousFuture(CustomStreamChatLocalizationsEn()); + + @override + bool shouldReload(_CustomStreamChatLocalizationsDelegate old) => false; +} + +/// Customized translations for English ('en') +class CustomStreamChatLocalizationsEn extends StreamChatLocalizationsEn { + /// A [LocalizationsDelegate] for [StreamChatLocalizationsEn]. + static const delegate = _CustomStreamChatLocalizationsDelegate(); + + @override + String get launchUrlError => 'My custom error'; +} + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + /// Create a new instance of [StreamChatClient] passing the apikey obtained + /// from your project dashboard. + final client = StreamChatClient( + 's2dxdhpxd94g', + logLevel: Level.INFO, + ); + + /// 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. + /// + /// Please see the following for more information: + /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ + await client.connectUser( + User(id: 'super-band-9'), + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' + '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + ); + + final channel = client.channel('messaging', id: 'godevs'); + + await channel.watch(); + + runApp( + MyApp( + client: client, + channel: channel, + ), + ); +} + +/// Example application using Stream Chat Flutter widgets. +/// +/// Stream Chat Flutter is a set of Flutter widgets which provide full chat +/// functionalities for building Flutter applications using Stream. If you'd +/// prefer using minimal wrapper widgets for your app, please see our other +/// package, `stream_chat_flutter_core`. +class MyApp extends StatelessWidget { + /// Example using Stream's Flutter package. + /// + /// If you'd prefer using minimal wrapper widgets for your app, please see + /// our other package, `stream_chat_flutter_core`. + const MyApp({ + Key? key, + required this.client, + required this.channel, + }) : super(key: key); + + /// Instance of Stream Client. + /// + /// Stream's [StreamChatClient] can be used to connect to our servers and + /// set the default user for the application. Performing these actions + /// trigger a websocket connection allowing for real-time updates. + final StreamChatClient client; + + /// Instance of the Channel + final Channel channel; + + @override + Widget build(BuildContext context) => MaterialApp( + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + // Add all the supported locales + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('it'), + ], + // Add overridden "CustomStreamChatLocalizationsEn.delegate" along with + // "GlobalStreamChatLocalizations.delegates" + localizationsDelegates: const [ + CustomStreamChatLocalizationsEn.delegate, + ...GlobalStreamChatLocalizations.delegates, + ], + builder: (context, widget) => StreamChat( + client: client, + child: widget, + ), + home: StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ); +} + +/// A list of messages sent in the current channel. +/// +/// This is implemented using [MessageListView], a widget that provides query +/// functionalities fetching the messages from the api and showing them in a +/// listView. +class ChannelPage extends StatelessWidget { + /// Creates the page that shows the list of messages + const ChannelPage({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: const ChannelHeader(), + body: Column( + children: const [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ); +} diff --git a/packages/stream_chat_localizations/example/pubspec.yaml b/packages/stream_chat_localizations/example/pubspec.yaml new file mode 100644 index 00000000..a139831e --- /dev/null +++ b/packages/stream_chat_localizations/example/pubspec.yaml @@ -0,0 +1,26 @@ +name: example +description: A new Flutter project. + +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + cupertino_icons: ^1.0.3 + flutter: + sdk: flutter + stream_chat_localizations: + path: ../ + +dependency_overrides: + stream_chat_flutter: + path: ../../stream_chat_flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true \ No newline at end of file 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..bb2d791f --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -0,0 +1,166 @@ +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_fr.dart'; + +part 'stream_chat_localizations_it.dart'; + +part 'stream_chat_localizations_hi.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', + 'hi', + 'fr', + 'it', +}; + +/// 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) { + final languageCode = locale.languageCode; + assert( + kStreamChatSupportedLanguages.contains(languageCode), + 'getStreamChatTranslation() called for unsupported locale "$locale"', + ); + switch (locale.languageCode) { + case 'en': + return const StreamChatLocalizationsEn(); + case 'hi': + return const StreamChatLocalizationsHi(); + case 'fr': + return const StreamChatLocalizationsFr(); + case 'it': + return const StreamChatLocalizationsIt(); + } +} + +/// 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 +/// // ... +/// ], +/// // ... +/// ) +/// ``` +/// +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; + + // ignore: unused_field + final String _localeName; + + /// 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 + /// ], + /// // ... + /// ) + /// ``` + static const List delegates = [ + GlobalStreamChatLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; +} + +class _StreamChatLocalizationsDelegate + extends LocalizationsDelegate { + const _StreamChatLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) => + kStreamChatSupportedLanguages.contains(locale.languageCode); + + static final _loadedTranslations = + >{}; + + @override + Future load(Locale locale) { + assert(isSupported(locale), ''); + return _loadedTranslations.putIfAbsent( + locale, + () => SynchronousFuture( + getStreamChatTranslation(locale)!, + ), + ); + } + + @override + bool shouldReload(_StreamChatLocalizationsDelegate old) => false; + + @override + 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..5041d398 --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -0,0 +1,360 @@ +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'; + + @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 is deleted.'; + + @override + String get messageDeletedLabel => 'Message deleted'; + + @override + String get messageReactionsLabel => '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 fileTooLargeAfterCompressionError(double limitInMB) => + 'The file is too large to upload. ' + 'The file size limit is $limitInMB MB. ' + 'We tried compressing it, but it was not enough.'; + + @override + String fileTooLargeError(double limitInMB) => + 'The file is too large to upload. The file size limit is $limitInMB MB.'; + + @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 somethingWentWrongError => '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'; + + String _getDay(DateTime dateTime) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = DateTime(now.year, now.month, now.day - 1); + + final date = DateTime(dateTime.year, dateTime.month, dateTime.day); + + if (date == today) { + return 'today'; + } else if (date == yesterday) { + return 'yesterday'; + } else { + return 'on ${Jiffy(date).MMMd}'; + } + } + + @override + String sentAtText({required DateTime date, required DateTime time}) => + 'Sent ${_getDay(date)} at ${Jiffy(time.toLocal()).format('HH:mm')}'; + + @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 searchingForNetworkText => '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'; + + @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'; + + @override + String get replyToMessageLabel => 'Reply to Message'; +} diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart new file mode 100644 index 00000000..1bbfbc5a --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -0,0 +1,364 @@ +part of 'stream_chat_localizations.dart'; + +/// The translations for French (`fr`). +class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { + /// Create an instance of the translation bundle for French. + const StreamChatLocalizationsFr({String localeName = 'fr'}) + : super(localeName: localeName); + + @override + String get launchUrlError => "Impossible de lancer l'url"; + + @override + String get loadingUsersError => 'Erreur de chargement des utilisateurs'; + + @override + String get noUsersLabel => "Il n'y a pas d'utilisateurs actuellement"; + + @override + String get retryLabel => 'R├йessayer'; + + @override + String get userLastOnlineText => 'Derni├иre fois en ligne'; + + @override + String get userOnlineText => 'En ligne'; + + @override + String userTypingText(Iterable users) { + if (users.isEmpty) return ''; + final first = users.first; + if (users.length == 1) { + return "${first.name} est en train d'├йcrire"; + } + return "${first.name} and ${users.length - 1} sont entrain d'├йcrire"; + } + + @override + String get threadReplyLabel => 'R├йponse au fil de discussion'; + + @override + String get onlyVisibleToYouText => 'Seulement visible par vous'; + + @override + String threadReplyCountText(int count) => + '$count R├йponses au fil de discussion'; + + @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 '├Йpingl├й par vous'; + return '├Йpingl├й par ${pinnedBy.name}'; + } + + @override + String get emptyMessagesText => "Il n'y a pas de messages actuellement"; + + @override + String get genericErrorText => 'Il y a eu un probl├иme'; + + @override + String get loadingMessagesError => 'Erreur de chargement des messages'; + + @override + String resultCountText(int count) => '$count r├йsultats'; + + @override + String get messageDeletedText => 'Ce message a ├йt├й supprim├й.'; + + @override + String get messageDeletedLabel => 'Message supprim├й'; + + @override + String get messageReactionsLabel => 'R├йactions aux messages'; + + @override + String get emptyChatMessagesText => 'Pas encore de chats ici...'; + + @override + String threadSeparatorText(int replyCount) { + if (replyCount == 1) return '1 R├йponse'; + return '$replyCount Replies'; + } + + @override + String get connectedLabel => 'Connect├й'; + + @override + String get disconnectedLabel => 'D├йconnect├й'; + + @override + String get reconnectingLabel => 'Reconnexion...'; + + @override + String get alsoSendAsDirectMessageLabel => + 'Envoyer aussi comme message direct'; + + @override + String get addACommentOrSendLabel => 'Ajouter un commentaire ou envoyer'; + + @override + String get searchGifLabel => 'Recherche de GIFs'; + + @override + String get writeAMessageLabel => '├Йcrire un message'; + + @override + String get instantCommandsLabel => 'Commandes instantan├йes'; + + @override + String fileTooLargeAfterCompressionError(double limitInMB) => + 'Le fichier est trop volumineux pour ├кtre t├йl├йcharg├й. ' + 'La taille maximale des fichiers est de $limitInMB Mo. ' + "Nous avons essay├й de le compresser, mais ce n'├йtait pas suffisant."; + + @override + String fileTooLargeError(double limitInMB) => + 'Le fichier est trop volumineux pour ├кtre t├йl├йcharg├й. ' + 'La taille limite du fichier est de $limitInMB Mo.'; + + @override + String emojiMatchingQueryText(String query) => + 'Emoji qui correspond ├а "$query"'; + + @override + String get addAFileLabel => 'Ajouter un fichier'; + + @override + String get photoFromCameraLabel => "Photo de l'appareil photo"; + + @override + String get uploadAFileLabel => 'Transf├йrer un fichier'; + + @override + String get uploadAPhotoLabel => 'Transf├йrer une photo'; + + @override + String get uploadAVideoLabel => 'Transf├йrer une vid├йo'; + + @override + String get videoFromCameraLabel => 'Vid├йo depuis la camera'; + + @override + String get okLabel => 'OK'; + + @override + String get somethingWentWrongError => 'Quelque chose a mal tourn├й'; + + @override + String get addMoreFilesLabel => "Ajouter d'autres fichiers"; + + @override + String get enablePhotoAndVideoAccessMessage => + "Veuillez autoriser l'acc├иs ├а vos photos" + '\net vid├йos afin de pouvoir les partager avec vos amis.'; + + @override + String get allowGalleryAccessMessage => "Autoriser l'acc├иs ├а votre galerie"; + + @override + String get flagMessageLabel => 'Signaler un message'; + + @override + String get flagMessageQuestion => + 'Voulez-vous envoyer une copie de ce message ├а un' + '\nmod├йrateur pour une enqu├кte plus approfondie ?'; + + @override + String get flagLabel => 'SIGNALER'; + + @override + String get cancelLabel => 'ANNULER'; + + @override + String get flagMessageSuccessfulLabel => 'Message signal├й'; + + @override + String get flagMessageSuccessfulText => + 'Ce message a ├йt├й signal├й ├а un mod├йrateur.'; + + @override + String get deleteLabel => 'SUPPRIMER'; + + @override + String get deleteMessageLabel => 'Supprimer le message'; + + @override + String get deleteMessageQuestion => + '├Кtes-vous s├╗r de vouloir supprimer d├йfinitivement ce\nmessage ?'; + + @override + String get operationCouldNotBeCompletedText => + "L'op├йration n'a pas pu ├кtre termin├йe."; + + @override + String get replyLabel => 'R├йpondre'; + + @override + String togglePinUnpinText({required bool pinned}) { + if (pinned) return 'D├йtacher de la conversation'; + return 'Attacher ├а la conversation'; + } + + @override + String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) { + if (isDeleteFailed) return 'Retenter de supprimer le message'; + return 'Supprimer le message'; + } + + @override + String get copyMessageLabel => 'Copier le message'; + + @override + String get editMessageLabel => 'Modifier le message'; + + @override + String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) { + if (isUpdateFailed) return 'Renvoyer le message modifi├й'; + return 'Renvoyer'; + } + + @override + String get photosLabel => 'Photos'; + + String _getDay(DateTime dateTime) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = DateTime(now.year, now.month, now.day - 1); + + final date = DateTime(dateTime.year, dateTime.month, dateTime.day); + + if (date == today) { + return "aujourd'hui"; + } else if (date == yesterday) { + return 'hier'; + } else { + return 'le ${Jiffy(date).MMMd}'; + } + } + + @override + String sentAtText({required DateTime date, required DateTime time}) => + 'Envoy├й ${_getDay(date)} ├а ${Jiffy(time.toLocal()).format('HH:mm')}'; + + @override + String get todayLabel => "Aujourd'hui"; + + @override + String get yesterdayLabel => 'Hier'; + + @override + String get channelIsMutedText => 'Le canal est coup├й'; + + @override + String get noTitleText => 'Aucun titre'; + + @override + String get letsStartChattingLabel => 'Commen├зons ├а discuter !'; + + @override + String get sendingFirstMessageLabel => + "Que diriez-vous d'envoyer votre premier message ├а un ami ?"; + + @override + String get startAChatLabel => 'Commencer une discussion'; + + @override + String get loadingChannelsError => 'Erreur lors du chargement des canaux'; + + @override + String get deleteConversationLabel => 'Supprimer la conversation'; + + @override + String get deleteConversationQuestion => + 'Vous ├кtes s├╗r de vouloir supprimer cette conversation ?'; + + @override + String get streamChatLabel => 'Stream Chat'; + + @override + String get searchingForNetworkText => 'Recherche de r├йseau'; + + @override + String get offlineLabel => 'Hors ligne...'; + + @override + String get tryAgainLabel => 'Essayer ├а nouveau'; + + @override + String membersCountText(int count) { + if (count == 1) return '1 Membre'; + return '$count Membres'; + } + + @override + String watchersCountText(int count) { + if (count == 1) return '1 En ligne'; + return '$count En ligne'; + } + + @override + String get viewInfoLabel => 'Voir les informations'; + + @override + String get leaveGroupLabel => 'Quitter le Group'; + + @override + String get leaveLabel => 'QUITTER'; + + @override + String get leaveConversationLabel => 'Quitter la conversation'; + + @override + String get leaveConversationQuestion => + 'Etes-vous s├╗r de vouloir quitter cette conversation ?'; + + @override + String get showInChatLabel => 'Montrer dans le Chat'; + + @override + String get saveImageLabel => "Sauvegarder l'image"; + + @override + String get saveVideoLabel => 'Sauvegarder la vid├йo'; + + @override + String get uploadErrorLabel => 'ERREUR DE TRANSFERT'; + + @override + String get giphyLabel => 'Giphy'; + + @override + String get shuffleLabel => 'M├йlanger'; + + @override + String get sendLabel => 'Envoyer'; + + @override + String get withText => 'avec'; + + @override + String get inText => 'dans'; + + @override + String get youText => 'Vous'; + + @override + String get ofText => 'de'; + + @override + String get fileText => 'Fichier'; + + @override + String get replyToMessageLabel => 'R├йpondre au Message'; +} 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..850e481b --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -0,0 +1,359 @@ +part of 'stream_chat_localizations.dart'; + +/// The translations for Hindi (`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 messageReactionsLabel => 'рд╕рдВрджреЗрд╢ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛рдПрдВ'; + + @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 fileTooLargeAfterCompressionError(double limitInMB) => + 'рдлрд╝рд╛рдЗрд▓ рдЕрдкрд▓реЛрдб рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдмрд╣реБрдд рдмрдбрд╝реА рд╣реИред ' + 'рдлрд╝рд╛рдЗрд▓ рдЖрдХрд╛рд░ рд╕реАрдорд╛ $limitInMB MB рд╣реИред ' + 'рд╣рдордиреЗ рдЗрд╕реЗ рдХрдВрдкреНрд░реЗрд╕ рдХрд░рдиреЗ рдХреА рдХреЛрд╢рд┐рд╢ рдХреА, рд▓реЗрдХрд┐рди рдпрд╣ рдХрд╛рдлреА рдирд╣реАрдВ рдерд╛ред'; + + @override + String fileTooLargeError(double limitInMB) => + 'рдлрд╝рд╛рдЗрд▓ рдЕрдкрд▓реЛрдб рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдмрд╣реБрдд рдмрдбрд╝реА рд╣реИред рдлрд╝рд╛рдЗрд▓ рдЖрдХрд╛рд░ рд╕реАрдорд╛ $limitInMB MB рд╣реИред'; + + @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 somethingWentWrongError => 'рд▓реЛрдб рдХрд░рдиреЗ рдореЗрдВ рд╕рдорд╕реНрдпрд╛'; + + @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 = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = DateTime(now.year, now.month, now.day - 1); + + final date = DateTime(dateTime.year, dateTime.month, dateTime.day); + + if (date == today) { + return 'рдЖрдЬ'; + } else if (date == yesterday) { + return 'рдХрд▓'; + } else { + return '${Jiffy(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 searchingForNetworkText => 'рдиреЗрдЯрд╡рд░реНрдХ рдЦреЛрдЬ рд░рд╣реЗ рд╣реИрдВ'; + + @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 => 'рдлрд╝рд╛рдЗрд▓'; + + @override + String get replyToMessageLabel => 'рд╕рдВрджреЗрд╢ рдХрд╛ рдЬрд╡рд╛рдм'; +} diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart new file mode 100644 index 00000000..8b2e1692 --- /dev/null +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -0,0 +1,361 @@ +part of 'stream_chat_localizations.dart'; + +/// The translations for Italian (`it`). +class StreamChatLocalizationsIt extends GlobalStreamChatLocalizations { + /// Create an instance of the translation bundle for Italian. + const StreamChatLocalizationsIt({String localeName = 'it'}) + : super(localeName: localeName); + + @override + String get launchUrlError => 'Impossibile aprire l\'url'; + + @override + String get loadingUsersError => 'Errore durante il carimento degli utenti'; + + @override + String get noUsersLabel => 'Non c\'├й nessun utente al momento'; + + @override + String get retryLabel => 'Riprova'; + + @override + String get userLastOnlineText => 'Ultimo accesso'; + + @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} sta scrivendo'; + } + return '${first.name} e altri ${users.length - 1} stanno scrivendo'; + } + + @override + String get threadReplyLabel => 'Rispondi nel thread'; + + @override + String get onlyVisibleToYouText => 'Visible solo a te'; + + @override + String threadReplyCountText(int count) => '$count risposte al thread'; + + @override + String attachmentsUploadProgressText({ + required int remaining, + required int total, + }) => + 'Caricamento $remaining/$total ...'; + + @override + String pinnedByUserText({ + required User pinnedBy, + required User currentUser, + }) { + final pinnedByCurrentUser = currentUser.id == pinnedBy.id; + if (pinnedByCurrentUser) return 'Messo in evidenza da te'; + return 'Messo in evidenza da ${pinnedBy.name}'; + } + + @override + String get emptyMessagesText => 'Non c\'├й nessun messaggio al momento'; + + @override + String get genericErrorText => 'Qualcosa ├и andato storto'; + + @override + String get loadingMessagesError => + 'Errore durante il caricamento dei messaggi'; + + @override + String resultCountText(int count) => '$count risultati'; + + @override + String get messageDeletedText => 'Questo messaggio ├и stato eliminato'; + + @override + String get messageDeletedLabel => 'Messaggio cancellato'; + + @override + String get messageReactionsLabel => 'Reazioni al messaggio'; + + @override + String get emptyChatMessagesText => 'Nessuna conversazione al momento...'; + + @override + String threadSeparatorText(int replyCount) { + if (replyCount == 1) return '1 risposta'; + return '$replyCount risposte'; + } + + @override + String get connectedLabel => 'Connesso'; + + @override + String get disconnectedLabel => 'Disconnesso'; + + @override + String get reconnectingLabel => 'Riconnessione in corso...'; + + @override + String get alsoSendAsDirectMessageLabel => + 'Manda anche come messaggio diretto'; + + @override + String get addACommentOrSendLabel => 'Aggiungi un commento o invia'; + + @override + String get searchGifLabel => 'Cerca una GIF'; + + @override + String get writeAMessageLabel => 'Scrivi un messaggio'; + + @override + String get instantCommandsLabel => 'Commandi istantanei'; + + @override + String fileTooLargeAfterCompressionError(double limitInMB) => + 'Il file ├и troppo grande per essere caricato. ' + 'Il file eccede il limite di $limitInMB MB. ' + 'Abbiamo provato a comprimerlo, ma non ├и stato abbastanza.'; + + @override + String fileTooLargeError(double limitInMB) => ''' +Il file ├и troppo grande per essere caricato. Il limite ├и di $limitInMB MB.'''; + + @override + String emojiMatchingQueryText(String query) => 'Emoji per "$query"'; + + @override + String get addAFileLabel => 'Aggiungi un file'; + + @override + String get photoFromCameraLabel => 'Immagine dalla fotocamera'; + + @override + String get uploadAFileLabel => 'Carica un file'; + + @override + String get uploadAPhotoLabel => 'Carica una foto'; + + @override + String get uploadAVideoLabel => 'Carica un video'; + + @override + String get videoFromCameraLabel => 'Video dalla fotocamera'; + + @override + String get okLabel => 'Ok'; + + @override + String get somethingWentWrongError => 'Qualcosa ├и andato storto'; + + @override + String get addMoreFilesLabel => 'Aggiungi altri file'; + + @override + String get enablePhotoAndVideoAccessMessage => + 'Per favore attiva l\'accesso alle foto' + '\ne ai video cos├н potrai condividerli con i tuoi amici.'; + + @override + String get allowGalleryAccessMessage => 'Permetti l\'accesso alla galleria'; + + @override + String get flagMessageLabel => 'Segnala messaggio'; + + @override + String get flagMessageQuestion => 'Vuoi mandare una copia di questo messaggio' + '\nad un moderatore?'; + + @override + String get flagLabel => 'SEGNALA'; + + @override + String get cancelLabel => 'ANNULLA'; + + @override + String get flagMessageSuccessfulLabel => 'Messaggio segnalato'; + + @override + String get flagMessageSuccessfulText => + 'Questo messaggio ├и stato segnalato ad un moderatore.'; + + @override + String get deleteLabel => 'CANCELLA'; + + @override + String get deleteMessageLabel => 'Cancella messaggio'; + + @override + String get deleteMessageQuestion => + 'Sei sicuro di voler definitivamente cancellare questo\nmessaggio?'; + + @override + String get operationCouldNotBeCompletedText => + 'Non ├и stato possibile completare questa operazione.'; + + @override + String get replyLabel => 'Rispondi'; + + @override + String togglePinUnpinText({required bool pinned}) { + if (pinned) return 'Rimuovi dagli elementi in evidenza'; + return 'Metti in evidenza'; + } + + @override + String toggleDeleteRetryDeleteMessageText({required bool isDeleteFailed}) { + if (isDeleteFailed) return 'Riprova a cancellare il messaggio'; + return 'Cancella il messaggio'; + } + + @override + String get copyMessageLabel => 'Copia messaggio'; + + @override + String get editMessageLabel => 'Modifica messaggio'; + + @override + String toggleResendOrResendEditedMessage({required bool isUpdateFailed}) { + if (isUpdateFailed) return 'Riprova modifica messaggio'; + return 'Riprova'; + } + + @override + String get photosLabel => 'Foto'; + + String _getDay(DateTime dateTime) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final yesterday = DateTime(now.year, now.month, now.day - 1); + + final date = DateTime(dateTime.year, dateTime.month, dateTime.day); + + if (date == today) { + return 'oggi'; + } else if (date == yesterday) { + return 'ieri'; + } else { + return 'il ${Jiffy(date).MMMd}'; + } + } + + @override + String sentAtText({required DateTime date, required DateTime time}) => + "Inviato ${_getDay(date)} alle ${Jiffy(time.toLocal()).format('HH:mm')}"; + + @override + String get todayLabel => 'Oggi'; + + @override + String get yesterdayLabel => 'Ieri'; + + @override + String get channelIsMutedText => 'Il canale ├и mutato'; + + @override + String get noTitleText => 'Nessun titolo'; + + @override + String get letsStartChattingLabel => 'Inizia una conversazione!'; + + @override + String get sendingFirstMessageLabel => + 'Che ne dici di mandare il tuo primo messaggio ad un amico?'; + + @override + String get startAChatLabel => 'Inizia una conversazione'; + + @override + String get loadingChannelsError => 'Errore durante il caricamento dei canali'; + + @override + String get deleteConversationLabel => 'Elemina conversazione'; + + @override + String get deleteConversationQuestion => + 'Sei sicuro di voler eliminare questa conversazione?'; + + @override + String get streamChatLabel => 'Stream Chat'; + + @override + String get searchingForNetworkText => 'Cercando una connessione'; + + @override + String get offlineLabel => 'Offline...'; + + @override + String get tryAgainLabel => 'Riprova'; + + @override + String membersCountText(int count) { + if (count == 1) return '1 membro'; + return '$count membri'; + } + + @override + String watchersCountText(int count) { + if (count == 1) return '1 Online'; + return '$count Online'; + } + + @override + String get viewInfoLabel => 'Vedi info'; + + @override + String get leaveGroupLabel => 'Esci dal gruppo'; + + @override + String get leaveLabel => 'ESCI'; + + @override + String get leaveConversationLabel => 'Esci dalla conversazione'; + + @override + String get leaveConversationQuestion => + 'Sei sicuro di voler lasciare questa conversazione?'; + + @override + String get showInChatLabel => 'Mostra nella chat'; + + @override + String get saveImageLabel => 'Salva immagine'; + + @override + String get saveVideoLabel => 'Salva video'; + + @override + String get uploadErrorLabel => 'ERRORE DURANTE IL CARICAMENTO'; + + @override + String get giphyLabel => 'Giphy'; + + @override + String get shuffleLabel => 'Shuffle'; + + @override + String get sendLabel => 'Invia'; + + @override + String get withText => 'con'; + + @override + String get inText => 'in'; + + @override + String get youText => 'te'; + + @override + String get ofText => 'di'; + + @override + String get fileText => 'file'; + + @override + String get replyToMessageLabel => 'Rispondi al messaggio'; +} 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..0cfb60ba --- /dev/null +++ b/packages/stream_chat_localizations/lib/stream_chat_localizations.dart @@ -0,0 +1,9 @@ +/// Localizations for the StreamChat Flutter library. +library stream_chat_localization; + +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 new file mode 100644 index 00000000..01b027a5 --- /dev/null +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -0,0 +1,21 @@ +name: stream_chat_localizations +description: The Official localizations for Stream Chat Flutter, a service for building chat applications +version: 1.0.0 +homepage: https://github.com/GetStream/stream-chat-flutter +repository: https://github.com/GetStream/stream-chat-flutter +issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + stream_chat_flutter: ^2.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/stream_chat_localizations/test/basics_test.dart b/packages/stream_chat_localizations/test/basics_test.dart new file mode 100644 index 00000000..c1315a9a --- /dev/null +++ b/packages/stream_chat_localizations/test/basics_test.dart @@ -0,0 +1,109 @@ +// ignore_for_file: omit_local_variable_types + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_localizations/src/stream_chat_localizations.dart'; + +void main() { + testWidgets('Nested Localizations', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + // Creates the outer Localizations widget. + home: ListView( + children: [ + const LocalizationTracker(key: ValueKey('outer')), + Localizations( + locale: const Locale('hi'), + delegates: GlobalStreamChatLocalizations.delegates, + child: const LocalizationTracker(key: ValueKey('inner')), + ), + ], + ), + )); + + final LocalizationTrackerState outerTracker = tester.state( + find.byKey(const ValueKey('outer'), skipOffstage: false)); + expect(outerTracker.captionFontSize, 12.0); + final LocalizationTrackerState innerTracker = tester.state( + find.byKey(const ValueKey('inner'), skipOffstage: false)); + expect(innerTracker.captionFontSize, 13.0); + }); + + testWidgets( + 'Localizations is compatible with ChangeNotifier.dispose() called ' + 'during didChangeDependencies', + (WidgetTester tester) async { + // PageView calls ScrollPosition.dispose() during didChangeDependencies. + await tester.pumpWidget(MaterialApp( + supportedLocales: const [ + Locale('en', 'US'), + Locale('hi', 'IN'), + ], + localizationsDelegates: const [ + DummyLocalizations.delegate, + GlobalStreamChatLocalizations.delegate, + ], + home: PageView(), + )); + + await tester.binding.setLocale('hi', 'IN'); + await tester.pump(); + await tester.pumpWidget(Container()); + }, + ); + + testWidgets('Locale without countryCode', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/pull/16782 + await tester.pumpWidget(MaterialApp( + localizationsDelegates: const >[ + GlobalStreamChatLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en', 'US'), + Locale('hi'), + ], + home: Container(), + )); + + await tester.binding.setLocale('hi', ''); + await tester.pump(); + await tester.binding.setLocale('en', 'US'); + await tester.pump(); + }); +} + +/// A localizations delegate that does not contain any useful data, and is only +/// used to trigger didChangeDependencies upon locale change. +class _DummyLocalizationsDelegate + extends LocalizationsDelegate { + const _DummyLocalizationsDelegate(); + + @override + Future load(Locale locale) async => DummyLocalizations(); + + @override + bool isSupported(Locale locale) => true; + + @override + bool shouldReload(_DummyLocalizationsDelegate old) => true; +} + +class DummyLocalizations { + static const delegate = _DummyLocalizationsDelegate(); +} + +class LocalizationTracker extends StatefulWidget { + const LocalizationTracker({Key? key}) : super(key: key); + + @override + State createState() => LocalizationTrackerState(); +} + +class LocalizationTrackerState extends State { + late double captionFontSize; + + @override + Widget build(BuildContext context) { + captionFontSize = Theme.of(context).textTheme.caption!.fontSize!; + return Container(); + } +} diff --git a/packages/stream_chat_localizations/test/override_test.dart b/packages/stream_chat_localizations/test/override_test.dart new file mode 100644 index 00000000..3e134b6f --- /dev/null +++ b/packages/stream_chat_localizations/test/override_test.dart @@ -0,0 +1,276 @@ +// ignore_for_file: prefer_expression_function_bodies + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_localizations/src/stream_chat_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class FooStreamChatLocalizations extends StreamChatLocalizationsEn { + FooStreamChatLocalizations( + Locale localeName, + this.launchUrlError, + ) : super(localeName: localeName.toString()); + + @override + final String launchUrlError; +} + +class FooStreamChatLocalizationsDelegate + extends LocalizationsDelegate { + const FooStreamChatLocalizationsDelegate({ + this.supportedLanguage = 'en', + this.launchUrlError = 'foo', + }); + + final String supportedLanguage; + final String launchUrlError; + + @override + bool isSupported(Locale locale) => + supportedLanguage == 'allLanguages' || + locale.languageCode == supportedLanguage; + + @override + Future load(Locale locale) => + SynchronousFuture( + FooStreamChatLocalizations(locale, launchUrlError), + ); + + @override + bool shouldReload(FooStreamChatLocalizationsDelegate old) => false; +} + +Widget buildFrame({ + Locale? locale, + Iterable delegates = + GlobalStreamChatLocalizations.delegates, + required WidgetBuilder buildContent, + LocaleResolutionCallback? localeResolutionCallback, + Iterable supportedLocales = const [ + Locale('en', 'US'), + Locale('hi', 'IN'), + ], +}) => + MaterialApp( + color: const Color(0xFFFFFFFF), + locale: locale, + supportedLocales: supportedLocales, + localizationsDelegates: delegates, + localeResolutionCallback: localeResolutionCallback, + onGenerateRoute: (RouteSettings settings) => MaterialPageRoute( + builder: (BuildContext context) => buildContent(context)), + ); + +void main() { + testWidgets( + 'Locale fallbacks', + (WidgetTester tester) async { + final Key textKey = UniqueKey(); + + await tester.pumpWidget( + buildFrame( + buildContent: (BuildContext context) => Text( + StreamChatLocalizations.of(context)!.launchUrlError, + key: textKey, + ), + ), + ); + + expect( + tester.widget(find.byKey(textKey)).data, + 'Cannot launch the url', + ); + + // Unrecognized locale falls back to 'en' + await tester.binding.setLocale('foo', 'BAR'); + await tester.pump(); + expect( + tester.widget(find.byKey(textKey)).data, + 'Cannot launch the url', + ); + + // Indian hindi locale, falls back to just 'hi' + await tester.binding.setLocale('hi', 'IN'); + await tester.pump(); + expect( + tester.widget(find.byKey(textKey)).data, + 'рдпреВрдЖрд░рдПрд▓ рд▓реЙрдиреНрдЪ рдирд╣реАрдВ рдХрд░ рд╕рдХрддреЗ', + ); + }, + ); + + testWidgets( + "Localizations.override widget tracks parent's locale", + (WidgetTester tester) async { + Widget buildLocaleFrame(Locale locale) => buildFrame( + locale: locale, + supportedLocales: [locale], + buildContent: (BuildContext context) => Localizations.override( + context: context, + child: Builder( + builder: (BuildContext context) { + // No StreamChatLocalizations are defined for the first + // Localizations ancestor, so we should get the values from + // the default one, i.e. the one created by WidgetsApp via + // the LocalizationsDelegate provided by MaterialApp. + return Text( + StreamChatLocalizations.of(context)!.launchUrlError, + ); + }, + ), + ), + ); + + await tester.pumpWidget(buildLocaleFrame(const Locale('en', 'US'))); + expect(find.text('Cannot launch the url'), findsOneWidget); + + await tester.pumpWidget(buildLocaleFrame(const Locale('hi', 'IN'))); + expect(find.text('рдпреВрдЖрд░рдПрд▓ рд▓реЙрдиреНрдЪ рдирд╣реАрдВ рдХрд░ рд╕рдХрддреЗ'), findsOneWidget); + }, + ); + + testWidgets('Localizations.override widget with hardwired locale', + (WidgetTester tester) async { + Widget buildLocaleFrame(Locale locale) => buildFrame( + locale: locale, + buildContent: (BuildContext context) { + return Localizations.override( + context: context, + locale: const Locale('en', 'US'), + child: Builder( + builder: (BuildContext context) { + // No StreamChatLocalizations are defined for the first + // Localizations ancestor, so we should get the values from + // the default one, i.e. the one created by WidgetsApp via + // the LocalizationsDelegate provided by MaterialApp. + return Text( + StreamChatLocalizations.of(context)!.launchUrlError, + ); + }, + ), + ); + }, + ); + + await tester.pumpWidget(buildLocaleFrame(const Locale('en', 'US'))); + expect(find.text('Cannot launch the url'), findsOneWidget); + + await tester.pumpWidget(buildLocaleFrame(const Locale('hi', 'IN'))); + expect(find.text('Cannot launch the url'), findsOneWidget); + }); + + testWidgets( + 'MaterialApp adds StreamChatLocalizations for additional languages', + (WidgetTester tester) async { + final Key textKey = UniqueKey(); + + await tester.pumpWidget(buildFrame( + delegates: >[ + GlobalStreamChatLocalizations.delegate, + const FooStreamChatLocalizationsDelegate( + supportedLanguage: 'fr', + launchUrlError: "Impossible de lancer l'url", + ), + const FooStreamChatLocalizationsDelegate( + supportedLanguage: 'de', + launchUrlError: 'Kann die URL nicht starten', + ), + ], + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('de'), + ], + buildContent: (BuildContext context) => Text( + StreamChatLocalizations.of(context)!.launchUrlError, + key: textKey, + ), + )); + + expect( + tester.widget(find.byKey(textKey)).data, + 'Cannot launch the url', + ); + + await tester.binding.setLocale('hi', 'IN'); + await tester.pump(); + expect(find.text('рдпреВрдЖрд░рдПрд▓ рд▓реЙрдиреНрдЪ рдирд╣реАрдВ рдХрд░ рд╕рдХрддреЗ'), findsOneWidget); + + await tester.binding.setLocale('fr', 'CA'); + await tester.pump(); + expect(find.text("Impossible de lancer l'url"), findsOneWidget); + + await tester.binding.setLocale('de', 'DE'); + await tester.pump(); + expect(find.text('Kann die URL nicht starten'), findsOneWidget); + }, + ); + + testWidgets( + 'MaterialApp overrides MaterialLocalizations for all locales', + (WidgetTester tester) async { + final Key textKey = UniqueKey(); + + await tester.pumpWidget(buildFrame( + // Accept whatever locale we're given + localeResolutionCallback: + (Locale? locale, Iterable supportedLocales) => locale, + delegates: [ + const FooStreamChatLocalizationsDelegate( + supportedLanguage: 'allLanguages', + ), + ], + buildContent: (BuildContext context) { + // Should always be 'foo', no matter what the locale is + return Text( + StreamChatLocalizations.of(context)!.launchUrlError, + key: textKey, + ); + }, + )); + + expect(tester.widget(find.byKey(textKey)).data, 'foo'); + + await tester.binding.setLocale('zh', 'CN'); + await tester.pump(); + expect(find.text('foo'), findsOneWidget); + + await tester.binding.setLocale('de', 'DE'); + await tester.pump(); + expect(find.text('foo'), findsOneWidget); + }, + ); + + testWidgets( + 'MaterialApp overrides MaterialLocalizations for default locale', + (WidgetTester tester) async { + final Key textKey = UniqueKey(); + + await tester.pumpWidget(buildFrame( + delegates: [ + const FooStreamChatLocalizationsDelegate(), + ], + // supportedLocales not specified, so all locales resolve to 'en' + buildContent: (BuildContext context) => Text( + StreamChatLocalizations.of(context)!.launchUrlError, + key: textKey, + ), + )); + + // Unsupported locale '_' (the widget tester's default) resolves to 'en'. + expect(tester.widget(find.byKey(textKey)).data, 'foo'); + + // Unsupported locale 'zh' resolves to 'en'. + await tester.binding.setLocale('zh', 'CN'); + await tester.pump(); + expect(find.text('foo'), findsOneWidget); + + // Unsupported locale 'de' resolves to 'en'. + await tester.binding.setLocale('de', 'DE'); + await tester.pump(); + expect(find.text('foo'), findsOneWidget); + }, + ); +} diff --git a/packages/stream_chat_localizations/test/translations_test.dart b/packages/stream_chat_localizations/test/translations_test.dart new file mode 100644 index 00000000..0e621159 --- /dev/null +++ b/packages/stream_chat_localizations/test/translations_test.dart @@ -0,0 +1,199 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_localizations/src/stream_chat_localizations.dart'; + +void main() { + 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); + expect(localizations.loadingUsersError, isNotNull); + expect(localizations.noUsersLabel, isNotNull); + expect(localizations.retryLabel, isNotNull); + expect(localizations.userLastOnlineText, isNotNull); + expect(localizations.userOnlineText, isNotNull); + expect(localizations.userOnlineText, isNotNull); + // no users + expect(localizations.userTypingText([]), isNotNull); + // single user + expect(localizations.userTypingText([User(id: 'test-id')]), isNotNull); + // multiple users + expect( + localizations.userTypingText([ + User(id: 'test-id-1'), + User(id: 'test-id-2'), + ]), + isNotNull, + ); + expect(localizations.threadReplyLabel, isNotNull); + expect(localizations.onlyVisibleToYouText, isNotNull); + expect(localizations.threadReplyCountText(3), isNotNull); + expect( + localizations.attachmentsUploadProgressText(remaining: 3, total: 10), + isNotNull, + ); + expect( + localizations.pinnedByUserText( + pinnedBy: User(id: 'pinned-by-user-id'), + currentUser: OwnUser(id: 'current-user-id'), + ), + isNotNull, + ); + expect(localizations.emptyMessagesText, isNotNull); + expect(localizations.genericErrorText, isNotNull); + expect(localizations.loadingMessagesError, isNotNull); + expect(localizations.resultCountText(3), isNotNull); + expect(localizations.messageDeletedText, isNotNull); + expect(localizations.messageDeletedLabel, isNotNull); + expect(localizations.messageReactionsLabel, isNotNull); + expect(localizations.emptyChatMessagesText, isNotNull); + expect(localizations.threadSeparatorText(3), isNotNull); + expect(localizations.connectedLabel, isNotNull); + expect(localizations.disconnectedLabel, isNotNull); + expect(localizations.reconnectingLabel, isNotNull); + expect(localizations.alsoSendAsDirectMessageLabel, isNotNull); + expect(localizations.addACommentOrSendLabel, isNotNull); + expect(localizations.searchGifLabel, isNotNull); + expect(localizations.writeAMessageLabel, isNotNull); + expect(localizations.instantCommandsLabel, isNotNull); + expect(localizations.fileTooLargeAfterCompressionError(33), isNotNull); + expect(localizations.fileTooLargeError(33), isNotNull); + expect(localizations.emojiMatchingQueryText('sahil'), isNotNull); + expect(localizations.addAFileLabel, isNotNull); + expect(localizations.photoFromCameraLabel, isNotNull); + expect(localizations.uploadAFileLabel, isNotNull); + expect(localizations.uploadAPhotoLabel, isNotNull); + expect(localizations.uploadAVideoLabel, isNotNull); + expect(localizations.videoFromCameraLabel, isNotNull); + expect(localizations.okLabel, isNotNull); + expect(localizations.somethingWentWrongError, isNotNull); + expect(localizations.addMoreFilesLabel, isNotNull); + expect(localizations.enablePhotoAndVideoAccessMessage, isNotNull); + expect(localizations.allowGalleryAccessMessage, isNotNull); + expect(localizations.flagMessageLabel, isNotNull); + expect(localizations.flagMessageQuestion, isNotNull); + expect(localizations.flagLabel, isNotNull); + expect(localizations.cancelLabel, isNotNull); + expect(localizations.flagMessageSuccessfulLabel, isNotNull); + expect(localizations.flagMessageSuccessfulText, isNotNull); + expect(localizations.deleteLabel, isNotNull); + expect(localizations.deleteMessageLabel, isNotNull); + expect(localizations.deleteMessageQuestion, isNotNull); + expect(localizations.operationCouldNotBeCompletedText, isNotNull); + expect(localizations.replyLabel, isNotNull); + // pinned + expect(localizations.togglePinUnpinText(pinned: true), isNotNull); + // un-pinned + expect(localizations.togglePinUnpinText(pinned: false), isNotNull); + // delete-failed + expect( + localizations.toggleDeleteRetryDeleteMessageText(isDeleteFailed: true), + isNotNull, + ); + // first-delete + expect( + localizations.toggleDeleteRetryDeleteMessageText(isDeleteFailed: false), + isNotNull, + ); + expect(localizations.copyMessageLabel, isNotNull); + expect(localizations.editMessageLabel, isNotNull); + // resend-failed + expect( + localizations.toggleResendOrResendEditedMessage(isUpdateFailed: true), + isNotNull, + ); + // first resend + expect( + localizations.toggleResendOrResendEditedMessage(isUpdateFailed: false), + isNotNull, + ); + expect(localizations.photosLabel, isNotNull); + // today + expect( + localizations.sentAtText( + date: DateTime.now(), + time: DateTime.now(), + ), + isNotNull, + ); + // yesterday + expect( + localizations.sentAtText( + date: DateTime.now().subtract(const Duration(days: 1)), + time: DateTime.now(), + ), + isNotNull, + ); + // any other day + expect( + localizations.sentAtText( + date: DateTime.now().subtract(const Duration(days: 3)), + time: DateTime.now(), + ), + isNotNull, + ); + expect(localizations.todayLabel, isNotNull); + expect(localizations.yesterdayLabel, isNotNull); + expect(localizations.channelIsMutedText, isNotNull); + expect(localizations.noTitleText, isNotNull); + expect(localizations.letsStartChattingLabel, isNotNull); + expect(localizations.sendingFirstMessageLabel, isNotNull); + expect(localizations.startAChatLabel, isNotNull); + expect(localizations.loadingChannelsError, isNotNull); + expect(localizations.deleteConversationLabel, isNotNull); + expect(localizations.deleteConversationQuestion, isNotNull); + expect(localizations.streamChatLabel, isNotNull); + expect(localizations.searchingForNetworkText, isNotNull); + expect(localizations.offlineLabel, isNotNull); + expect(localizations.tryAgainLabel, isNotNull); + // 1 member + expect(localizations.membersCountText(1), isNotNull); + // 3 members + expect(localizations.membersCountText(3), isNotNull); + // 1 member + expect(localizations.watchersCountText(1), isNotNull); + // 3 members + expect(localizations.watchersCountText(3), isNotNull); + expect(localizations.viewInfoLabel, isNotNull); + expect(localizations.leaveGroupLabel, isNotNull); + expect(localizations.leaveLabel, isNotNull); + expect(localizations.leaveConversationLabel, isNotNull); + expect(localizations.leaveConversationQuestion, isNotNull); + expect(localizations.showInChatLabel, isNotNull); + expect(localizations.saveImageLabel, isNotNull); + expect(localizations.saveVideoLabel, isNotNull); + expect(localizations.uploadErrorLabel, isNotNull); + expect(localizations.giphyLabel, isNotNull); + expect(localizations.shuffleLabel, isNotNull); + expect(localizations.sendLabel, isNotNull); + expect(localizations.withText, isNotNull); + expect(localizations.inText, isNotNull); + expect(localizations.youText, isNotNull); + expect(localizations.ofText, isNotNull); + expect(localizations.fileText, isNotNull); + expect(localizations.replyToMessageLabel, isNotNull); + }); + } + + test('should throw if try to load locale which is not supported', () async { + const locale = Locale('not-supported-locale'); + try { + getStreamChatTranslation(locale); + } catch (e) { + expect(e, isA()); + } + }); + + test('`.toString`', () { + final supportedLocales = kStreamChatSupportedLanguages.length; + expect( + GlobalStreamChatLocalizations.delegate.toString(), + 'GlobalStreamChatLocalizations.delegate($supportedLocales locales)', + ); + }); +} diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 3a65db58..459b7127 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.1.0 + +тЬЕ Added +- Added support for `Message.i18n` +- Added support for `User.language` + ## 2.0.0 * Migrate this package to null safety * Minor fixes and improvements diff --git a/packages/stream_chat_persistence/example/lib/main.dart b/packages/stream_chat_persistence/example/lib/main.dart index ab6dcd1c..5c83ed78 100644 --- a/packages/stream_chat_persistence/example/lib/main.dart +++ b/packages/stream_chat_persistence/example/lib/main.dart @@ -255,5 +255,5 @@ class _MessageViewState extends State { /// Helper extension for quickly retrieving /// the current user id from a [StreamChatClient]. extension on StreamChatClient { - String get uid => state.user!.id; + String get uid => state.currentUser!.id; } diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart index 447533e0..7f5bd4d0 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart @@ -51,7 +51,7 @@ class MoorChatDatabase extends _$MoorChatDatabase { // you should bump this number whenever you change or add a table definition. @override - int get schemaVersion => 4; + int get schemaVersion => 5; @override MigrationStrategy get migration => MigrationStrategy( diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart index 6a5bc38a..a61d1e6e 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.g.dart @@ -647,6 +647,9 @@ class MessageEntity extends DataClass implements Insertable { /// The channel cid of which this message is part of final String? channelCid; + /// A Map of [messageText] translations. + final Map? i18n; + /// Message custom extraData final Map? extraData; MessageEntity( @@ -673,6 +676,7 @@ class MessageEntity extends DataClass implements Insertable { this.pinExpires, this.pinnedByUserId, this.channelCid, + this.i18n, this.extraData}); factory MessageEntity.fromData( Map data, GeneratedDatabase db, @@ -725,7 +729,9 @@ class MessageEntity extends DataClass implements Insertable { .mapFromDatabaseResponse(data['${effectivePrefix}pinned_by_user_id']), channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), - extraData: $MessagesTable.$converter5.mapToDart(const StringType() + i18n: $MessagesTable.$converter5.mapToDart(const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}i18n'])), + extraData: $MessagesTable.$converter6.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), ); } @@ -797,8 +803,12 @@ class MessageEntity extends DataClass implements Insertable { if (!nullToAbsent || channelCid != null) { map['channel_cid'] = Variable(channelCid); } - if (!nullToAbsent || extraData != null) { + if (!nullToAbsent || i18n != null) { final converter = $MessagesTable.$converter5; + map['i18n'] = Variable(converter.mapToSql(i18n)); + } + if (!nullToAbsent || extraData != null) { + final converter = $MessagesTable.$converter6; map['extra_data'] = Variable(converter.mapToSql(extraData)); } return map; @@ -833,6 +843,7 @@ class MessageEntity extends DataClass implements Insertable { pinExpires: serializer.fromJson(json['pinExpires']), pinnedByUserId: serializer.fromJson(json['pinnedByUserId']), channelCid: serializer.fromJson(json['channelCid']), + i18n: serializer.fromJson?>(json['i18n']), extraData: serializer.fromJson?>(json['extraData']), ); } @@ -863,6 +874,7 @@ class MessageEntity extends DataClass implements Insertable { 'pinExpires': serializer.toJson(pinExpires), 'pinnedByUserId': serializer.toJson(pinnedByUserId), 'channelCid': serializer.toJson(channelCid), + 'i18n': serializer.toJson?>(i18n), 'extraData': serializer.toJson?>(extraData), }; } @@ -891,6 +903,7 @@ class MessageEntity extends DataClass implements Insertable { Value pinExpires = const Value.absent(), Value pinnedByUserId = const Value.absent(), Value channelCid = const Value.absent(), + Value?> i18n = const Value.absent(), Value?> extraData = const Value.absent()}) => MessageEntity( id: id ?? this.id, @@ -922,6 +935,7 @@ class MessageEntity extends DataClass implements Insertable { pinnedByUserId: pinnedByUserId.present ? pinnedByUserId.value : this.pinnedByUserId, channelCid: channelCid.present ? channelCid.value : this.channelCid, + i18n: i18n.present ? i18n.value : this.i18n, extraData: extraData.present ? extraData.value : this.extraData, ); @override @@ -950,6 +964,7 @@ class MessageEntity extends DataClass implements Insertable { ..write('pinExpires: $pinExpires, ') ..write('pinnedByUserId: $pinnedByUserId, ') ..write('channelCid: $channelCid, ') + ..write('i18n: $i18n, ') ..write('extraData: $extraData') ..write(')')) .toString(); @@ -998,7 +1013,7 @@ class MessageEntity extends DataClass implements Insertable { .hashCode, $mrjc( pinned.hashCode, - $mrjc(pinnedAt.hashCode, $mrjc(pinExpires.hashCode, $mrjc(pinnedByUserId.hashCode, $mrjc(channelCid.hashCode, extraData.hashCode)))))))))))))))))))))))); + $mrjc(pinnedAt.hashCode, $mrjc(pinExpires.hashCode, $mrjc(pinnedByUserId.hashCode, $mrjc(channelCid.hashCode, $mrjc(i18n.hashCode, extraData.hashCode))))))))))))))))))))))))); @override bool operator ==(Object other) => identical(this, other) || @@ -1026,6 +1041,7 @@ class MessageEntity extends DataClass implements Insertable { other.pinExpires == this.pinExpires && other.pinnedByUserId == this.pinnedByUserId && other.channelCid == this.channelCid && + other.i18n == this.i18n && other.extraData == this.extraData); } @@ -1053,6 +1069,7 @@ class MessagesCompanion extends UpdateCompanion { final Value pinExpires; final Value pinnedByUserId; final Value channelCid; + final Value?> i18n; final Value?> extraData; const MessagesCompanion({ this.id = const Value.absent(), @@ -1078,6 +1095,7 @@ class MessagesCompanion extends UpdateCompanion { this.pinExpires = const Value.absent(), this.pinnedByUserId = const Value.absent(), this.channelCid = const Value.absent(), + this.i18n = const Value.absent(), this.extraData = const Value.absent(), }); MessagesCompanion.insert({ @@ -1104,6 +1122,7 @@ class MessagesCompanion extends UpdateCompanion { this.pinExpires = const Value.absent(), this.pinnedByUserId = const Value.absent(), this.channelCid = const Value.absent(), + this.i18n = const Value.absent(), this.extraData = const Value.absent(), }) : id = Value(id), attachments = Value(attachments), @@ -1132,6 +1151,7 @@ class MessagesCompanion extends UpdateCompanion { Expression? pinExpires, Expression? pinnedByUserId, Expression? channelCid, + Expression?>? i18n, Expression?>? extraData, }) { return RawValuesInsertable({ @@ -1158,6 +1178,7 @@ class MessagesCompanion extends UpdateCompanion { if (pinExpires != null) 'pin_expires': pinExpires, if (pinnedByUserId != null) 'pinned_by_user_id': pinnedByUserId, if (channelCid != null) 'channel_cid': channelCid, + if (i18n != null) 'i18n': i18n, if (extraData != null) 'extra_data': extraData, }); } @@ -1186,6 +1207,7 @@ class MessagesCompanion extends UpdateCompanion { Value? pinExpires, Value? pinnedByUserId, Value? channelCid, + Value?>? i18n, Value?>? extraData}) { return MessagesCompanion( id: id ?? this.id, @@ -1211,6 +1233,7 @@ class MessagesCompanion extends UpdateCompanion { pinExpires: pinExpires ?? this.pinExpires, pinnedByUserId: pinnedByUserId ?? this.pinnedByUserId, channelCid: channelCid ?? this.channelCid, + i18n: i18n ?? this.i18n, extraData: extraData ?? this.extraData, ); } @@ -1296,8 +1319,12 @@ class MessagesCompanion extends UpdateCompanion { if (channelCid.present) { map['channel_cid'] = Variable(channelCid.value); } - if (extraData.present) { + if (i18n.present) { final converter = $MessagesTable.$converter5; + map['i18n'] = Variable(converter.mapToSql(i18n.value)); + } + if (extraData.present) { + final converter = $MessagesTable.$converter6; map['extra_data'] = Variable(converter.mapToSql(extraData.value)); } @@ -1330,6 +1357,7 @@ class MessagesCompanion extends UpdateCompanion { ..write('pinExpires: $pinExpires, ') ..write('pinnedByUserId: $pinnedByUserId, ') ..write('channelCid: $channelCid, ') + ..write('i18n: $i18n, ') ..write('extraData: $extraData') ..write(')')) .toString(); @@ -1468,11 +1496,16 @@ class $MessagesTable extends Messages requiredDuringInsert: false, $customConstraints: 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE'); + final VerificationMeta _i18nMeta = const VerificationMeta('i18n'); + late final GeneratedColumnWithTypeConverter, String?> + i18n = GeneratedColumn('i18n', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>($MessagesTable.$converter5); final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); late final GeneratedColumnWithTypeConverter, String?> extraData = GeneratedColumn('extra_data', aliasedName, true, typeName: 'TEXT', requiredDuringInsert: false) - .withConverter>($MessagesTable.$converter5); + .withConverter>($MessagesTable.$converter6); @override List get $columns => [ id, @@ -1498,6 +1531,7 @@ class $MessagesTable extends Messages pinExpires, pinnedByUserId, channelCid, + i18n, extraData ]; @override @@ -1601,6 +1635,7 @@ class $MessagesTable extends Messages channelCid.isAcceptableOrUnknown( data['channel_cid']!, _channelCidMeta)); } + context.handle(_i18nMeta, const VerificationResult.success()); context.handle(_extraDataMeta, const VerificationResult.success()); return context; } @@ -1628,7 +1663,9 @@ class $MessagesTable extends Messages MapConverter(); static TypeConverter, String> $converter4 = MapConverter(); - static TypeConverter, String> $converter5 = + static TypeConverter, String> $converter5 = + MapConverter(); + static TypeConverter, String> $converter6 = MapConverter(); } @@ -1704,6 +1741,9 @@ class PinnedMessageEntity extends DataClass /// The channel cid of which this message is part of final String? channelCid; + /// A Map of [messageText] translations. + final Map? i18n; + /// Message custom extraData final Map? extraData; PinnedMessageEntity( @@ -1730,6 +1770,7 @@ class PinnedMessageEntity extends DataClass this.pinExpires, this.pinnedByUserId, this.channelCid, + this.i18n, this.extraData}); factory PinnedMessageEntity.fromData( Map data, GeneratedDatabase db, @@ -1785,7 +1826,9 @@ class PinnedMessageEntity extends DataClass .mapFromDatabaseResponse(data['${effectivePrefix}pinned_by_user_id']), channelCid: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}channel_cid']), - extraData: $PinnedMessagesTable.$converter5.mapToDart(const StringType() + i18n: $PinnedMessagesTable.$converter5.mapToDart(const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}i18n'])), + extraData: $PinnedMessagesTable.$converter6.mapToDart(const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}extra_data'])), ); } @@ -1857,8 +1900,12 @@ class PinnedMessageEntity extends DataClass if (!nullToAbsent || channelCid != null) { map['channel_cid'] = Variable(channelCid); } - if (!nullToAbsent || extraData != null) { + if (!nullToAbsent || i18n != null) { final converter = $PinnedMessagesTable.$converter5; + map['i18n'] = Variable(converter.mapToSql(i18n)); + } + if (!nullToAbsent || extraData != null) { + final converter = $PinnedMessagesTable.$converter6; map['extra_data'] = Variable(converter.mapToSql(extraData)); } return map; @@ -1893,6 +1940,7 @@ class PinnedMessageEntity extends DataClass pinExpires: serializer.fromJson(json['pinExpires']), pinnedByUserId: serializer.fromJson(json['pinnedByUserId']), channelCid: serializer.fromJson(json['channelCid']), + i18n: serializer.fromJson?>(json['i18n']), extraData: serializer.fromJson?>(json['extraData']), ); } @@ -1923,6 +1971,7 @@ class PinnedMessageEntity extends DataClass 'pinExpires': serializer.toJson(pinExpires), 'pinnedByUserId': serializer.toJson(pinnedByUserId), 'channelCid': serializer.toJson(channelCid), + 'i18n': serializer.toJson?>(i18n), 'extraData': serializer.toJson?>(extraData), }; } @@ -1951,6 +2000,7 @@ class PinnedMessageEntity extends DataClass Value pinExpires = const Value.absent(), Value pinnedByUserId = const Value.absent(), Value channelCid = const Value.absent(), + Value?> i18n = const Value.absent(), Value?> extraData = const Value.absent()}) => PinnedMessageEntity( id: id ?? this.id, @@ -1982,6 +2032,7 @@ class PinnedMessageEntity extends DataClass pinnedByUserId: pinnedByUserId.present ? pinnedByUserId.value : this.pinnedByUserId, channelCid: channelCid.present ? channelCid.value : this.channelCid, + i18n: i18n.present ? i18n.value : this.i18n, extraData: extraData.present ? extraData.value : this.extraData, ); @override @@ -2010,6 +2061,7 @@ class PinnedMessageEntity extends DataClass ..write('pinExpires: $pinExpires, ') ..write('pinnedByUserId: $pinnedByUserId, ') ..write('channelCid: $channelCid, ') + ..write('i18n: $i18n, ') ..write('extraData: $extraData') ..write(')')) .toString(); @@ -2058,7 +2110,7 @@ class PinnedMessageEntity extends DataClass .hashCode, $mrjc( pinned.hashCode, - $mrjc(pinnedAt.hashCode, $mrjc(pinExpires.hashCode, $mrjc(pinnedByUserId.hashCode, $mrjc(channelCid.hashCode, extraData.hashCode)))))))))))))))))))))))); + $mrjc(pinnedAt.hashCode, $mrjc(pinExpires.hashCode, $mrjc(pinnedByUserId.hashCode, $mrjc(channelCid.hashCode, $mrjc(i18n.hashCode, extraData.hashCode))))))))))))))))))))))))); @override bool operator ==(Object other) => identical(this, other) || @@ -2086,6 +2138,7 @@ class PinnedMessageEntity extends DataClass other.pinExpires == this.pinExpires && other.pinnedByUserId == this.pinnedByUserId && other.channelCid == this.channelCid && + other.i18n == this.i18n && other.extraData == this.extraData); } @@ -2113,6 +2166,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { final Value pinExpires; final Value pinnedByUserId; final Value channelCid; + final Value?> i18n; final Value?> extraData; const PinnedMessagesCompanion({ this.id = const Value.absent(), @@ -2138,6 +2192,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.pinExpires = const Value.absent(), this.pinnedByUserId = const Value.absent(), this.channelCid = const Value.absent(), + this.i18n = const Value.absent(), this.extraData = const Value.absent(), }); PinnedMessagesCompanion.insert({ @@ -2164,6 +2219,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.pinExpires = const Value.absent(), this.pinnedByUserId = const Value.absent(), this.channelCid = const Value.absent(), + this.i18n = const Value.absent(), this.extraData = const Value.absent(), }) : id = Value(id), attachments = Value(attachments), @@ -2192,6 +2248,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { Expression? pinExpires, Expression? pinnedByUserId, Expression? channelCid, + Expression?>? i18n, Expression?>? extraData, }) { return RawValuesInsertable({ @@ -2218,6 +2275,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { if (pinExpires != null) 'pin_expires': pinExpires, if (pinnedByUserId != null) 'pinned_by_user_id': pinnedByUserId, if (channelCid != null) 'channel_cid': channelCid, + if (i18n != null) 'i18n': i18n, if (extraData != null) 'extra_data': extraData, }); } @@ -2246,6 +2304,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { Value? pinExpires, Value? pinnedByUserId, Value? channelCid, + Value?>? i18n, Value?>? extraData}) { return PinnedMessagesCompanion( id: id ?? this.id, @@ -2271,6 +2330,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { pinExpires: pinExpires ?? this.pinExpires, pinnedByUserId: pinnedByUserId ?? this.pinnedByUserId, channelCid: channelCid ?? this.channelCid, + i18n: i18n ?? this.i18n, extraData: extraData ?? this.extraData, ); } @@ -2356,8 +2416,12 @@ class PinnedMessagesCompanion extends UpdateCompanion { if (channelCid.present) { map['channel_cid'] = Variable(channelCid.value); } - if (extraData.present) { + if (i18n.present) { final converter = $PinnedMessagesTable.$converter5; + map['i18n'] = Variable(converter.mapToSql(i18n.value)); + } + if (extraData.present) { + final converter = $PinnedMessagesTable.$converter6; map['extra_data'] = Variable(converter.mapToSql(extraData.value)); } @@ -2390,6 +2454,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { ..write('pinExpires: $pinExpires, ') ..write('pinnedByUserId: $pinnedByUserId, ') ..write('channelCid: $channelCid, ') + ..write('i18n: $i18n, ') ..write('extraData: $extraData') ..write(')')) .toString(); @@ -2529,12 +2594,17 @@ class $PinnedMessagesTable extends PinnedMessages requiredDuringInsert: false, $customConstraints: 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE'); + final VerificationMeta _i18nMeta = const VerificationMeta('i18n'); + late final GeneratedColumnWithTypeConverter, String?> + i18n = GeneratedColumn('i18n', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false) + .withConverter>($PinnedMessagesTable.$converter5); final VerificationMeta _extraDataMeta = const VerificationMeta('extraData'); late final GeneratedColumnWithTypeConverter, String?> extraData = GeneratedColumn('extra_data', aliasedName, true, typeName: 'TEXT', requiredDuringInsert: false) .withConverter>( - $PinnedMessagesTable.$converter5); + $PinnedMessagesTable.$converter6); @override List get $columns => [ id, @@ -2560,6 +2630,7 @@ class $PinnedMessagesTable extends PinnedMessages pinExpires, pinnedByUserId, channelCid, + i18n, extraData ]; @override @@ -2664,6 +2735,7 @@ class $PinnedMessagesTable extends PinnedMessages channelCid.isAcceptableOrUnknown( data['channel_cid']!, _channelCidMeta)); } + context.handle(_i18nMeta, const VerificationResult.success()); context.handle(_extraDataMeta, const VerificationResult.success()); return context; } @@ -2691,7 +2763,9 @@ class $PinnedMessagesTable extends PinnedMessages MapConverter(); static TypeConverter, String> $converter4 = MapConverter(); - static TypeConverter, String> $converter5 = + static TypeConverter, String> $converter5 = + MapConverter(); + static TypeConverter, String> $converter6 = MapConverter(); } @@ -3030,6 +3104,9 @@ class UserEntity extends DataClass implements Insertable { /// User role final String? role; + /// The language this user prefers. + final String? language; + /// Date of user creation final DateTime createdAt; @@ -3050,6 +3127,7 @@ class UserEntity extends DataClass implements Insertable { UserEntity( {required this.id, this.role, + this.language, required this.createdAt, required this.updatedAt, this.lastActive, @@ -3064,6 +3142,8 @@ class UserEntity extends DataClass implements Insertable { .mapFromDatabaseResponse(data['${effectivePrefix}id'])!, role: const StringType() .mapFromDatabaseResponse(data['${effectivePrefix}role']), + language: const StringType() + .mapFromDatabaseResponse(data['${effectivePrefix}language']), createdAt: const DateTimeType() .mapFromDatabaseResponse(data['${effectivePrefix}created_at'])!, updatedAt: const DateTimeType() @@ -3085,6 +3165,9 @@ class UserEntity extends DataClass implements Insertable { if (!nullToAbsent || role != null) { map['role'] = Variable(role); } + if (!nullToAbsent || language != null) { + map['language'] = Variable(language); + } map['created_at'] = Variable(createdAt); map['updated_at'] = Variable(updatedAt); if (!nullToAbsent || lastActive != null) { @@ -3105,6 +3188,7 @@ class UserEntity extends DataClass implements Insertable { return UserEntity( id: serializer.fromJson(json['id']), role: serializer.fromJson(json['role']), + language: serializer.fromJson(json['language']), createdAt: serializer.fromJson(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), lastActive: serializer.fromJson(json['lastActive']), @@ -3119,6 +3203,7 @@ class UserEntity extends DataClass implements Insertable { return { 'id': serializer.toJson(id), 'role': serializer.toJson(role), + 'language': serializer.toJson(language), 'createdAt': serializer.toJson(createdAt), 'updatedAt': serializer.toJson(updatedAt), 'lastActive': serializer.toJson(lastActive), @@ -3131,6 +3216,7 @@ class UserEntity extends DataClass implements Insertable { UserEntity copyWith( {String? id, Value role = const Value.absent(), + Value language = const Value.absent(), DateTime? createdAt, DateTime? updatedAt, Value lastActive = const Value.absent(), @@ -3140,6 +3226,7 @@ class UserEntity extends DataClass implements Insertable { UserEntity( id: id ?? this.id, role: role.present ? role.value : this.role, + language: language.present ? language.value : this.language, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, lastActive: lastActive.present ? lastActive.value : this.lastActive, @@ -3152,6 +3239,7 @@ class UserEntity extends DataClass implements Insertable { return (StringBuffer('UserEntity(') ..write('id: $id, ') ..write('role: $role, ') + ..write('language: $language, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') ..write('lastActive: $lastActive, ') @@ -3168,19 +3256,24 @@ class UserEntity extends DataClass implements Insertable { $mrjc( role.hashCode, $mrjc( - createdAt.hashCode, + language.hashCode, $mrjc( - updatedAt.hashCode, + createdAt.hashCode, $mrjc( - lastActive.hashCode, - $mrjc(online.hashCode, - $mrjc(banned.hashCode, extraData.hashCode)))))))); + updatedAt.hashCode, + $mrjc( + lastActive.hashCode, + $mrjc( + online.hashCode, + $mrjc( + banned.hashCode, extraData.hashCode))))))))); @override bool operator ==(Object other) => identical(this, other) || (other is UserEntity && other.id == this.id && other.role == this.role && + other.language == this.language && other.createdAt == this.createdAt && other.updatedAt == this.updatedAt && other.lastActive == this.lastActive && @@ -3192,6 +3285,7 @@ class UserEntity extends DataClass implements Insertable { class UsersCompanion extends UpdateCompanion { final Value id; final Value role; + final Value language; final Value createdAt; final Value updatedAt; final Value lastActive; @@ -3201,6 +3295,7 @@ class UsersCompanion extends UpdateCompanion { const UsersCompanion({ this.id = const Value.absent(), this.role = const Value.absent(), + this.language = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), this.lastActive = const Value.absent(), @@ -3211,6 +3306,7 @@ class UsersCompanion extends UpdateCompanion { UsersCompanion.insert({ required String id, this.role = const Value.absent(), + this.language = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), this.lastActive = const Value.absent(), @@ -3222,6 +3318,7 @@ class UsersCompanion extends UpdateCompanion { static Insertable custom({ Expression? id, Expression? role, + Expression? language, Expression? createdAt, Expression? updatedAt, Expression? lastActive, @@ -3232,6 +3329,7 @@ class UsersCompanion extends UpdateCompanion { return RawValuesInsertable({ if (id != null) 'id': id, if (role != null) 'role': role, + if (language != null) 'language': language, if (createdAt != null) 'created_at': createdAt, if (updatedAt != null) 'updated_at': updatedAt, if (lastActive != null) 'last_active': lastActive, @@ -3244,6 +3342,7 @@ class UsersCompanion extends UpdateCompanion { UsersCompanion copyWith( {Value? id, Value? role, + Value? language, Value? createdAt, Value? updatedAt, Value? lastActive, @@ -3253,6 +3352,7 @@ class UsersCompanion extends UpdateCompanion { return UsersCompanion( id: id ?? this.id, role: role ?? this.role, + language: language ?? this.language, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, lastActive: lastActive ?? this.lastActive, @@ -3271,6 +3371,9 @@ class UsersCompanion extends UpdateCompanion { if (role.present) { map['role'] = Variable(role.value); } + if (language.present) { + map['language'] = Variable(language.value); + } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } @@ -3299,6 +3402,7 @@ class UsersCompanion extends UpdateCompanion { return (StringBuffer('UsersCompanion(') ..write('id: $id, ') ..write('role: $role, ') + ..write('language: $language, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') ..write('lastActive: $lastActive, ') @@ -3322,6 +3426,10 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { late final GeneratedColumn role = GeneratedColumn( 'role', aliasedName, true, typeName: 'TEXT', requiredDuringInsert: false); + final VerificationMeta _languageMeta = const VerificationMeta('language'); + late final GeneratedColumn language = GeneratedColumn( + 'language', aliasedName, true, + typeName: 'TEXT', requiredDuringInsert: false); final VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); late final GeneratedColumn createdAt = GeneratedColumn( 'created_at', aliasedName, false, @@ -3358,8 +3466,17 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { typeName: 'TEXT', requiredDuringInsert: true) .withConverter>($UsersTable.$converter0); @override - List get $columns => - [id, role, createdAt, updatedAt, lastActive, online, banned, extraData]; + List get $columns => [ + id, + role, + language, + createdAt, + updatedAt, + lastActive, + online, + banned, + extraData + ]; @override String get aliasedName => _alias ?? 'users'; @override @@ -3378,6 +3495,10 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { context.handle( _roleMeta, role.isAcceptableOrUnknown(data['role']!, _roleMeta)); } + if (data.containsKey('language')) { + context.handle(_languageMeta, + language.isAcceptableOrUnknown(data['language']!, _languageMeta)); + } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); diff --git a/packages/stream_chat_persistence/lib/src/entity/messages.dart b/packages/stream_chat_persistence/lib/src/entity/messages.dart index 4006d6ce..0aef2b84 100644 --- a/packages/stream_chat_persistence/lib/src/entity/messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/messages.dart @@ -80,6 +80,9 @@ class Messages extends Table { TextColumn get channelCid => text().nullable().customConstraint( 'NULLABLE REFERENCES channels(cid) ON DELETE CASCADE')(); + /// A Map of [messageText] translations. + TextColumn get i18n => text().nullable().map(MapConverter())(); + /// Message custom extraData TextColumn get extraData => text().nullable().map(MapConverter())(); diff --git a/packages/stream_chat_persistence/lib/src/entity/users.dart b/packages/stream_chat_persistence/lib/src/entity/users.dart index 303092c9..286bfb40 100644 --- a/packages/stream_chat_persistence/lib/src/entity/users.dart +++ b/packages/stream_chat_persistence/lib/src/entity/users.dart @@ -11,6 +11,9 @@ class Users extends Table { /// User role TextColumn get role => text().nullable()(); + /// The language this user prefers. + TextColumn get language => text().nullable()(); + /// Date of user creation DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); diff --git a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart index 558c6e45..85692296 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart @@ -44,6 +44,7 @@ extension MessageEntityX on MessageEntity { pinnedBy: pinnedBy, mentionedUsers: mentionedUsers.map((e) => User.fromJson(jsonDecode(e))).toList(), + i18n: i18n, ); } @@ -75,5 +76,6 @@ extension MessageX on Message { pinnedAt: pinnedAt, pinExpires: pinExpires, pinnedByUserId: pinnedBy?.id, + i18n: i18n, ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart index 1083e239..b1a7849a 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart @@ -42,6 +42,9 @@ extension PinnedMessageEntityX on PinnedMessageEntity { pinnedAt: pinnedAt, pinExpires: pinExpires, pinnedBy: pinnedBy, + mentionedUsers: + mentionedUsers.map((e) => User.fromJson(jsonDecode(e))).toList(), + i18n: i18n, ); } @@ -73,5 +76,6 @@ extension PMessageX on Message { pinnedAt: pinnedAt, pinExpires: pinExpires, pinnedByUserId: pinnedBy?.id, + i18n: i18n, ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart index 533a45b1..8b38a74d 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart @@ -7,6 +7,7 @@ extension UserEntityX on UserEntity { User toUser() => User( id: id, updatedAt: updatedAt, + language: language, role: role, online: online, lastActive: lastActive, @@ -22,6 +23,7 @@ extension UserX on User { UserEntity toEntity() => UserEntity( id: id, role: role, + language: language, createdAt: createdAt, updatedAt: updatedAt, lastActive: lastActive, diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 08029e91..f3ea112a 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_persistence homepage: https://github.com/GetStream/stream-chat-flutter description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. -version: 2.0.0 +version: 2.1.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -18,7 +18,7 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^2.0.0 + stream_chat: ^2.1.0 dev_dependencies: build_runner: ^2.0.1 diff --git a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart index 62a3df15..81e629b6 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart @@ -1,5 +1,3 @@ -import 'dart:math' as math; - import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/channel_query_dao.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; @@ -84,9 +82,9 @@ void main() { cid: cids[index], createdBy: users[index], config: ChannelConfig(), - extraData: {'test_custom_field': math.Random().nextInt(100)}, + extraData: {'test_custom_field': 3 + index}, createdAt: now, - memberCount: math.Random().nextInt(100), + memberCount: 3 + index, lastMessageAt: now.add(Duration(hours: index)), ), ).reversed.toList(growable: false); @@ -99,6 +97,8 @@ void main() { } group('getChannels', () { + tearDown(() async => database.flush()); + final filter = Filter.in_('members', const ['testUserId']); test('should return empty list of channels', () async { diff --git a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart index 54cc219a..fd0a43ce 100644 --- a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart @@ -35,10 +35,15 @@ void main() { replyCount: index, updatedAt: DateTime.now(), extraData: const {'extra_test_field': 'extraTestData'}, - text: 'Dummy text #$index', + text: 'Hello #$index', pinned: math.Random().nextBool(), pinnedAt: DateTime.now(), pinnedBy: User(id: 'testUserId$index'), + i18n: { + 'en_text': 'Hello #$index', + 'hi_text': 'рдирдорд╕реНрддреЗ #$index', + 'language': 'en', + }, ), ); final quotedMessages = List.generate( @@ -52,11 +57,16 @@ void main() { replyCount: index, updatedAt: DateTime.now(), extraData: const {'extra_test_field': 'extraTestData'}, - text: 'Dummy text #$index', + text: 'Hello #$index', quotedMessageId: messages[index].id, pinned: math.Random().nextBool(), pinnedAt: DateTime.now(), pinnedBy: User(id: 'testUserId$index'), + i18n: { + 'en_text': 'Hello #$index', + 'hi_text': 'рдирдорд╕реНрддреЗ #$index', + 'language': 'en', + }, ), ); final threadMessages = List.generate( @@ -72,10 +82,15 @@ void main() { replyCount: index, updatedAt: DateTime.now(), extraData: const {'extra_test_field': 'extraTestData'}, - text: 'Dummy text #$index', + text: 'Hello #$index', pinned: math.Random().nextBool(), pinnedAt: DateTime.now(), pinnedBy: User(id: 'testUserId$index'), + i18n: { + 'en_text': 'Hello #$index', + 'hi_text': 'рдирдорд╕реНрддреЗ #$index', + 'language': 'en', + }, ), ); final allMessages = [ diff --git a/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart index ffb50edd..5b5d04d4 100644 --- a/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart @@ -22,6 +22,7 @@ void main() { (index) => User( id: 'testUserId$index', role: 'testRole', + language: 'hi', createdAt: DateTime.now(), updatedAt: DateTime.now(), lastActive: DateTime.now(), diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart index 7d867deb..0f9e8cf6 100644 --- a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -56,11 +56,16 @@ void main() { extraData: {'extra_test_data': 'extraData'}, userId: user.id, deletedAt: DateTime.now(), - messageText: 'dummy text', + messageText: 'Hello', pinned: true, pinExpires: DateTime.now().toUtc(), pinnedAt: DateTime.now(), pinnedByUserId: user.id, + i18n: const { + 'en_text': 'Hello', + 'hi_text': 'рдирдорд╕реНрддреЗ', + 'language': 'en', + }, ); final message = entity.toMessage( user: user, @@ -99,6 +104,7 @@ void main() { expect(message.pinnedBy!.id, entity.pinnedByUserId); expect(message.reactionCounts, entity.reactionCounts); expect(message.reactionScores, entity.reactionScores); + expect(message.i18n, entity.i18n); for (var i = 0; i < message.attachments.length; i++) { final messageAttachment = message.attachments[i]; final entityAttachmentData = jsonDecode(entity.attachments[i]); @@ -156,11 +162,16 @@ void main() { extraData: const {'extra_test_data': 'extraData'}, user: user, deletedAt: DateTime.now(), - text: 'dummy text', + text: 'Hello', pinned: true, pinExpires: DateTime.now(), pinnedAt: DateTime.now(), pinnedBy: user, + i18n: const { + 'en_text': 'Hello', + 'hi_text': 'рдирдорд╕реНрддреЗ', + 'language': 'en', + }, ); final entity = message.toEntity(cid: cid); expect(entity, isA()); @@ -193,5 +204,6 @@ void main() { entity.attachments, message.attachments.map((it) => jsonEncode(it.toData())).toList(), ); + expect(entity.i18n, message.i18n); }); } diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart index 8b5f4db4..5150df62 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart @@ -54,11 +54,16 @@ void main() { extraData: {'extra_test_data': 'extraData'}, userId: user.id, deletedAt: DateTime.now(), - messageText: 'dummy text', + messageText: 'Hello', pinned: true, pinExpires: DateTime.now().toUtc(), pinnedAt: DateTime.now(), pinnedByUserId: user.id, + i18n: const { + 'en_text': 'Hello', + 'hi_text': 'рдирдорд╕реНрддреЗ', + 'language': 'en', + }, ); final message = entity.toMessage( user: user, @@ -92,6 +97,7 @@ void main() { expect(message.pinnedBy!.id, entity.pinnedByUserId); expect(message.reactionCounts, entity.reactionCounts); expect(message.reactionScores, entity.reactionScores); + expect(message.i18n, entity.i18n); for (var i = 0; i < message.attachments.length; i++) { final messageAttachment = message.attachments[i]; final entityAttachmentData = jsonDecode(entity.attachments[i]); @@ -146,11 +152,16 @@ void main() { extraData: const {'extra_test_data': 'extraData'}, user: user, deletedAt: DateTime.now(), - text: 'dummy text', + text: 'Hello', pinned: true, pinExpires: DateTime.now(), pinnedAt: DateTime.now(), pinnedBy: user, + i18n: const { + 'en_text': 'Hello', + 'hi_text': 'рдирдорд╕реНрддреЗ', + 'language': 'en', + }, ); final entity = message.toPinnedEntity(cid: cid); expect(entity, isA()); @@ -181,5 +192,6 @@ void main() { entity.attachments, message.attachments.map((it) => jsonEncode(it.toData())).toList(), ); + expect(entity.i18n, message.i18n); }); } diff --git a/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart index 4454ad73..eca2a32a 100644 --- a/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart @@ -11,6 +11,7 @@ void main() { final entity = UserEntity( id: 'testUserId', role: 'testType', + language: 'hi', createdAt: DateTime.now(), updatedAt: DateTime.now(), lastActive: DateTime.now(), @@ -22,6 +23,7 @@ void main() { expect(user, isA()); expect(user.id, entity.id); expect(user.role, entity.role); + expect(user.language, entity.language); expect(user.createdAt, isSameDateAs(entity.createdAt)); expect(user.updatedAt, isSameDateAs(entity.updatedAt)); expect(user.lastActive, isSameDateAs(entity.lastActive!)); @@ -34,6 +36,7 @@ void main() { final user = User( id: 'testUserId', role: 'testType', + language: 'hi', createdAt: DateTime.now(), updatedAt: DateTime.now(), lastActive: DateTime.now(), @@ -45,6 +48,7 @@ void main() { expect(entity, isA()); expect(entity.id, user.id); expect(entity.role, user.role); + expect(entity.language, user.language); expect(entity.createdAt, isSameDateAs(user.createdAt)); expect(entity.updatedAt, isSameDateAs(user.updatedAt)); expect(entity.lastActive, isSameDateAs(user.lastActive!));