Merge pull request #583 from GetStream/release/2.1.0

chore(repo): 2.1.0
This commit is contained in:
Salvatore Giordano
2021-07-28 12:36:41 +02:00
committed by GitHub
245 changed files with 9654 additions and 788 deletions
+4 -1
View File
@@ -12,13 +12,16 @@ jobs:
main: main:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: amannn/action-semantic-pull-request@v2.1.0 - uses: amannn/action-semantic-pull-request@v3.4.0
with: with:
scopes: | scopes: |
llc llc
persistence persistence
core core
ui ui
doc
repo
localization
requireScope: true requireScope: true
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3
View File
@@ -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) ### [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. 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 ## Flutter Chat Tutorial
The best place to start is the [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/). The best place to start is the [Flutter Chat Tutorial](https://getstream.io/chat/flutter/tutorial/).
+4 -4
View File
@@ -13,7 +13,7 @@ scripts:
analyze: analyze:
run: | run: |
melos exec -c 4 --ignore="*example*" -- \ melos exec -c 5 --ignore="*example*" -- \
dart analyze --fatal-infos . dart analyze --fatal-infos .
description: | description: |
Run `dart analyze` in all packages. Run `dart analyze` in all packages.
@@ -26,7 +26,7 @@ scripts:
lint:pub: lint:pub:
run: | run: |
melos exec -c 4 --no-private --ignore="*example*" -- \ melos exec -c 5 --no-private --ignore="*example*" -- \
pub publish --dry-run pub publish --dry-run
description: | description: |
Run `pub publish --dry-run` in all packages. Run `pub publish --dry-run` in all packages.
@@ -56,7 +56,7 @@ scripts:
dir-exists: test dir-exists: test
test:flutter: 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. description: Run Flutter tests for a specific package in this project.
select-package: select-package:
flutter: true flutter: true
@@ -64,7 +64,7 @@ scripts:
coverage:ignore-file: coverage:ignore-file:
run: | 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. description: Removes all the ignored files from the coverage report.
select-package: select-package:
dir-exists: coverage dir-exists: coverage
+21
View File
@@ -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 ## 2.0.0
🛑️ Breaking Changes from `1.5.3` 🛑️ Breaking Changes from `1.5.3`
+1 -1
View File
@@ -245,5 +245,5 @@ class _MessageViewState extends State<MessageView> {
/// Helper extension for quickly retrieving /// Helper extension for quickly retrieving
/// the current user id from a [StreamChatClient]. /// the current user id from a [StreamChatClient].
extension on StreamChatClient { extension on StreamChatClient {
String get uid => state.user!.id; String get uid => state.currentUser!.id;
} }
@@ -65,12 +65,12 @@ class Channel {
/// Returns true if the channel is muted /// Returns true if the channel is muted
bool get isMuted => bool get isMuted =>
_client.state.user?.channelMutes _client.state.currentUser?.channelMutes
.any((element) => element.channel.cid == cid) == .any((element) => element.channel.cid == cid) ==
true; true;
/// Returns true if the channel is muted as a stream /// Returns true if the channel is muted as a stream
Stream<bool>? get isMutedStream => _client.state.userStream Stream<bool>? get isMutedStream => _client.state.currentUserStream
.map((event) => .map((event) =>
event!.channelMutes.any((element) => element.channel.cid == cid) == event!.channelMutes.any((element) => element.channel.cid == cid) ==
true) true)
@@ -382,7 +382,7 @@ class Channel {
// ignore: parameter_assignments // ignore: parameter_assignments
message = message.copyWith( message = message.copyWith(
createdAt: message.createdAt, createdAt: message.createdAt,
user: _client.state.user, user: _client.state.currentUser,
quotedMessage: quotedMessage, quotedMessage: quotedMessage,
status: MessageSendingStatus.sending, status: MessageSendingStatus.sending,
attachments: message.attachments.map( attachments: message.attachments.map(
@@ -693,7 +693,7 @@ class Channel {
_checkInitialized(); _checkInitialized();
final messageId = message.id; final messageId = message.id;
final now = DateTime.now(); final now = DateTime.now();
final user = _client.state.user; final user = _client.state.currentUser;
final latestReactions = [...message.latestReactions ?? <Reaction>[]]; final latestReactions = [...message.latestReactions ?? <Reaction>[]];
if (enforceUnique) { if (enforceUnique) {
@@ -750,7 +750,7 @@ class Channel {
Future<EmptyResponse> deleteReaction( Future<EmptyResponse> deleteReaction(
Message message, Reaction reaction) async { Message message, Reaction reaction) async {
final type = reaction.type; final type = reaction.type;
final user = _client.state.user; final user = _client.state.currentUser;
final reactionCounts = {...message.reactionCounts ?? <String, int>{}}; final reactionCounts = {...message.reactionCounts ?? <String, int>{}};
if (reactionCounts.containsKey(type)) { if (reactionCounts.containsKey(type)) {
@@ -1314,7 +1314,7 @@ class ChannelClientState {
void _computeInitialUnread() { void _computeInitialUnread() {
final userRead = channelState.read.firstWhereOrNull( 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) { if (userRead != null) {
unreadCount = userRead.unreadMessages; unreadCount = userRead.unreadMessages;
@@ -1431,7 +1431,7 @@ class ChannelClientState {
void _listenReactionDeleted() { void _listenReactionDeleted() {
_subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) { _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( final message = event.message!.copyWith(
ownReactions: [...event.message!.latestReactions!] ownReactions: [...event.message!.latestReactions!]
..removeWhere((it) => it.userId != userId), ..removeWhere((it) => it.userId != userId),
@@ -1442,7 +1442,7 @@ class ChannelClientState {
void _listenReactions() { void _listenReactions() {
_subscriptions.add(_channel.on(EventType.reactionNew).listen((event) { _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( final message = event.message!.copyWith(
ownReactions: [...event.message!.latestReactions!] ownReactions: [...event.message!.latestReactions!]
..removeWhere((it) => it.userId != userId), ..removeWhere((it) => it.userId != userId),
@@ -1458,7 +1458,7 @@ class ChannelClientState {
EventType.reactionUpdated, EventType.reactionUpdated,
) )
.listen((event) { .listen((event) {
final userId = _channel.client.state.user!.id; final userId = _channel.client.state.currentUser!.id;
final message = event.message!.copyWith( final message = event.message!.copyWith(
ownReactions: [...event.message!.latestReactions!] ownReactions: [...event.message!.latestReactions!]
..removeWhere((it) => it.userId != userId), ..removeWhere((it) => it.userId != userId),
@@ -1552,7 +1552,7 @@ class ChannelClientState {
if (userReadIndex != null && userReadIndex != -1) { if (userReadIndex != null && userReadIndex != -1) {
final userRead = readList.removeAt(userReadIndex); 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; unreadCount = 0;
} }
readList.add(Read( readList.add(Read(
@@ -1642,8 +1642,9 @@ class ChannelClientState {
int get unreadCount => _unreadCountController.value; int get unreadCount => _unreadCountController.value;
bool _countMessageAsUnread(Message message) { bool _countMessageAsUnread(Message message) {
final userId = _channel.client.state.user?.id; final userId = _channel.client.state.currentUser?.id;
final userIsMuted = _channel.client.state.user?.mutes.firstWhereOrNull( final userIsMuted =
_channel.client.state.currentUser?.mutes.firstWhereOrNull(
(m) => m.user.id == message.user?.id, (m) => m.user.id == message.user?.id,
) != ) !=
null; null;
@@ -1795,7 +1796,7 @@ class ChannelClientState {
(event) { (event) {
if (event.user != null) { if (event.user != null) {
final user = event.user!; final user = event.user!;
if (user.id != _channel.client.state.user?.id) { if (user.id != _channel.client.state.currentUser?.id) {
_typings[user] = event; _typings[user] = event;
_typingEventsController.add(_typings); _typingEventsController.add(_typings);
} }
@@ -1808,7 +1809,7 @@ class ChannelClientState {
(event) { (event) {
if (event.user != null) { if (event.user != null) {
final user = event.user!; final user = event.user!;
if (user.id != _channel.client.state.user?.id) { if (user.id != _channel.client.state.currentUser?.id) {
_typings.remove(event.user); _typings.remove(event.user);
_typingEventsController.add(_typings); _typingEventsController.add(_typings);
} }
+42 -40
View File
@@ -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/message.dart';
import 'package:stream_chat/src/core/models/own_user.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/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/core/util/utils.dart';
import 'package:stream_chat/src/db/chat_persistence_client.dart'; import 'package:stream_chat/src/db/chat_persistence_client.dart';
import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/location.dart'; import 'package:stream_chat/src/location.dart';
import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/src/ws/connection_status.dart';
import 'package:stream_chat/src/ws/websocket.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 /// Handler function used for logging records. Function requires a single
/// [LogRecord] as the only parameter. /// [LogRecord] as the only parameter.
@@ -42,6 +44,10 @@ final _levelEmojiMapper = {
Level.SEVERE: '🚨', Level.SEVERE: '🚨',
}; };
final _userAgent = 'stream-chat-dart-client-'
'${CurrentPlatform.name}-'
'${PACKAGE_VERSION.split('+')[0]}';
/// The official Dart client for Stream Chat, /// The official Dart client for Stream Chat,
/// a service for building chat applications. /// a service for building chat applications.
/// This library can be used on any Dart project and on both mobile and web apps /// This library can be used on any Dart project and on both mobile and web apps
@@ -80,6 +86,7 @@ class StreamChatClient {
location: location, location: location,
connectTimeout: connectTimeout, connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout, receiveTimeout: receiveTimeout,
headers: {'X-Stream-Client': _userAgent},
); );
_chatApi = chatApi ?? _chatApi = chatApi ??
@@ -99,6 +106,7 @@ class StreamChatClient {
tokenManager: _tokenManager, tokenManager: _tokenManager,
handler: handleEvent, handler: handleEvent,
logger: detachedLogger('🔌'), logger: detachedLogger('🔌'),
queryParameters: {'X-Stream-Client': _userAgent},
); );
_retryPolicy = retryPolicy ?? _retryPolicy = retryPolicy ??
@@ -308,23 +316,21 @@ class StreamChatClient {
); );
final ownUser = OwnUser.fromUser(user); final ownUser = OwnUser.fromUser(user);
state.user = ownUser; state.currentUser = ownUser;
if (!connectWebSocket) { if (!connectWebSocket) return ownUser;
return ownUser;
}
try { try {
if (_originalChatPersistenceClient != null) { if (_originalChatPersistenceClient != null) {
_chatPersistenceClient = _originalChatPersistenceClient; _chatPersistenceClient = _originalChatPersistenceClient;
await _chatPersistenceClient!.connect(ownUser.id); await _chatPersistenceClient!.connect(ownUser.id);
} }
final res = await openConnection(); final connectedUser = await openConnection();
return res; return state.currentUser = connectedUser;
} catch (e, stk) { } catch (e, stk) {
if (e is StreamWebSocketError && e.isRetriable) { if (e is StreamWebSocketError && e.isRetriable) {
final event = await _chatPersistenceClient?.getConnectionInfo(); 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); logger.severe('error connecting user : ${ownUser.id}', e, stk);
rethrow; rethrow;
@@ -334,12 +340,12 @@ class StreamChatClient {
/// Creates a new WebSocket connection with the current user. /// Creates a new WebSocket connection with the current user.
Future<OwnUser> openConnection() async { Future<OwnUser> openConnection() async {
assert( assert(
state.user != null, state.currentUser != null,
'User is not set on client, ' 'User is not set on client, '
'use `connectUser` or `connectAnonymousUser` instead', 'use `connectUser` or `connectAnonymousUser` instead',
); );
final user = state.user!; final user = state.currentUser!;
logger.info('Opening web-socket connection for ${user.id}'); logger.info('Opening web-socket connection for ${user.id}');
@@ -363,7 +369,7 @@ class StreamChatClient {
try { try {
final event = await _ws.connect(user); final event = await _ws.connect(user);
return event.me?.merge(user) ?? user; return user.merge(event.me);
} catch (e, stk) { } catch (e, stk) {
logger.severe('error connecting ws', e, stk); logger.severe('error connecting ws', e, stk);
rethrow; rethrow;
@@ -378,7 +384,7 @@ class StreamChatClient {
void closeConnection() { void closeConnection() {
if (wsConnectionStatus == ConnectionStatus.disconnected) return; 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; _wsConnectionStatus = ConnectionStatus.disconnected;
_connectionStatusSubscription?.cancel(); _connectionStatusSubscription?.cancel();
@@ -388,9 +394,6 @@ class StreamChatClient {
} }
void _handleHealthCheckEvent(Event event) { void _handleHealthCheckEvent(Event event) {
final user = event.me;
if (user != null) state.user = user;
final connectionId = event.connectionId; final connectionId = event.connectionId;
if (connectionId != null) { if (connectionId != null) {
_connectionIdManager.setConnectionId(connectionId); _connectionIdManager.setConnectionId(connectionId);
@@ -1290,7 +1293,7 @@ class StreamChatClient {
/// If [flushChatPersistence] is true the client deletes all offline /// If [flushChatPersistence] is true the client deletes all offline
/// user's data. /// user's data.
Future<void> disconnectUser({bool flushChatPersistence = false}) async { Future<void> disconnectUser({bool flushChatPersistence = false}) async {
logger.info('Disconnecting user : ${state.user?.id}'); logger.info('Disconnecting user : ${state.currentUser?.id}');
// resetting state // resetting state
state.dispose(); state.dispose();
@@ -1331,22 +1334,6 @@ class ClientState {
/// Creates a new instance listening to events and updating the state /// Creates a new instance listening to events and updating the state
ClientState(this._client) { ClientState(this._client) {
_subscriptions.addAll([ _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 _client
.on() .on()
.map((event) => event.unreadChannels) .map((event) => event.unreadChannels)
@@ -1386,8 +1373,8 @@ class ClientState {
void _listenUserUpdated() { void _listenUserUpdated() {
_subscriptions.add(_client.on(EventType.userUpdated).listen((event) { _subscriptions.add(_client.on(EventType.userUpdated).listen((event) {
if (event.user!.id == user!.id) { if (event.user!.id == currentUser!.id) {
user = OwnUser.fromJson(event.user!.toJson()); currentUser = OwnUser.fromJson(event.user!.toJson());
} }
updateUser(event.user); updateUser(event.user);
})); }));
@@ -1409,9 +1396,10 @@ class ClientState {
final StreamChatClient _client; final StreamChatClient _client;
/// Update user information /// Sets the user currently interacting with the client
set user(OwnUser? user) { /// note: this fully overrides the [currentUser]
_userController.add(user); set currentUser(OwnUser? user) {
_currentUserController.add(user);
} }
/// Update all the [users] with the provided [userList] /// Update all the [users] with the provided [userList]
@@ -1428,10 +1416,24 @@ class ClientState {
void updateUser(User? user) => updateUsers([user]); void updateUser(User? user) => updateUsers([user]);
/// The current user /// The current user
OwnUser? get user => _userController.valueOrNull; OwnUser? get currentUser => _currentUserController.valueOrNull;
/// The current user as a stream /// The current user as a stream
Stream<OwnUser?> get userStream => _userController.stream; Stream<OwnUser?> 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<OwnUser?> get userStream => _currentUserController.stream;
// coverage:ignore-end
/// The current user /// The current user
Map<String, User> get users => _usersController.value; Map<String, User> get users => _usersController.value;
@@ -1463,7 +1465,7 @@ class ClientState {
} }
final _channelsController = BehaviorSubject<Map<String, Channel>>.seeded({}); final _channelsController = BehaviorSubject<Map<String, Channel>>.seeded({});
final _userController = BehaviorSubject<OwnUser?>(); final _currentUserController = BehaviorSubject<OwnUser?>();
final _usersController = BehaviorSubject<Map<String, User>>.seeded({}); final _usersController = BehaviorSubject<Map<String, User>>.seeded({});
final _unreadChannelsController = BehaviorSubject<int>.seeded(0); final _unreadChannelsController = BehaviorSubject<int>.seeded(0);
final _totalUnreadCountController = BehaviorSubject<int>.seeded(0); final _totalUnreadCountController = BehaviorSubject<int>.seeded(0);
@@ -1471,7 +1473,7 @@ class ClientState {
/// Call this method to dispose this object /// Call this method to dispose this object
void dispose() { void dispose() {
_subscriptions.forEach((s) => s.cancel()); _subscriptions.forEach((s) => s.cancel());
_userController.close(); _currentUserController.close();
_unreadChannelsController.close(); _unreadChannelsController.close();
_totalUnreadCountController.close(); _totalUnreadCountController.close();
channels.values.forEach((c) => c.dispose()); channels.values.forEach((c) => c.dispose());
@@ -289,6 +289,7 @@ class ChannelApi {
) async { ) async {
final response = await _client.post( final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/stop-watching', '${_getChannelUrl(channelId, channelType)}/stop-watching',
data: {},
); );
return EmptyResponse.fromJson(response.data); return EmptyResponse.fromJson(response.data);
} }
@@ -75,7 +75,7 @@ class QueryChannelsResponse extends _BaseResponse {
@JsonSerializable(createToJson: false) @JsonSerializable(createToJson: false)
class TranslateMessageResponse extends _BaseResponse { class TranslateMessageResponse extends _BaseResponse {
/// Translated message /// Translated message
late TranslatedMessage message; late Message message;
/// Create a new instance from a json /// Create a new instance from a json
static TranslateMessageResponse fromJson(Map<String, dynamic> json) => static TranslateMessageResponse fromJson(Map<String, dynamic> json) =>
@@ -47,8 +47,7 @@ TranslateMessageResponse _$TranslateMessageResponseFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) {
return TranslateMessageResponse() return TranslateMessageResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..message = ..message = Message.fromJson(json['message'] as Map<String, dynamic>);
TranslatedMessage.fromJson(json['message'] as Map<String, dynamic>);
} }
QueryMembersResponse _$QueryMembersResponseFromJson(Map<String, dynamic> json) { QueryMembersResponse _$QueryMembersResponseFromJson(Map<String, dynamic> json) {
@@ -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/interceptor/logging_interceptor.dart';
import 'package:stream_chat/src/core/http/stream_chat_dio_error.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/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/src/location.dart';
import 'package:stream_chat/version.dart';
part 'stream_http_client_options.dart'; part 'stream_http_client_options.dart';
@@ -33,11 +31,14 @@ class StreamHttpClient {
..options.baseUrl = _options.baseUrl ..options.baseUrl = _options.baseUrl
..options.receiveTimeout = _options.receiveTimeout.inMilliseconds ..options.receiveTimeout = _options.receiveTimeout.inMilliseconds
..options.connectTimeout = _options.connectTimeout.inMilliseconds ..options.connectTimeout = _options.connectTimeout.inMilliseconds
..options.queryParameters = {'api_key': apiKey} ..options.queryParameters = {
'api_key': apiKey,
..._options.queryParameters,
}
..options.headers = { ..options.headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Stream-Client': _options.userAgent,
'Content-Encoding': 'application/gzip', 'Content-Encoding': 'application/gzip',
..._options.headers,
} }
..interceptors.addAll([ ..interceptors.addAll([
if (tokenManager != null) AuthInterceptor(this, tokenManager), if (tokenManager != null) AuthInterceptor(this, tokenManager),
@@ -10,6 +10,8 @@ class StreamHttpClientOptions {
this.location, this.location,
this.connectTimeout = const Duration(seconds: 6), this.connectTimeout = const Duration(seconds: 6),
this.receiveTimeout = const Duration(seconds: 6), this.receiveTimeout = const Duration(seconds: 6),
this.queryParameters = const {},
this.headers = const {},
}) : _baseUrl = baseUrl ?? _defaultBaseURL; }) : _baseUrl = baseUrl ?? _defaultBaseURL;
final String _baseUrl; final String _baseUrl;
@@ -32,8 +34,20 @@ class StreamHttpClientOptions {
/// received timeout, default to 6s /// received timeout, default to 6s
final Duration receiveTimeout; final Duration receiveTimeout;
/// Get the current user agent /// Common query parameters.
String get userAgent => 'stream-chat-dart-client-' ///
'${CurrentPlatform.name}-' /// List values use the default [ListFormat.multiCompatible].
'${PACKAGE_VERSION.split('+')[0]}'; ///
/// The value can be overridden per parameter by adding a [MultiParam]
/// object wrapping the actual List value and the desired format.
final Map<String, Object?> 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<String, Object?> headers;
} }
@@ -2,8 +2,8 @@ import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/core/models/attachment.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/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/models/user.dart';
import 'package:stream_chat/src/core/util/serializer.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
part 'message.g.dart'; part 'message.g.dart';
@@ -73,6 +73,7 @@ class Message extends Equatable {
this.extraData = const {}, this.extraData = const {},
this.deletedAt, this.deletedAt,
this.status = MessageSendingStatus.sent, this.status = MessageSendingStatus.sent,
this.i18n,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
pinExpires = pinExpires?.toUtc(), pinExpires = pinExpires?.toUtc(),
createdAt = createdAt ?? DateTime.now(), createdAt = createdAt ?? DateTime.now(),
@@ -218,6 +219,10 @@ class Message extends Equatable {
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly) @JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final DateTime? deletedAt; final DateTime? deletedAt;
/// A Map of translations.
@JsonKey(includeIfNull: false)
final Map<String, String>? i18n;
/// Known top level fields. /// Known top level fields.
/// Useful for [Serializer] methods. /// Useful for [Serializer] methods.
static const topLevelFields = [ static const topLevelFields = [
@@ -248,6 +253,7 @@ class Message extends Equatable {
'pinned_at', 'pinned_at',
'pin_expires', 'pin_expires',
'pinned_by', 'pinned_by',
'i18n',
]; ];
/// Serialize to json /// Serialize to json
@@ -285,6 +291,7 @@ class Message extends Equatable {
User? pinnedBy, User? pinnedBy,
Map<String, Object?>? extraData, Map<String, Object?>? extraData,
MessageSendingStatus? status, MessageSendingStatus? status,
Map<String, String>? i18n,
}) { }) {
assert(() { assert(() {
if (pinExpires is! DateTime && if (pinExpires is! DateTime &&
@@ -324,6 +331,7 @@ class Message extends Equatable {
pinnedBy: pinnedBy ?? this.pinnedBy, pinnedBy: pinnedBy ?? this.pinnedBy,
pinExpires: pinExpires:
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?, pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
i18n: i18n ?? this.i18n,
); );
} }
@@ -358,6 +366,7 @@ class Message extends Equatable {
pinnedAt: other.pinnedAt, pinnedAt: other.pinnedAt,
pinExpires: other.pinExpires, pinExpires: other.pinExpires,
pinnedBy: other.pinnedBy, pinnedBy: other.pinnedBy,
i18n: other.i18n,
); );
@override @override
@@ -390,35 +399,6 @@ class Message extends Equatable {
pinnedBy, pinnedBy,
extraData, extraData,
status, 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<String, dynamic> json) =>
_$TranslatedMessageFromJson(
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
);
/// A Map of
final Map<String, String>? i18n;
/// Known top level fields.
/// Useful for [Serializer] methods.
static final topLevelFields = [
'i18n',
...Message.topLevelFields,
];
/// Serialize to json
@override
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
_$TranslatedMessageToJson(this),
);
}
@@ -67,6 +67,9 @@ Message _$MessageFromJson(Map<String, dynamic> json) {
deletedAt: json['deleted_at'] == null deletedAt: json['deleted_at'] == null
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
i18n: (json['i18n'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
),
); );
} }
@@ -107,18 +110,6 @@ Map<String, dynamic> _$MessageToJson(Message instance) {
val['pinned_by'] = readonly(instance.pinnedBy); val['pinned_by'] = readonly(instance.pinnedBy);
val['extra_data'] = instance.extraData; val['extra_data'] = instance.extraData;
writeNotNull('deleted_at', readonly(instance.deletedAt)); writeNotNull('deleted_at', readonly(instance.deletedAt));
writeNotNull('i18n', instance.i18n);
return val; return val;
} }
TranslatedMessage _$TranslatedMessageFromJson(Map<String, dynamic> json) {
return TranslatedMessage(
(json['i18n'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
),
);
}
Map<String, dynamic> _$TranslatedMessageToJson(TranslatedMessage instance) =>
<String, dynamic>{
'i18n': instance.i18n,
};
@@ -27,6 +27,7 @@ class OwnUser extends User {
Map<String, Object?> extraData = const {}, Map<String, Object?> extraData = const {},
bool banned = false, bool banned = false,
List<String> teams = const [], List<String> teams = const [],
String? language,
}) : super( }) : super(
id: id, id: id,
role: role, role: role,
@@ -37,6 +38,7 @@ class OwnUser extends User {
extraData: extraData, extraData: extraData,
banned: banned, banned: banned,
teams: teams, teams: teams,
language: language,
); );
/// Create a new instance from a json /// Create a new instance from a json
@@ -54,6 +56,7 @@ class OwnUser extends User {
banned: user.banned, banned: user.banned,
extraData: user.extraData, extraData: user.extraData,
teams: user.teams, teams: user.teams,
language: user.language,
); );
/// Creates a copy of [OwnUser] with specified attributes overridden. /// Creates a copy of [OwnUser] with specified attributes overridden.
@@ -73,6 +76,7 @@ class OwnUser extends User {
List<Mute>? mutes, List<Mute>? mutes,
int? totalUnreadCount, int? totalUnreadCount,
int? unreadChannels, int? unreadChannels,
String? language,
}) => }) =>
OwnUser( OwnUser(
id: id ?? this.id, id: id ?? this.id,
@@ -89,15 +93,13 @@ class OwnUser extends User {
mutes: mutes ?? this.mutes, mutes: mutes ?? this.mutes,
totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount,
unreadChannels: unreadChannels ?? this.unreadChannels, unreadChannels: unreadChannels ?? this.unreadChannels,
language: language ?? this.language,
); );
/// Returns a new [OwnUser] that is a combination of this ownUser /// Returns a new [OwnUser] that is a combination of this ownUser
/// and the given [other] ownUser. /// and the given [other] ownUser.
OwnUser merge(OwnUser? other) { OwnUser merge(OwnUser? other) {
if (other == null) { if (other == null) return this;
return this;
}
return copyWith( return copyWith(
banned: other.banned, banned: other.banned,
channelMutes: other.channelMutes, channelMutes: other.channelMutes,
@@ -113,6 +115,7 @@ class OwnUser extends User {
totalUnreadCount: other.totalUnreadCount, totalUnreadCount: other.totalUnreadCount,
unreadChannels: other.unreadChannels, unreadChannels: other.unreadChannels,
updatedAt: other.updatedAt, updatedAt: other.updatedAt,
language: other.language,
); );
} }
@@ -36,5 +36,9 @@ OwnUser _$OwnUserFromJson(Map<String, dynamic> json) {
online: json['online'] as bool? ?? false, online: json['online'] as bool? ?? false,
extraData: json['extra_data'] as Map<String, dynamic>? ?? {}, extraData: json['extra_data'] as Map<String, dynamic>? ?? {},
banned: json['banned'] as bool? ?? false, banned: json['banned'] as bool? ?? false,
teams:
(json['teams'] as List<dynamic>?)?.map((e) => e as String).toList() ??
[],
language: json['language'] as String?,
); );
} }
@@ -18,6 +18,7 @@ class User extends Equatable {
this.extraData = const {}, this.extraData = const {},
this.banned = false, this.banned = false,
this.teams = const [], this.teams = const [],
this.language,
}) : createdAt = createdAt ?? DateTime.now(), }) : createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now(); updatedAt = updatedAt ?? DateTime.now();
@@ -36,6 +37,7 @@ class User extends Equatable {
'online', 'online',
'banned', 'banned',
'teams', 'teams',
'language',
]; ];
/// User id /// User id
@@ -82,8 +84,11 @@ class User extends Equatable {
) )
final Map<String, Object?> extraData; final Map<String, Object?> extraData;
@override /// The language this user prefers.
int get hashCode => id.hashCode; ///
/// Defaults to 'en'.
@JsonKey(includeIfNull: false)
final String? language;
/// Shortcut for user name /// Shortcut for user name
String get name { String get name {
@@ -98,11 +103,6 @@ class User extends Equatable {
static List<String>? toIds(List<User>? users) => static List<String>? toIds(List<User>? users) =>
users?.map((u) => u.id).toList(); 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 /// Serialize to json
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot( Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
_$UserToJson(this), _$UserToJson(this),
@@ -119,6 +119,7 @@ class User extends Equatable {
Map<String, Object?>? extraData, Map<String, Object?>? extraData,
bool? banned, bool? banned,
List<String>? teams, List<String>? teams,
String? language,
}) => }) =>
User( User(
id: id ?? this.id, id: id ?? this.id,
@@ -130,18 +131,9 @@ class User extends Equatable {
extraData: extraData ?? this.extraData, extraData: extraData ?? this.extraData,
banned: banned ?? this.banned, banned: banned ?? this.banned,
teams: teams ?? this.teams, teams: teams ?? this.teams,
language: language ?? this.language,
); );
@override @override
List<Object?> get props => [ List<Object?> get props => [id];
id,
role,
teams,
createdAt,
updatedAt,
lastActive,
online,
banned,
extraData,
];
} }
@@ -25,6 +25,7 @@ User _$UserFromJson(Map<String, dynamic> json) {
teams: teams:
(json['teams'] as List<dynamic>?)?.map((e) => e as String).toList() ?? (json['teams'] as List<dynamic>?)?.map((e) => e as String).toList() ??
[], [],
language: json['language'] as String?,
); );
} }
@@ -47,5 +48,6 @@ Map<String, dynamic> _$UserToJson(User instance) {
writeNotNull('online', readonly(instance.online)); writeNotNull('online', readonly(instance.online));
writeNotNull('banned', readonly(instance.banned)); writeNotNull('banned', readonly(instance.banned));
val['extra_data'] = instance.extraData; val['extra_data'] = instance.extraData;
writeNotNull('language', instance.language);
return val; return val;
} }
@@ -41,11 +41,15 @@ class WebSocket with TimerHelper {
this.reconnectionMonitorInterval = 10, this.reconnectionMonitorInterval = 10,
this.healthCheckInterval = 20, this.healthCheckInterval = 20,
this.reconnectionMonitorTimeout = 40, this.reconnectionMonitorTimeout = 40,
this.queryParameters = const {},
}) : _logger = logger; }) : _logger = logger;
/// ///
final String apiKey; final String apiKey;
/// Additional query parameters to be added to the websocket url
final Map<String, Object?> queryParameters;
/// WS base url /// WS base url
final String baseUrl; final String baseUrl;
@@ -156,6 +160,7 @@ class WebSocket with TimerHelper {
'api_key': apiKey, 'api_key': apiKey,
'authorization': token.rawValue, 'authorization': token.rawValue,
'stream-auth-type': token.authType.raw, 'stream-auth-type': token.authType.raw,
...queryParameters,
}; };
final scheme = baseUrl.startsWith('https') ? 'wss' : 'ws'; final scheme = baseUrl.startsWith('https') ? 'wss' : 'ws';
final host = baseUrl.replaceAll(RegExp(r'(^\w+:|^)\/\/'), ''); final host = baseUrl.replaceAll(RegExp(r'(^\w+:|^)\/\/'), '');
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version /// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header /// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names // ignore: constant_identifier_names
const PACKAGE_VERSION = '2.0.0'; const PACKAGE_VERSION = '2.1.0';
+1 -1
View File
@@ -1,7 +1,7 @@
name: stream_chat name: stream_chat
homepage: https://getstream.io/ homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications. 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 repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
+2 -1
View File
@@ -1,5 +1,6 @@
{ {
"id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e",
"role": "test-role", "role": "test-role",
"name": "John" "name": "John",
"language": "en"
} }
@@ -1610,9 +1610,12 @@ void main() {
const messageId = 'test-message-id'; const messageId = 'test-message-id';
const language = 'hi'; // Hindi const language = 'hi'; // Hindi
const translatedMessageText = 'नमस्ते'; const translatedMessageText = 'नमस्ते';
final translatedMessage = TranslatedMessage(const {
final translatedMessage = Message(
i18n: const {
language: translatedMessageText, language: translatedMessageText,
}); },
);
when(() => client.translateMessage(messageId, language)).thenAnswer( when(() => client.translateMessage(messageId, language)).thenAnswer(
(_) async => TranslateMessageResponse()..message = translatedMessage, (_) async => TranslateMessageResponse()..message = translatedMessage,
@@ -155,7 +155,7 @@ void main() {
group('`.openConnection`', () { group('`.openConnection`', () {
test('should throw if state does not contain user', () async { test('should throw if state does not contain user', () async {
expect(client.state.user, isNull); expect(client.state.currentUser, isNull);
try { try {
await client.openConnection(); await client.openConnection();
} catch (e) { } catch (e) {
@@ -164,7 +164,7 @@ void main() {
}); });
test('should throw if connection is already in progress', () async { test('should throw if connection is already in progress', () async {
expect(client.state.user, isNull); expect(client.state.currentUser, isNull);
try { try {
await client.connectAnonymousUser(); await client.connectAnonymousUser();
await client.openConnection(); await client.openConnection();
@@ -179,7 +179,7 @@ void main() {
}); });
test('should throw if connection is already available', () async { test('should throw if connection is already available', () async {
expect(client.state.user, isNull); expect(client.state.currentUser, isNull);
try { try {
await client.connectAnonymousUser(); await client.connectAnonymousUser();
// waiting 300ms for `wsConnectionStatusStream` to emit // waiting 300ms for `wsConnectionStatusStream` to emit
@@ -799,7 +799,7 @@ void main() {
}); });
test('`.disconnectUser` should reset state and user', () async { test('`.disconnectUser` should reset state and user', () async {
expect(client.state.user, isNotNull); expect(client.state.currentUser, isNotNull);
expect(client.wsConnectionStatus, ConnectionStatus.connected); expect(client.wsConnectionStatus, ConnectionStatus.connected);
expectLater( expectLater(
@@ -810,7 +810,7 @@ void main() {
await client.disconnectUser(); await client.disconnectUser();
expect(client.state.user, isNull); expect(client.state.currentUser, isNull);
expect(client.wsConnectionStatus, ConnectionStatus.disconnected); expect(client.wsConnectionStatus, ConnectionStatus.disconnected);
}); });
}); });
@@ -2109,9 +2109,11 @@ void main() {
const messageId = 'test-message-id'; const messageId = 'test-message-id';
const language = 'hi'; // Hindi const language = 'hi'; // Hindi
const translatedMessageText = 'नमस्ते'; const translatedMessageText = 'नमस्ते';
final translatedMessage = TranslatedMessage(const { final translatedMessage = Message(
i18n: const {
language: translatedMessageText, language: translatedMessageText,
}); },
);
when(() => api.message.translateMessage(messageId, language)).thenAnswer( when(() => api.message.translateMessage(messageId, language)).thenAnswer(
(_) async => TranslateMessageResponse()..message = translatedMessage, (_) async => TranslateMessageResponse()..message = translatedMessage,
@@ -595,14 +595,14 @@ void main() {
final path = '${_getChannelUrl(channelId, channelType)}/stop-watching'; final path = '${_getChannelUrl(channelId, channelType)}/stop-watching';
when(() => client.post(path)).thenAnswer( when(() => client.post(path, data: {})).thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{})); (_) async => successResponse(path, data: <String, dynamic>{}));
final res = await channelApi.stopWatching(channelId, channelType); final res = await channelApi.stopWatching(channelId, channelType);
expect(res, isNotNull); expect(res, isNotNull);
verify(() => client.post(path)).called(1); verify(() => client.post(path, data: {})).called(1);
verifyNoMoreInteractions(client); verifyNoMoreInteractions(client);
}); });
} }
@@ -371,9 +371,11 @@ void main() {
final path = '/messages/${message.id}/translate'; final path = '/messages/${message.id}/translate';
const translatedMessageText = 'नमस्ते'; const translatedMessageText = 'नमस्ते';
final translatedMessage = TranslatedMessage(const { final translatedMessage = Message(
i18n: const {
language: translatedMessageText, language: translatedMessageText,
}); },
);
when(() => client.post( when(() => client.post(
path, path,
@@ -9,6 +9,8 @@ void main() {
expect(options.baseUrl, 'https://chat-us-east-1.stream-io-api.com'); expect(options.baseUrl, 'https://chat-us-east-1.stream-io-api.com');
expect(options.connectTimeout, const Duration(seconds: 6)); expect(options.connectTimeout, const Duration(seconds: 6));
expect(options.receiveTimeout, 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', () { test('should override all the default set params', () {
@@ -16,11 +18,15 @@ void main() {
baseUrl: 'base-url', baseUrl: 'base-url',
connectTimeout: Duration(seconds: 3), connectTimeout: Duration(seconds: 3),
receiveTimeout: Duration(seconds: 3), receiveTimeout: Duration(seconds: 3),
headers: {'test': 'test'},
queryParameters: {'123': '123'},
); );
expect(options.location, isNull); expect(options.location, isNull);
expect(options.baseUrl, 'base-url'); expect(options.baseUrl, 'base-url');
expect(options.connectTimeout, const Duration(seconds: 3)); expect(options.connectTimeout, const Duration(seconds: 3));
expect(options.receiveTimeout, 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', () { group('should create baseUrl according to provided location', () {
@@ -96,7 +96,7 @@ void main() {
await client.get('path'); await client.get('path');
} catch (_) {} } catch (_) {}
verify(() => logger.info(any())).called(16); verify(() => logger.info(any())).called(greaterThan(0));
}); });
test('loggingInterceptor should log error', () async { test('loggingInterceptor should log error', () async {
@@ -108,7 +108,7 @@ void main() {
await client.get('path'); await client.get('path');
} catch (_) {} } catch (_) {}
verify(() => logger.severe(any())).called(8); verify(() => logger.severe(any())).called(greaterThan(0));
}); });
test('`.lock` should lock the dio client', () async { test('`.lock` should lock the dio client', () async {
@@ -28,6 +28,7 @@ void main() {
expect(message.pinnedAt, null); expect(message.pinnedAt, null);
expect(message.pinExpires, null); expect(message.pinExpires, null);
expect(message.pinnedBy, null); expect(message.pinnedBy, null);
expect(message.i18n, null);
}); });
test('should serialize to json correctly', () { test('should serialize to json correctly', () {
@@ -29,6 +29,7 @@ void main() {
expect(newUser.id, user.id); expect(newUser.id, user.id);
expect(newUser.role, user.role); expect(newUser.role, user.role);
expect(newUser.name, user.name); expect(newUser.name, user.name);
expect(newUser.language, user.language);
newUser = user.copyWith( newUser = user.copyWith(
id: 'test', id: 'test',
@@ -41,6 +42,7 @@ void main() {
expect(newUser.id, 'test'); expect(newUser.id, 'test');
expect(newUser.role, 'test'); expect(newUser.role, 'test');
expect(newUser.name, 'test'); expect(newUser.name, 'test');
expect(newUser.language, 'en');
}); });
}); });
} }
+1 -1
View File
@@ -89,7 +89,7 @@ class FakeChatApi extends Fake implements StreamChatApi {
class FakeClientState extends Fake implements ClientState { class FakeClientState extends Fake implements ClientState {
@override @override
OwnUser? get user => OwnUser(id: 'test-user-id'); OwnUser? get currentUser => OwnUser(id: 'test-user-id');
@override @override
int totalUnreadCount = 0; int totalUnreadCount = 0;
+18
View File
@@ -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 ## 2.0.0
🛑️ Breaking Changes from `1.5.4` 🛑️ Breaking Changes from `1.5.4`
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion 29 compileSdkVersion 30
sourceSets { sourceSets {
main.java.srcDirs += 'src/main/kotlin' 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). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example" applicationId "com.example.example"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 30
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
@@ -1,12 +1,12 @@
buildscript { buildscript {
ext.kotlin_version = '1.3.50' ext.kotlin_version = '1.5.20'
repositories { repositories {
google() google()
jcenter() jcenter()
} }
dependencies { 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" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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
@@ -1,11 +1,8 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_persistence/stream_chat_persistence.dart'; import 'package:stream_chat_localizations/stream_chat_localizations.dart';
/// A chat-persisted StreamChatClient
final chatPersistentClient = StreamChatPersistenceClient(
logLevel: Level.INFO,
);
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@@ -15,7 +12,7 @@ void main() async {
final client = StreamChatClient( final client = StreamChatClient(
's2dxdhpxd94g', 's2dxdhpxd94g',
logLevel: Level.INFO, logLevel: Level.INFO,
)..chatPersistenceClient = chatPersistentClient; );
/// Set the current user and connect the websocket. In a production /// Set the current user and connect the websocket. In a production
/// scenario, this should be done using a backend to generate a user token /// 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( Widget build(BuildContext context) => MaterialApp(
theme: ThemeData.light(), theme: ThemeData.light(),
darkTheme: ThemeData.dark(), darkTheme: ThemeData.dark(),
supportedLocales: const [
Locale('en'),
Locale('hi'),
Locale('fr'),
Locale('it'),
],
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
builder: (context, widget) => StreamChat( builder: (context, widget) => StreamChat(
client: client, client: client,
child: widget, child: widget,
@@ -103,7 +103,7 @@ class ChannelListPage extends StatelessWidget {
: null, : null,
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).user!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
pagination: const PaginationParams( pagination: const PaginationParams(
@@ -80,7 +80,7 @@ class ChannelListPage extends StatelessWidget {
child: ChannelListView( child: ChannelListView(
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).user!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
pagination: const PaginationParams( pagination: const PaginationParams(
@@ -81,7 +81,7 @@ class ChannelListPage extends StatelessWidget {
child: ChannelListView( child: ChannelListView(
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).user!.id], [StreamChat.of(context).currentUser!.id],
), ),
channelPreviewBuilder: _channelPreviewBuilder, channelPreviewBuilder: _channelPreviewBuilder,
// sort: [SortOption('last_message_at')], // sort: [SortOption('last_message_at')],
@@ -66,7 +66,7 @@ class ChannelListPage extends StatelessWidget {
child: ChannelListView( child: ChannelListView(
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).user!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
pagination: const PaginationParams( pagination: const PaginationParams(
@@ -72,7 +72,7 @@ class ChannelListPage extends StatelessWidget {
child: ChannelListView( child: ChannelListView(
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).user!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
pagination: const PaginationParams( pagination: const PaginationParams(
@@ -115,7 +115,8 @@ class ChannelPage extends StatelessWidget {
MessageWidget _, MessageWidget _,
) { ) {
final message = details.message; 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 textAlign = isCurrentUser ? TextAlign.right : TextAlign.left;
final color = isCurrentUser ? Colors.blueGrey : Colors.blue; final color = isCurrentUser ? Colors.blueGrey : Colors.blue;
@@ -99,7 +99,7 @@ class ChannelListPage extends StatelessWidget {
child: ChannelListView( child: ChannelListView(
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).user!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
pagination: const PaginationParams( pagination: const PaginationParams(
@@ -0,0 +1,6 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/xcuserdata/
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -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 = "<group>"; };
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 = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
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 = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
/* 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 = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
35E4E72D48C70FBCFEDFB30C /* Pods */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* example.app */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
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 = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
5ED1F4FA50EB1433A201473C /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* 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 = "<group>";
};
/* 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 */;
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,9 @@
import Cocoa
import FlutterMacOS
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,339 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</window>
</objects>
</document>
@@ -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.
@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"
@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "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
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
@@ -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()
}
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>
@@ -33,6 +33,8 @@ dependencies:
# path: ../../stream_chat_flutter_core # path: ../../stream_chat_flutter_core
stream_chat_flutter: stream_chat_flutter:
path: ../ path: ../
stream_chat_localizations:
path: ../../stream_chat_localizations
stream_chat_persistence: stream_chat_persistence:
path: ../../stream_chat_persistence path: ../../stream_chat_persistence
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/upload_progress_indicator.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/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget to build in progress /// Widget to build in progress
typedef InProgressBuilder = Widget Function(BuildContext, int, int); typedef InProgressBuilder = Widget Function(BuildContext, int, int);
@@ -226,7 +227,7 @@ class _FailedState extends StatelessWidget {
horizontal: 12, horizontal: 12,
), ),
child: Text( child: Text(
'UPLOAD ERROR', context.translations.uploadErrorLabel,
style: theme.textTheme.footnote.copyWith( style: theme.textTheme.footnote.copyWith(
color: theme.colorTheme.barsBg, color: theme.colorTheme.barsBg,
), ),
@@ -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/utils.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
// ignore: always_use_package_imports import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
import 'attachment_widget.dart';
/// Widget for displaying file attachments /// Widget for displaying file attachments
class FileAttachment extends AttachmentWidget { class FileAttachment extends AttachmentWidget {
@@ -76,7 +76,7 @@ class FileAttachment extends AttachmentWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
attachment.title ?? 'File', attachment.title ?? context.translations.fileText,
style: StreamChatTheme.of(context).textTheme.bodyBold, style: StreamChatTheme.of(context).textTheme.bodyBold,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -286,7 +286,10 @@ class FileAttachment extends AttachmentWidget {
progressIndicatorColor: theme.colorTheme.accentPrimary, progressIndicatorColor: theme.colorTheme.accentPrimary,
), ),
success: () => Text(fileSize(size), style: textStyle), success: () => Text(fileSize(size), style: textStyle),
failed: (_) => Text('UPLOAD ERROR', style: textStyle), failed: (_) => Text(
context.translations.uploadErrorLabel,
style: textStyle,
),
); );
} }
} }
@@ -2,8 +2,10 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_widget.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/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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 /// Widget for showing a GIF attachment
class GiphyAttachment extends AttachmentWidget { class GiphyAttachment extends AttachmentWidget {
@@ -71,9 +73,9 @@ class GiphyAttachment extends AttachmentWidget {
children: [ children: [
StreamSvgIcon.giphyIcon(), StreamSvgIcon.giphyIcon(),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text( Text(
'Giphy', context.translations.giphyLabel,
style: TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
if (attachment.title != null) if (attachment.title != null)
@@ -134,7 +136,7 @@ class GiphyAttachment extends AttachmentWidget {
}); });
}, },
child: Text( child: Text(
'Cancel', context.translations.cancelLabel.toLowerCase(),
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
.textTheme .textTheme
.bodyBold .bodyBold
@@ -166,7 +168,7 @@ class GiphyAttachment extends AttachmentWidget {
}); });
}, },
child: Text( child: Text(
'Shuffle', context.translations.shuffleLabel,
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
.textTheme .textTheme
.bodyBold .bodyBold
@@ -199,7 +201,7 @@ class GiphyAttachment extends AttachmentWidget {
}); });
}, },
child: Text( child: Text(
'Send', context.translations.sendLabel,
style: TextStyle( style: TextStyle(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
@@ -216,36 +218,11 @@ class GiphyAttachment extends AttachmentWidget {
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Align( const Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row( child: VisibleFootnote(),
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)),
),
],
),
), ),
), ),
], ],
@@ -339,7 +316,7 @@ class GiphyAttachment extends AttachmentWidget {
size: 16, size: 16,
), ),
Text( Text(
'GIPHY', context.translations.giphyLabel.toUpperCase(),
style: TextStyle( style: TextStyle(
color: color:
StreamChatTheme.of(context).colorTheme.barsBg, StreamChatTheme.of(context).colorTheme.barsBg,
@@ -48,7 +48,7 @@ class AttachmentActionsModal extends StatelessWidget {
child: _buildPage(context), child: _buildPage(context),
); );
Widget _buildPage(context) { Widget _buildPage(BuildContext context) {
final theme = StreamChatTheme.of(context); final theme = StreamChatTheme.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
@@ -69,7 +69,7 @@ class AttachmentActionsModal extends StatelessWidget {
children: [ children: [
_buildButton( _buildButton(
context, context,
'Reply', context.translations.replyLabel,
StreamSvgIcon.iconCurveLineLeftUp( StreamSvgIcon.iconCurveLineLeftUp(
size: 24, size: 24,
color: theme.colorTheme.textLowEmphasis, color: theme.colorTheme.textLowEmphasis,
@@ -80,7 +80,7 @@ class AttachmentActionsModal extends StatelessWidget {
), ),
_buildButton( _buildButton(
context, context,
'Show in Chat', context.translations.showInChatLabel,
StreamSvgIcon.eye( StreamSvgIcon.eye(
size: 24, size: 24,
color: theme.colorTheme.textHighEmphasis, color: theme.colorTheme.textHighEmphasis,
@@ -89,8 +89,9 @@ class AttachmentActionsModal extends StatelessWidget {
), ),
_buildButton( _buildButton(
context, context,
// ignore: lines_longer_than_80_chars message.attachments[currentIndex].type == 'video'
'Save ${message.attachments[currentIndex].type == 'video' ? 'Video' : 'Image'}', ? context.translations.saveVideoLabel
: context.translations.saveImageLabel,
StreamSvgIcon.iconSave( StreamSvgIcon.iconSave(
size: 24, size: 24,
color: theme.colorTheme.textLowEmphasis, 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( _buildButton(
context, context,
'Delete', context.translations.deleteLabel.capitalize(),
StreamSvgIcon.delete( StreamSvgIcon.delete(
size: 24, size: 24,
color: theme.colorTheme.accentError, color: theme.colorTheme.accentError,
@@ -141,7 +141,7 @@ class ChannelAvatar extends StatelessWidget {
return child; return child;
} }
final currentUser = streamChat.user!; final currentUser = streamChat.currentUser!;
final otherMembers = channel.state!.members final otherMembers = channel.state!.members
.where((it) => it.userId != currentUser.id) .where((it) => it.userId != currentUser.id)
.toList(growable: false); .toList(growable: false);
@@ -149,7 +149,7 @@ class ChannelAvatar extends StatelessWidget {
// our own space, no other members // our own space, no other members
if (otherMembers.isEmpty) { if (otherMembers.isEmpty) {
return BetterStreamBuilder<User>( return BetterStreamBuilder<User>(
stream: streamChat.client.state.userStream.map((it) => it!), stream: streamChat.client.state.currentUserStream.map((it) => it!),
initialData: currentUser, initialData: currentUser,
builder: (context, user) => UserAvatar( builder: (context, user) => UserAvatar(
borderRadius: borderRadius ?? previewTheme?.borderRadius, borderRadius: borderRadius ?? previewTheme?.borderRadius,
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/src/channel_info.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Bottom Sheet with options /// Bottom Sheet with options
class ChannelBottomSheet extends StatefulWidget { class ChannelBottomSheet extends StatefulWidget {
@@ -27,8 +28,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
final members = channel.state?.members ?? []; final members = channel.state?.members ?? [];
final userAsMember = final userAsMember = members
members.firstWhere((e) => e.user?.id == _streamChatState.user?.id); .firstWhere((e) => e.user?.id == _streamChatState.currentUser?.id);
final isOwner = userAsMember.role == 'owner'; final isOwner = userAsMember.role == 'owner';
return Material( return Material(
@@ -149,7 +150,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
color: _streamChatThemeData.colorTheme.textLowEmphasis, color: _streamChatThemeData.colorTheme.textLowEmphasis,
), ),
), ),
title: 'View Info', title: context.translations.viewInfoLabel,
onTap: widget.onViewInfoTap, onTap: widget.onViewInfoTap,
), ),
if (!channel.isDistinct) if (!channel.isDistinct)
@@ -160,7 +161,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
color: _streamChatThemeData.colorTheme.textLowEmphasis, color: _streamChatThemeData.colorTheme.textLowEmphasis,
), ),
), ),
title: 'Leave Group', title: context.translations.leaveGroupLabel,
onTap: () async { onTap: () async {
setState(() { setState(() {
_showActions = false; _showActions = false;
@@ -179,7 +180,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
color: _streamChatThemeData.colorTheme.accentError, color: _streamChatThemeData.colorTheme.accentError,
), ),
), ),
title: 'Delete Conversation', title: context.translations.deleteConversationLabel,
titleColor: _streamChatThemeData.colorTheme.accentError, titleColor: _streamChatThemeData.colorTheme.accentError,
onTap: () async { onTap: () async {
setState(() { setState(() {
@@ -198,7 +199,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
color: _streamChatThemeData.colorTheme.textLowEmphasis, color: _streamChatThemeData.colorTheme.textLowEmphasis,
), ),
), ),
title: 'Cancel', title: context.translations.cancelLabel,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
}, },
@@ -219,10 +220,10 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
Future<void> _showDeleteDialog() async { Future<void> _showDeleteDialog() async {
final res = await showConfirmationDialog( final res = await showConfirmationDialog(
context, context,
title: 'Delete Conversation', title: context.translations.deleteConversationLabel,
okText: 'DELETE', okText: context.translations.deleteLabel,
question: 'Are you sure you want to delete this conversation?', question: context.translations.deleteConversationQuestion,
cancelText: 'CANCEL', cancelText: context.translations.cancelLabel,
icon: StreamSvgIcon.delete( icon: StreamSvgIcon.delete(
color: _streamChatThemeData.colorTheme.accentError, color: _streamChatThemeData.colorTheme.accentError,
), ),
@@ -237,17 +238,17 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
Future<void> _showLeaveDialog() async { Future<void> _showLeaveDialog() async {
final res = await showConfirmationDialog( final res = await showConfirmationDialog(
context, context,
title: 'Leave conversation', title: context.translations.leaveConversationLabel,
okText: 'LEAVE', okText: context.translations.leaveLabel,
question: 'Are you sure you want to leave this conversation?', question: context.translations.leaveConversationQuestion,
cancelText: 'CANCEL', cancelText: context.translations.cancelLabel,
icon: StreamSvgIcon.userRemove( icon: StreamSvgIcon.userRemove(
color: _streamChatThemeData.colorTheme.accentError, color: _streamChatThemeData.colorTheme.accentError,
), ),
); );
if (res == true) { if (res == true) {
final channel = _streamChannelState.channel; final channel = _streamChannelState.channel;
final user = _streamChatState.user; final user = _streamChatState.currentUser;
if (user != null) { if (user != null) {
await channel.removeMembers([user.id]); await channel.removeMembers([user.id]);
} }
@@ -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/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_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.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header_paint.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) { switch (status) {
case ConnectionStatus.connected: case ConnectionStatus.connected:
statusString = 'Connected'; statusString = context.translations.connectedLabel;
showStatus = false; showStatus = false;
break; break;
case ConnectionStatus.connecting: case ConnectionStatus.connecting:
statusString = 'Reconnecting...'; statusString = context.translations.reconnectingLabel;
break; break;
case ConnectionStatus.disconnected: case ConnectionStatus.disconnected:
statusString = 'Disconnected'; statusString = context.translations.disconnectedLabel;
break; break;
} }
@@ -2,6 +2,7 @@ import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget which shows channel info /// Widget which shows channel info
class ChannelInfo extends StatelessWidget { class ChannelInfo extends StatelessWidget {
@@ -55,10 +56,13 @@ class ChannelInfo extends StatelessWidget {
) { ) {
Widget? alternativeWidget; Widget? alternativeWidget;
if (channel.memberCount != null && channel.memberCount! > 2) { final memberCount = channel.memberCount;
var text = '${channel.memberCount} Members'; if (memberCount != null && memberCount > 2) {
var text = context.translations.membersCountText(memberCount);
final watcherCount = channel.state?.watcherCount ?? 0; final watcherCount = channel.state?.watcherCount ?? 0;
if (watcherCount > 0) text += ' $watcherCount Online'; if (watcherCount > 0) {
text += ' ${context.translations.watchersCountText(watcherCount)}';
}
alternativeWidget = Text( alternativeWidget = Text(
text, text,
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
@@ -67,7 +71,7 @@ class ChannelInfo extends StatelessWidget {
.subtitle, .subtitle,
); );
} else { } else {
final userId = StreamChat.of(context).user?.id; final userId = StreamChat.of(context).currentUser?.id;
final otherMember = members?.firstWhereOrNull( final otherMember = members?.firstWhereOrNull(
(element) => element.userId != userId, (element) => element.userId != userId,
); );
@@ -75,12 +79,13 @@ class ChannelInfo extends StatelessWidget {
if (otherMember != null) { if (otherMember != null) {
if (otherMember.user?.online == true) { if (otherMember.user?.online == true) {
alternativeWidget = Text( alternativeWidget = Text(
'Online', context.translations.userOnlineText,
style: textStyle, style: textStyle,
); );
} else { } else {
alternativeWidget = Text( alternativeWidget = Text(
'Last seen ${Jiffy(otherMember.user?.lastActive).fromNow()}', '${context.translations.userLastOnlineText} '
'${Jiffy(otherMember.user?.lastActive).fromNow()}',
style: textStyle, style: textStyle,
); );
} }
@@ -111,7 +116,7 @@ class ChannelInfo extends StatelessWidget {
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
'Searching for Network', context.translations.searchingForNetworkText,
style: textStyle, style: textStyle,
), ),
], ],
@@ -125,7 +130,7 @@ class ChannelInfo extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
'Offline...', context.translations.offlineLabel,
style: textStyle, style: textStyle,
), ),
TextButton( TextButton(
@@ -141,7 +146,7 @@ class ChannelInfo extends StatelessWidget {
..closeConnection() ..closeConnection()
..openConnection(), ..openConnection(),
child: Text( child: Text(
'Try Again', context.translations.tryAgainLabel,
style: textStyle?.copyWith( style: textStyle?.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentPrimary, color: StreamChatTheme.of(context).colorTheme.accentPrimary,
), ),
@@ -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/src/stream_neumorphic_button.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget builder for title /// Widget builder for title
typedef TitleBuilder = Widget Function( typedef TitleBuilder = Widget Function(
@@ -95,7 +96,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _client = client ?? StreamChat.of(context).client; final _client = client ?? StreamChat.of(context).client;
final user = _client.state.user; final user = _client.state.currentUser;
return ConnectionStatusBuilder( return ConnectionStatusBuilder(
statusBuilder: (context, status) { statusBuilder: (context, status) {
var statusString = ''; var statusString = '';
@@ -103,21 +104,20 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
switch (status) { switch (status) {
case ConnectionStatus.connected: case ConnectionStatus.connected:
statusString = 'Connected'; statusString = context.translations.connectedLabel;
showStatus = false; showStatus = false;
break; break;
case ConnectionStatus.connecting: case ConnectionStatus.connecting:
statusString = 'Reconnecting...'; statusString = context.translations.reconnectingLabel;
break; break;
case ConnectionStatus.disconnected: case ConnectionStatus.disconnected:
statusString = 'Disconnected'; statusString = context.translations.disconnectedLabel;
break; break;
} }
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
return InfoTile( return InfoTile(
// ignore: avoid_bool_literals_in_conditional_expressions showMessage: showConnectionStateTile && showStatus,
showMessage: showConnectionStateTile ? showStatus : false,
message: statusString, message: statusString,
child: AppBar( child: AppBar(
textTheme: Theme.of(context).textTheme, textTheme: Theme.of(context).textTheme,
@@ -207,7 +207,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
Widget _buildConnectedTitleState(BuildContext context) { Widget _buildConnectedTitleState(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
return Text( return Text(
'Stream Chat', context.translations.streamChatLabel,
style: chatThemeData.textTheme.headlineBold.copyWith( style: chatThemeData.textTheme.headlineBold.copyWith(
color: chatThemeData.colorTheme.textHighEmphasis, color: chatThemeData.colorTheme.textHighEmphasis,
), ),
@@ -226,7 +226,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
'Searching for Network', context.translations.searchingForNetworkText,
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
.channelListHeaderTheme .channelListHeaderTheme
.title .title
@@ -247,7 +247,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
'Offline...', context.translations.offlineLabel,
style: chatThemeData.channelListHeaderTheme.title?.copyWith( style: chatThemeData.channelListHeaderTheme.title?.copyWith(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -258,7 +258,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
..closeConnection() ..closeConnection()
..openConnection(), ..openConnection(),
child: Text( child: Text(
'Try Again', context.translations.tryAgainLabel,
style: chatThemeData.channelListHeaderTheme.title?.copyWith( style: chatThemeData.channelListHeaderTheme.title?.copyWith(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -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/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Callback called when tapping on a channel /// Callback called when tapping on a channel
typedef ChannelTapCallback = void Function(Channel, Widget?); typedef ChannelTapCallback = void Function(Channel, Widget?);
@@ -231,10 +232,21 @@ class _ChannelListViewState extends State<ChannelListView> {
); );
} }
return LazyLoadScrollView( child = LazyLoadScrollView(
onEndOfPage: () => _channelListController.paginateData!(), onEndOfPage: () => _channelListController.paginateData!(),
child: child, child: child,
); );
final backgroundColor = ChannelListViewTheme.of(context).backgroundColor;
if (backgroundColor != null) {
return ColoredBox(
color: backgroundColor,
child: child,
);
}
return child;
} }
Widget _buildListView(BuildContext context, List<Channel> channels) { Widget _buildListView(BuildContext context, List<Channel> channels) {
@@ -290,7 +302,7 @@ class _ChannelListViewState extends State<ChannelListView> {
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Text( child: Text(
'Lets start chatting!', context.translations.letsStartChattingLabel,
style: chatThemeData.textTheme.headline, style: chatThemeData.textTheme.headline,
), ),
), ),
@@ -300,7 +312,7 @@ class _ChannelListViewState extends State<ChannelListView> {
horizontal: 52, horizontal: 52,
), ),
child: Text( child: Text(
'How about sending your first message to a friend?', context.translations.sendingFirstMessageLabel,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: chatThemeData.textTheme.body.copyWith( style: chatThemeData.textTheme.body.copyWith(
color: chatThemeData.colorTheme.textLowEmphasis, color: chatThemeData.colorTheme.textLowEmphasis,
@@ -319,7 +331,7 @@ class _ChannelListViewState extends State<ChannelListView> {
child: TextButton( child: TextButton(
onPressed: widget.onStartChatPressed, onPressed: widget.onStartChatPressed,
child: Text( child: Text(
'Start a chat', context.translations.startAChatLabel,
style: chatThemeData.textTheme.bodyBold.copyWith( style: chatThemeData.textTheme.bodyBold.copyWith(
color: chatThemeData.colorTheme.accentPrimary, color: chatThemeData.colorTheme.accentPrimary,
), ),
@@ -455,9 +467,9 @@ class _ChannelListViewState extends State<ChannelListView> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Text.rich( Text.rich(
const TextSpan( TextSpan(
children: [ children: [
WidgetSpan( const WidgetSpan(
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: 2, right: 2,
@@ -465,14 +477,14 @@ class _ChannelListViewState extends State<ChannelListView> {
child: Icon(Icons.error_outline), child: Icon(Icons.error_outline),
), ),
), ),
TextSpan(text: 'Error loading channels'), TextSpan(text: context.translations.loadingChannelsError),
], ],
), ),
style: Theme.of(context).textTheme.headline6, style: Theme.of(context).textTheme.headline6,
), ),
TextButton( TextButton(
onPressed: () => _channelListController.loadData!(), onPressed: () => _channelListController.loadData!(),
child: const Text('Retry'), child: Text(context.translations.retryLabel),
), ),
], ],
), ),
@@ -541,7 +553,7 @@ class _ChannelListViewState extends State<ChannelListView> {
'owner', 'owner',
].contains(channel.state!.members ].contains(channel.state!.members
.firstWhereOrNull( .firstWhereOrNull(
(m) => m.userId == channel.client.state.user?.id) (m) => m.userId == channel.client.state.currentUser?.id)
?.role)) ?.role))
IconSlideAction( IconSlideAction(
color: backgroundColor, color: backgroundColor,
@@ -550,17 +562,16 @@ class _ChannelListViewState extends State<ChannelListView> {
), ),
onTap: widget.onDeletePressed != null onTap: widget.onDeletePressed != null
? () { ? () {
widget.onDeletePressed!(channel); widget.onDeletePressed?.call(channel);
} }
: () async { : () async {
final res = await showConfirmationDialog( final res = await showConfirmationDialog(
context, context,
title: 'Delete Conversation', title: context.translations.deleteConversationLabel,
okText: 'DELETE',
question: question:
// ignore: lines_longer_than_80_chars context.translations.deleteConversationQuestion,
'Are you sure you want to delete this conversation?', okText: context.translations.deleteLabel,
cancelText: 'CANCEL', cancelText: context.translations.cancelLabel,
icon: StreamSvgIcon.delete( icon: StreamSvgIcon.delete(
color: chatThemeData.colorTheme.accentError, color: chatThemeData.colorTheme.accentError,
), ),
@@ -571,10 +582,6 @@ class _ChannelListViewState extends State<ChannelListView> {
}, },
), ),
], ],
child: DecoratedBox(
decoration: BoxDecoration(
color: chatThemeData.colorTheme.appBg,
),
child: widget.channelPreviewBuilder?.call(context, channel) ?? child: widget.channelPreviewBuilder?.call(context, channel) ??
ChannelPreview( ChannelPreview(
onLongPress: widget.onChannelLongPress, onLongPress: widget.onChannelLongPress,
@@ -583,7 +590,6 @@ class _ChannelListViewState extends State<ChannelListView> {
onTap: (channel) => onTap(channel, widget.channelWidget), onTap: (channel) => onTap(channel, widget.channelWidget),
), ),
), ),
),
); );
} }
@@ -662,7 +668,7 @@ class _ChannelListViewState extends State<ChannelListView> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Text( child: Text(
'Error loading channels', context.translations.loadingChannelsError,
style: theme.textTheme.body.copyWith( style: theme.textTheme.body.copyWith(
color: Colors.white, color: Colors.white,
), ),
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// It shows the current [Channel] name using a [Text] widget. /// It shows the current [Channel] name using a [Text] widget.
/// ///
@@ -44,10 +45,12 @@ class ChannelName extends StatelessWidget {
) => ) =>
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
var title = 'No title'; var title = context.translations.noTitleText;
if (extraData['name'] == null) { if (extraData['name'] != null) {
final otherMembers = title = extraData['name'];
members?.where((member) => member.userId != client.user!.id); } else {
final otherMembers = members
?.where((member) => member.userId != client.currentUser!.id);
if (otherMembers?.length == 1) { if (otherMembers?.length == 1) {
if (otherMembers!.first.user != null) { if (otherMembers!.first.user != null) {
title = otherMembers.first.user!.name; title = otherMembers.first.user!.name;
@@ -71,8 +74,6 @@ class ChannelName extends StatelessWidget {
title = '${currentMembers.map((e) => e.user?.name).join(', ')} ' title = '${currentMembers.map((e) => e.user?.name).join(', ')} '
'${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
} }
} else {
title = extraData['name'];
} }
return Text( return Text(
@@ -6,6 +6,7 @@ import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.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/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.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 || if (members?.isEmpty == true ||
members?.any((Member e) => members?.any((Member e) =>
e.user!.id == e.user!.id ==
channel.client.state.user?.id) != channel.client.state.currentUser?.id) !=
true) { true) {
return const SizedBox(); return const SizedBox();
} }
@@ -124,7 +125,7 @@ class ChannelPreview extends StatelessWidget {
(m) => !m.isDeleted && m.shadowed != true, (m) => !m.isDeleted && m.shadowed != true,
); );
if (lastMessage?.user?.id == if (lastMessage?.user?.id ==
streamChatState.user?.id) { streamChatState.currentUser?.id) {
return Padding( return Padding(
padding: const EdgeInsets.only(right: 4), padding: const EdgeInsets.only(right: 4),
child: SendingIndicator( child: SendingIndicator(
@@ -133,7 +134,8 @@ class ChannelPreview extends StatelessWidget {
isMessageRead: channel.state!.read isMessageRead: channel.state!.read
?.where((element) => ?.where((element) =>
element.user.id != element.user.id !=
channel.client.state.user!.id) channel
.client.state.currentUser!.id)
.where((element) => element.lastRead .where((element) => element.lastRead
.isAfter(lastMessage.createdAt)) .isAfter(lastMessage.createdAt))
.isNotEmpty == .isNotEmpty ==
@@ -172,7 +174,7 @@ class ChannelPreview extends StatelessWidget {
startOfDay startOfDay
.subtract(const Duration(days: 1)) .subtract(const Duration(days: 1))
.millisecondsSinceEpoch) { .millisecondsSinceEpoch) {
stringDate = 'Yesterday'; stringDate = context.translations.yesterdayLabel;
} else if (startOfDay.difference(lastMessageAt).inDays < 7) { } else if (startOfDay.difference(lastMessageAt).inDays < 7) {
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
} else { } else {
@@ -197,7 +199,7 @@ class ChannelPreview extends StatelessWidget {
size: 16, size: 16,
), ),
Text( Text(
' Channel is muted', ' ${context.translations.channelIsMutedText}',
style: chatThemeData.channelPreviewTheme.subtitle, style: chatThemeData.channelPreviewTheme.subtitle,
), ),
], ],
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.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 /// It shows a date divider depending on the date difference
class DateDivider extends StatelessWidget { class DateDivider extends StatelessWidget {
@@ -24,10 +25,10 @@ class DateDivider extends StatelessWidget {
String dayInfo; String dayInfo;
if (Jiffy(createdAt).isSame(now, Units.DAY)) { if (Jiffy(createdAt).isSame(now, Units.DAY)) {
dayInfo = 'Today'; dayInfo = context.translations.todayLabel;
} else if (Jiffy(createdAt) } else if (Jiffy(createdAt)
.isSame(now.subtract(const Duration(days: 1)), Units.DAY)) { .isSame(now.subtract(const Duration(days: 1)), Units.DAY)) {
dayInfo = 'Yesterday'; dayInfo = context.translations.yesterdayLabel;
} else if (Jiffy(createdAt).isAfter( } else if (Jiffy(createdAt).isAfter(
now.subtract(const Duration(days: 7)), now.subtract(const Duration(days: 7)),
Units.DAY, Units.DAY,
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget to display deleted message /// Widget to display deleted message
class DeletedMessage extends StatelessWidget { class DeletedMessage extends StatelessWidget {
@@ -49,7 +50,7 @@ class DeletedMessage extends StatelessWidget {
horizontal: 16, horizontal: 16,
), ),
child: Text( child: Text(
'Message deleted', context.translations.messageDeletedLabel,
style: messageTheme.messageText?.copyWith( style: messageTheme.messageText?.copyWith(
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
color: messageTheme.createdAt?.color, color: messageTheme.createdAt?.color,
@@ -2,6 +2,7 @@ import 'package:characters/characters.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/emoji/emoji.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'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
final _emojiChars = Emoji.chars(); final _emojiChars = Emoji.chars();
@@ -9,7 +10,8 @@ final _emojiChars = Emoji.chars();
/// String extension /// String extension
extension StringExtension on String { extension StringExtension on String {
/// Returns the capitalized 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. /// Returns whether the string contains only emoji's or not.
/// ///
@@ -103,6 +105,11 @@ extension BuildContextX on BuildContext {
// ignore: public_member_api_docs // ignore: public_member_api_docs
double get textScaleFactor => double get textScaleFactor =>
MediaQuery.maybeOf(this)?.textScaleFactor ?? 1.0; 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] /// Extension on [BorderRadius]
@@ -4,12 +4,12 @@ import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart'; import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:stream_chat_flutter/src/gallery_footer.dart'; import 'package:stream_chat_flutter/src/gallery_footer.dart';
import 'package:stream_chat_flutter/src/gallery_header.dart'; import 'package:stream_chat_flutter/src/gallery_header.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Return action for coming back from pages /// Return action for coming back from pages
enum ReturnActionType { enum ReturnActionType {
@@ -182,9 +182,10 @@ class _FullScreenMediaState extends State<FullScreenMedia>
children: [ children: [
GalleryHeader( GalleryHeader(
userName: widget.userName, userName: widget.userName,
sentAt: sentAt: context.translations.sentAtText(
// ignore: lines_longer_than_80_chars date: widget.message.createdAt,
'Sent ${getDay(widget.message.createdAt.toLocal())} at ${Jiffy(widget.message.createdAt.toLocal()).format('HH:mm')}', time: widget.message.createdAt,
),
onBackPressed: () { onBackPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
@@ -197,7 +198,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
); );
}, },
), ),
if (widget.message.type != 'ephemeral') if (!widget.message.isEphemeral)
GalleryFooter( GalleryFooter(
currentPage: _currentPage, currentPage: _currentPage,
totalPages: widget.mediaAttachments.length, totalPages: widget.mediaAttachments.length,
@@ -222,22 +223,6 @@ class _FullScreenMediaState extends State<FullScreenMedia>
), ),
); );
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 @override
void dispose() async { void dispose() async {
for (final package in videoPackages.values) { for (final package in videoPackages.values) {
@@ -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/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Footer widget for media display /// Footer widget for media display
class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
@@ -135,7 +136,9 @@ class _GalleryFooterState extends State<GalleryFooter> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Text( Text(
'${widget.currentPage + 1} of ${widget.totalPages}', '${widget.currentPage + 1} '
'${context.translations.ofText} '
'${widget.totalPages}',
style: galleryFooterThemeData.titleTextStyle, style: galleryFooterThemeData.titleTextStyle,
), ),
], ],
@@ -191,7 +194,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Text( child: Text(
'Photos', context.translations.photosLabel,
style: style:
galleryFooterThemeData.bottomSheetPhotosTextStyle, galleryFooterThemeData.bottomSheetPhotosTextStyle,
), ),
@@ -67,7 +67,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
: const SizedBox(), : const SizedBox(),
backgroundColor: galleryHeaderThemeData.backgroundColor, backgroundColor: galleryHeaderThemeData.backgroundColor,
actions: <Widget>[ actions: <Widget>[
if (message.type != 'ephemeral') if (!message.isEphemeral)
IconButton( IconButton(
icon: StreamSvgIcon.iconMenuPoint( icon: StreamSvgIcon.iconMenuPoint(
color: galleryHeaderThemeData.iconMenuPointColor, color: galleryHeaderThemeData.iconMenuPointColor,
@@ -78,7 +78,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
], ],
centerTitle: true, centerTitle: true,
title: message.type != 'ephemeral' title: !message.isEphemeral
? InkWell( ? InkWell(
onTap: onTitleTap, onTap: onTitleTap,
child: SizedBox( child: SizedBox(
@@ -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<GradientAvatar> {
@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 = <Offset4>[];
final points = <Offset>{};
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<Offset> transformPoints(Set<Offset> points, Size size) {
final transformedList = <Offset>[];
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<Color> gradient;
/// Draw the polygon on canvas
void draw(Canvas canvas, List<Offset> 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)],
];
@@ -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<StreamChatLocalizations>(
/// 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<StreamChatLocalizations>(
context,
StreamChatLocalizations,
);
}
@@ -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<User> 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<User> 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 => 'Lets 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';
}
@@ -101,7 +101,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
Widget _showMessageOptionsModal() { Widget _showMessageOptionsModal() {
final mediaQueryData = MediaQuery.of(context); final mediaQueryData = MediaQuery.of(context);
final size = mediaQueryData.size; final size = mediaQueryData.size;
final user = StreamChat.of(context).user; final user = StreamChat.of(context).currentUser;
final roughMaxSize = 2 * size.width / 3; final roughMaxSize = 2 * size.width / 3;
var messageTextLength = widget.message.text!.length; var messageTextLength = widget.message.text!.length;
@@ -268,16 +268,14 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
final streamChatThemeData = StreamChatTheme.of(context); final streamChatThemeData = StreamChatTheme.of(context);
final answer = await showConfirmationDialog( final answer = await showConfirmationDialog(
context, context,
title: 'Flag Message', title: context.translations.flagMessageLabel,
icon: StreamSvgIcon.flag( icon: StreamSvgIcon.flag(
color: streamChatThemeData.colorTheme.accentError, color: streamChatThemeData.colorTheme.accentError,
size: 24, size: 24,
), ),
question: question: context.translations.flagMessageQuestion,
// ignore: lines_longer_than_80_chars okText: context.translations.flagLabel,
'Do you want to send a copy of this message to a\nmoderator for further investigation?', cancelText: context.translations.cancelLabel,
okText: 'FLAG',
cancelText: 'CANCEL',
); );
final theme = streamChatThemeData; final theme = streamChatThemeData;
@@ -290,9 +288,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
color: theme.colorTheme.accentError, color: theme.colorTheme.accentError,
size: 24, size: 24,
), ),
details: 'The message has been reported to a moderator.', details: context.translations.flagMessageSuccessfulText,
title: 'Message flagged', title: context.translations.flagMessageSuccessfulLabel,
okText: 'OK', okText: context.translations.okLabel,
); );
} catch (err) { } catch (err) {
if (err is StreamChatNetworkError && if (err is StreamChatNetworkError &&
@@ -303,9 +301,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
color: theme.colorTheme.accentError, color: theme.colorTheme.accentError,
size: 24, size: 24,
), ),
details: 'The message has been reported to a moderator.', details: context.translations.flagMessageSuccessfulText,
title: 'Message flagged', title: context.translations.flagMessageSuccessfulLabel,
okText: 'OK', okText: context.translations.okLabel,
); );
} else { } else {
_showErrorAlert(); _showErrorAlert();
@@ -335,14 +333,14 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
}); });
final answer = await showConfirmationDialog( final answer = await showConfirmationDialog(
context, context,
title: 'Delete message', title: context.translations.deleteMessageLabel,
icon: StreamSvgIcon.flag( icon: StreamSvgIcon.flag(
color: StreamChatTheme.of(context).colorTheme.accentError, color: StreamChatTheme.of(context).colorTheme.accentError,
size: 24, size: 24,
), ),
question: 'Are you sure you want to permanently delete this\nmessage?', question: context.translations.deleteMessageQuestion,
okText: 'DELETE', okText: context.translations.deleteLabel,
cancelText: 'CANCEL', cancelText: context.translations.cancelLabel,
); );
if (answer == true) { if (answer == true) {
@@ -366,9 +364,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
color: StreamChatTheme.of(context).colorTheme.accentError, color: StreamChatTheme.of(context).colorTheme.accentError,
size: 24, size: 24,
), ),
details: 'The operation couldn\'t be completed.', details: context.translations.operationCouldNotBeCompletedText,
title: 'Something went wrong', title: context.translations.somethingWentWrongError,
okText: 'OK', okText: context.translations.okLabel,
); );
} }
@@ -390,7 +388,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text( Text(
'Reply', context.translations.replyLabel,
style: streamChatThemeData.textTheme.body, style: streamChatThemeData.textTheme.body,
), ),
], ],
@@ -412,7 +410,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text( Text(
'Flag Message', context.translations.flagMessageLabel,
style: streamChatThemeData.textTheme.body, style: streamChatThemeData.textTheme.body,
), ),
], ],
@@ -435,7 +433,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text( Text(
'${widget.message.pinned ? 'Unpin from' : 'Pin to'} Conversation', context.translations.togglePinUnpinText(
pinned: widget.message.pinned,
),
style: streamChatThemeData.textTheme.body, style: streamChatThemeData.textTheme.body,
), ),
], ],
@@ -458,7 +458,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text( Text(
isDeleteFailed ? 'Retry Deleting Message' : 'Delete Message', context.translations.toggleDeleteRetryDeleteMessageText(
isDeleteFailed: isDeleteFailed,
),
style: StreamChatTheme.of(context) style: StreamChatTheme.of(context)
.textTheme .textTheme
.body .body
@@ -487,7 +489,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text( Text(
'Copy Message', context.translations.copyMessageLabel,
style: streamChatThemeData.textTheme.body, style: streamChatThemeData.textTheme.body,
), ),
], ],
@@ -512,7 +514,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text( Text(
'Edit Message', context.translations.editMessageLabel,
style: streamChatThemeData.textTheme.body, style: streamChatThemeData.textTheme.body,
), ),
], ],
@@ -544,7 +546,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text( Text(
isUpdateFailed ? 'Resend Edited Message' : 'Resend', context.translations.toggleResendOrResendEditedMessage(
isUpdateFailed: isUpdateFailed,
),
style: streamChatThemeData.textTheme.body, style: streamChatThemeData.textTheme.body,
), ),
], ],
@@ -588,9 +592,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
color: streamChatThemeData.colorTheme.disabled, color: streamChatThemeData.colorTheme.disabled,
), ),
), ),
const Text( Text(
'Edit Message', context.translations.editMessageLabel,
style: TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
IconButton( IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
@@ -636,7 +640,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text( Text(
'Thread Reply', context.translations.threadReplyLabel,
style: streamChatThemeData.textTheme.body, style: streamChatThemeData.textTheme.body,
), ),
], ],
@@ -357,9 +357,9 @@ class MessageInputState extends State<MessageInput> {
color: _streamChatTheme.colorTheme.disabled, color: _streamChatTheme.colorTheme.disabled,
), ),
), ),
const Text( Text(
'Reply to Message', context.translations.replyToMessageLabel,
style: TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
IconButton( IconButton(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
@@ -461,7 +461,7 @@ class MessageInputState extends State<MessageInput> {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text( child: Text(
'Also send as direct message', context.translations.alsoSendAsDirectMessageLabel,
style: _streamChatTheme.textTheme.footnote.copyWith( style: _streamChatTheme.textTheme.footnote.copyWith(
color: _streamChatTheme.colorTheme.textHighEmphasis color: _streamChatTheme.colorTheme.textHighEmphasis
.withOpacity(0.5), .withOpacity(0.5),
@@ -586,7 +586,7 @@ class MessageInputState extends State<MessageInput> {
style: _streamChatTheme.messageInputTheme.inputTextStyle, style: _streamChatTheme.messageInputTheme.inputTextStyle,
autofocus: widget.autofocus, autofocus: widget.autofocus,
textAlignVertical: TextAlignVertical.center, textAlignVertical: TextAlignVertical.center,
decoration: _getInputDecoration(), decoration: _getInputDecoration(context),
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
), ),
) )
@@ -598,11 +598,11 @@ class MessageInputState extends State<MessageInput> {
); );
} }
InputDecoration _getInputDecoration() { InputDecoration _getInputDecoration(BuildContext context) {
final passedDecoration = _streamChatTheme.messageInputTheme.inputDecoration; final passedDecoration = _streamChatTheme.messageInputTheme.inputDecoration;
return InputDecoration( return InputDecoration(
isDense: true, isDense: true,
hintText: _getHint(), hintText: _getHint(context),
hintStyle: _streamChatTheme.messageInputTheme.inputTextStyle!.copyWith( hintStyle: _streamChatTheme.messageInputTheme.inputTextStyle!.copyWith(
color: _streamChatTheme.colorTheme.textLowEmphasis, color: _streamChatTheme.colorTheme.textLowEmphasis,
), ),
@@ -751,14 +751,14 @@ class MessageInputState extends State<MessageInput> {
); );
} }
String _getHint() { String _getHint(BuildContext context) {
if (_commandEnabled && _chosenCommand!.name == 'giphy') { if (_commandEnabled && _chosenCommand!.name == 'giphy') {
return 'Search GIFs'; return context.translations.searchGifLabel;
} }
if (_attachments.isNotEmpty) { 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) { void _checkEmoji(String s, BuildContext context) {
@@ -881,7 +881,7 @@ class MessageInputState extends State<MessageInput> {
), ),
), ),
Text( Text(
'Instant Commands', context.translations.instantCommandsLabel,
style: TextStyle( style: TextStyle(
color: _streamChatTheme.colorTheme.textHighEmphasis color: _streamChatTheme.colorTheme.textHighEmphasis
.withOpacity(.5), .withOpacity(.5),
@@ -1144,8 +1144,9 @@ class MessageInputState extends State<MessageInput> {
if (mediaInfo.filesize! > widget.maxAttachmentSize) { if (mediaInfo.filesize! > widget.maxAttachmentSize) {
_showErrorAlert( _showErrorAlert(
// ignore: lines_longer_than_80_chars context.translations.fileTooLargeAfterCompressionError(
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.', widget.maxAttachmentSize / (1024 * 1024),
),
); );
return; return;
} }
@@ -1156,9 +1157,9 @@ class MessageInputState extends State<MessageInput> {
path: mediaInfo.path, path: mediaInfo.path,
); );
} else { } else {
_showErrorAlert( _showErrorAlert(context.translations.fileTooLargeError(
'The file is too large to upload. The file size limit is 20MB.', widget.maxAttachmentSize / (1024 * 1024),
); ));
return; return;
} }
} }
@@ -1426,7 +1427,9 @@ class MessageInputState extends State<MessageInput> {
), ),
Flexible( Flexible(
child: Text( child: Text(
'Emoji matching "$query"', context.translations.emojiMatchingQueryText(
query,
),
style: TextStyle( style: TextStyle(
color: _streamChatTheme color: _streamChatTheme
.colorTheme.textHighEmphasis .colorTheme.textHighEmphasis
@@ -1776,17 +1779,17 @@ class MessageInputState extends State<MessageInput> {
builder: (_) => Column( builder: (_) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
const ListTile( ListTile(
title: Text( title: Text(
'Add a file', context.translations.addAFileLabel,
style: TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
ListTile( ListTile(
leading: const Icon(Icons.image), leading: const Icon(Icons.image),
title: const Text('Upload a photo'), title: Text(context.translations.uploadAPhotoLabel),
onTap: () { onTap: () {
pickFile(DefaultAttachmentTypes.image); pickFile(DefaultAttachmentTypes.image);
Navigator.pop(context); Navigator.pop(context);
@@ -1794,7 +1797,7 @@ class MessageInputState extends State<MessageInput> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.video_library), leading: const Icon(Icons.video_library),
title: const Text('Upload a video'), title: Text(context.translations.uploadAVideoLabel),
onTap: () { onTap: () {
pickFile(DefaultAttachmentTypes.video); pickFile(DefaultAttachmentTypes.video);
Navigator.pop(context); Navigator.pop(context);
@@ -1803,7 +1806,7 @@ class MessageInputState extends State<MessageInput> {
if (!kIsWeb) if (!kIsWeb)
ListTile( ListTile(
leading: const Icon(Icons.camera_alt), leading: const Icon(Icons.camera_alt),
title: const Text('Photo from camera'), title: Text(context.translations.photoFromCameraLabel),
onTap: () { onTap: () {
pickFile(DefaultAttachmentTypes.image, true); pickFile(DefaultAttachmentTypes.image, true);
Navigator.pop(context); Navigator.pop(context);
@@ -1812,7 +1815,7 @@ class MessageInputState extends State<MessageInput> {
if (!kIsWeb) if (!kIsWeb)
ListTile( ListTile(
leading: const Icon(Icons.videocam), leading: const Icon(Icons.videocam),
title: const Text('Video from camera'), title: Text(context.translations.videoFromCameraLabel),
onTap: () { onTap: () {
pickFile(DefaultAttachmentTypes.video, true); pickFile(DefaultAttachmentTypes.video, true);
Navigator.pop(context); Navigator.pop(context);
@@ -1820,7 +1823,7 @@ class MessageInputState extends State<MessageInput> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.insert_drive_file), leading: const Icon(Icons.insert_drive_file),
title: const Text('Upload a file'), title: Text(context.translations.uploadAFileLabel),
onTap: () { onTap: () {
pickFile(DefaultAttachmentTypes.file); pickFile(DefaultAttachmentTypes.file);
Navigator.pop(context); Navigator.pop(context);
@@ -1923,8 +1926,9 @@ class MessageInputState extends State<MessageInput> {
if (mediaInfo.filesize! > widget.maxAttachmentSize) { if (mediaInfo.filesize! > widget.maxAttachmentSize) {
_showErrorAlert( _showErrorAlert(
// ignore: lines_longer_than_80_chars context.translations.fileTooLargeAfterCompressionError(
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.', widget.maxAttachmentSize / (1024 * 1024),
),
); );
return; return;
} }
@@ -1935,9 +1939,9 @@ class MessageInputState extends State<MessageInput> {
path: mediaInfo.path, path: mediaInfo.path,
); );
} else { } else {
_showErrorAlert( _showErrorAlert(context.translations.fileTooLargeError(
'The file is too large to upload. The file size limit is 20MB.', widget.maxAttachmentSize / (1024 * 1024),
); ));
return; return;
} }
} }
@@ -2117,7 +2121,7 @@ class MessageInputState extends State<MessageInput> {
height: 26, height: 26,
), ),
Text( Text(
'Something went wrong', context.translations.somethingWentWrongError,
style: _streamChatTheme.textTheme.headlineBold, style: _streamChatTheme.textTheme.headlineBold,
), ),
const SizedBox( const SizedBox(
@@ -2146,7 +2150,7 @@ class MessageInputState extends State<MessageInput> {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text( child: Text(
'OK', context.translations.okLabel,
style: _streamChatTheme.textTheme.bodyBold.copyWith( style: _streamChatTheme.textTheme.bodyBold.copyWith(
color: _streamChatTheme.colorTheme.accentPrimary), color: _streamChatTheme.colorTheme.accentPrimary),
), ),
@@ -2257,10 +2261,10 @@ class _PickerWidget extends StatefulWidget {
final StreamChatThemeData streamChatTheme; final StreamChatThemeData streamChatTheme;
@override @override
__PickerWidgetState createState() => __PickerWidgetState(); _PickerWidgetState createState() => _PickerWidgetState();
} }
class __PickerWidgetState extends State<_PickerWidget> { class _PickerWidgetState extends State<_PickerWidget> {
Future<bool>? requestPermission; Future<bool>? requestPermission;
@override @override
@@ -2292,7 +2296,7 @@ class __PickerWidgetState extends State<_PickerWidget> {
color: widget.streamChatTheme.colorTheme.inputBg, color: widget.streamChatTheme.colorTheme.inputBg,
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
'Add more files', context.translations.addMoreFilesLabel,
style: TextStyle( style: TextStyle(
color: widget.streamChatTheme.colorTheme.accentPrimary, color: widget.streamChatTheme.colorTheme.accentPrimary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -2324,8 +2328,7 @@ class __PickerWidgetState extends State<_PickerWidget> {
color: widget.streamChatTheme.colorTheme.disabled, color: widget.streamChatTheme.colorTheme.disabled,
), ),
Text( Text(
// ignore: lines_longer_than_80_chars context.translations.enablePhotoAndVideoAccessMessage,
'Please enable access to your photos \nand videos so you can share them with friends.',
style: widget.streamChatTheme.textTheme.body.copyWith( style: widget.streamChatTheme.textTheme.body.copyWith(
color: color:
widget.streamChatTheme.colorTheme.textLowEmphasis), widget.streamChatTheme.colorTheme.textLowEmphasis),
@@ -2334,7 +2337,7 @@ class __PickerWidgetState extends State<_PickerWidget> {
const SizedBox(height: 6), const SizedBox(height: 6),
Center( Center(
child: Text( child: Text(
'Allow access to your gallery', context.translations.allowGalleryAccessMessage,
style: widget.streamChatTheme.textTheme.bodyBold.copyWith( style: widget.streamChatTheme.textTheme.bodyBold.copyWith(
color: widget.streamChatTheme.colorTheme.accentPrimary, color: widget.streamChatTheme.colorTheme.accentPrimary,
), ),
@@ -166,11 +166,23 @@ class MessageListView extends StatefulWidget {
this.showFloatingDateDivider = true, this.showFloatingDateDivider = true,
this.threadSeparatorBuilder, this.threadSeparatorBuilder,
this.messageListController, this.messageListController,
this.reverse = true,
this.paginationLimit = 20,
}) : super(key: key); }) : super(key: key);
/// Function used to build a custom message widget /// Function used to build a custom message widget
final MessageBuilder? messageBuilder; 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 /// Function used to build a custom system message widget
final SystemMessageBuilder? systemMessageBuilder; final SystemMessageBuilder? systemMessageBuilder;
@@ -323,11 +335,13 @@ class _MessageListViewState extends State<MessageListView> {
bool _inBetweenList = false; bool _inBetweenList = false;
late final _defaultController = MessageListController(); late final _defaultController = MessageListController();
MessageListController get _messageListController => MessageListController get _messageListController =>
widget.messageListController ?? _defaultController; widget.messageListController ?? _defaultController;
@override @override
Widget build(BuildContext context) => MessageListCore( Widget build(BuildContext context) => MessageListCore(
paginationLimit: widget.paginationLimit,
messageFilter: widget.messageFilter, messageFilter: widget.messageFilter,
loadingBuilder: widget.loadingBuilder ?? loadingBuilder: widget.loadingBuilder ??
(context) => const Center( (context) => const Center(
@@ -336,7 +350,7 @@ class _MessageListViewState extends State<MessageListView> {
emptyBuilder: widget.emptyBuilder ?? emptyBuilder: widget.emptyBuilder ??
(context) => Center( (context) => Center(
child: Text( child: Text(
'No chats here yet...', context.translations.emptyChatMessagesText,
style: _streamTheme.textTheme.footnote.copyWith( style: _streamTheme.textTheme.footnote.copyWith(
color: _streamTheme.colorTheme.textHighEmphasis color: _streamTheme.colorTheme.textHighEmphasis
.withOpacity(.5)), .withOpacity(.5)),
@@ -349,7 +363,7 @@ class _MessageListViewState extends State<MessageListView> {
errorBuilder: widget.errorBuilder ?? errorBuilder: widget.errorBuilder ??
(BuildContext context, Object error) => Center( (BuildContext context, Object error) => Center(
child: Text( child: Text(
'Something went wrong', context.translations.genericErrorText,
style: _streamTheme.textTheme.footnote.copyWith( style: _streamTheme.textTheme.footnote.copyWith(
color: _streamTheme.colorTheme.textHighEmphasis color: _streamTheme.colorTheme.textHighEmphasis
.withOpacity(.5)), .withOpacity(.5)),
@@ -386,7 +400,7 @@ class _MessageListViewState extends State<MessageListView> {
1 // parent message 1 // parent message
; ;
return Stack( final child = Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
ConnectionStatusBuilder( ConnectionStatusBuilder(
@@ -395,14 +409,14 @@ class _MessageListViewState extends State<MessageListView> {
var showStatus = true; var showStatus = true;
switch (status) { switch (status) {
case ConnectionStatus.connected: case ConnectionStatus.connected:
statusString = 'Connected'; statusString = context.translations.connectedLabel;
showStatus = false; showStatus = false;
break; break;
case ConnectionStatus.connecting: case ConnectionStatus.connecting:
statusString = 'Reconnecting...'; statusString = context.translations.reconnectingLabel;
break; break;
case ConnectionStatus.disconnected: case ConnectionStatus.disconnected:
statusString = 'Disconnected'; statusString = context.translations.disconnectedLabel;
break; break;
} }
@@ -445,7 +459,7 @@ class _MessageListViewState extends State<MessageListView> {
initialAlignment: initialAlignment ?? 0, initialAlignment: initialAlignment ?? 0,
physics: widget.scrollPhysics, physics: widget.scrollPhysics,
itemScrollController: _scrollController, itemScrollController: _scrollController,
reverse: true, reverse: widget.reverse,
addAutomaticKeepAlives: false, addAutomaticKeepAlives: false,
itemCount: itemCount, itemCount: itemCount,
@@ -528,7 +542,9 @@ class _MessageListViewState extends State<MessageListView> {
}, },
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i == itemCount - 1) { if (i == itemCount - 1) {
if (widget.parentMessage == null) return const Offstage(); if (widget.parentMessage == null) {
return const Offstage();
}
return buildParentMessage(widget.parentMessage!); return buildParentMessage(widget.parentMessage!);
} }
@@ -584,6 +600,17 @@ class _MessageListViewState extends State<MessageListView> {
_buildFloatingDateDivider(itemCount), _buildFloatingDateDivider(itemCount),
], ],
); );
final backgroundColor = MessageListViewTheme.of(context).backgroundColor;
if (backgroundColor != null) {
return ColoredBox(
color: backgroundColor,
child: child,
);
}
return child;
} }
Widget _buildThreadSeparator() { Widget _buildThreadSeparator() {
@@ -591,7 +618,7 @@ class _MessageListViewState extends State<MessageListView> {
return widget.threadSeparatorBuilder!.call(context); return widget.threadSeparatorBuilder!.call(context);
} }
final replyCount = widget.parentMessage!.replyCount; final replyCount = widget.parentMessage!.replyCount!;
return DecoratedBox( return DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: _streamTheme.colorTheme.bgGradient, gradient: _streamTheme.colorTheme.bgGradient,
@@ -599,7 +626,7 @@ class _MessageListViewState extends State<MessageListView> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Text( child: Text(
'$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', context.translations.threadSeparatorText(replyCount),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: _streamTheme.channelTheme.channelHeaderTheme.subtitle, style: _streamTheme.channelTheme.channelHeaderTheme.subtitle,
), ),
@@ -608,7 +635,10 @@ class _MessageListViewState extends State<MessageListView> {
} }
Positioned _buildFloatingDateDivider(int itemCount) => Positioned( Positioned _buildFloatingDateDivider(int itemCount) => Positioned(
top: 20, top: widget.reverse ? 20 : null,
bottom: widget.reverse ? null : 20,
left: 0,
right: 0,
child: BetterStreamBuilder<Iterable<ItemPosition>>( child: BetterStreamBuilder<Iterable<ItemPosition>>(
initialData: _itemPositionListener.itemPositions.value, initialData: _itemPositionListener.itemPositions.value,
stream: _itemPositionStream, stream: _itemPositionStream,
@@ -640,7 +670,9 @@ class _MessageListViewState extends State<MessageListView> {
); );
Future<void> _paginateData( Future<void> _paginateData(
StreamChannelState? channel, QueryDirection direction) => StreamChannelState? channel,
QueryDirection direction,
) =>
_messageListController.paginateData!(direction: direction); _messageListController.paginateData!(direction: direction);
int? _getTopElementIndex(Iterable<ItemPosition> values) { int? _getTopElementIndex(Iterable<ItemPosition> values) {
@@ -672,7 +704,8 @@ class _MessageListViewState extends State<MessageListView> {
final unreadCount = snapshot.data!.item2; final unreadCount = snapshot.data!.item2;
final showUnreadCount = unreadCount > 0 && final showUnreadCount = unreadCount > 0 &&
streamChannel!.channel.state!.members.any((e) => streamChannel!.channel.state!.members.any((e) =>
e.userId == streamChannel!.channel.client.state.user!.id); e.userId ==
streamChannel!.channel.client.state.currentUser!.id);
return Positioned( return Positioned(
bottom: 8, bottom: 8,
right: 8, right: 8,
@@ -700,7 +733,11 @@ class _MessageListViewState extends State<MessageListView> {
); );
} }
}, },
child: StreamSvgIcon.down( child: widget.reverse
? StreamSvgIcon.down(
color: _streamTheme.colorTheme.textHighEmphasis,
)
: StreamSvgIcon.up(
color: _streamTheme.colorTheme.textHighEmphasis, color: _streamTheme.colorTheme.textHighEmphasis,
), ),
), ),
@@ -774,9 +811,10 @@ class _MessageListViewState extends State<MessageListView> {
Widget buildParentMessage( Widget buildParentMessage(
Message message, 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 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 members = StreamChannel.of(context).channel.state?.members ?? [];
final currentUserMember = final currentUserMember =
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
@@ -861,7 +899,7 @@ class _MessageListViewState extends State<MessageListView> {
); );
} }
final userId = StreamChat.of(context).user!.id; final userId = StreamChat.of(context).currentUser!.id;
final isMyMessage = message.user!.id == userId; final isMyMessage = message.user!.id == userId;
final nextMessage = index - 1 >= 0 ? messages[index - 1] : null; final nextMessage = index - 1 >= 0 ? messages[index - 1] : null;
final isNextUserSame = final isNextUserSame =
@@ -924,7 +962,7 @@ class _MessageListViewState extends State<MessageListView> {
? BorderSide.none ? BorderSide.none
: null; : null;
final currentUser = StreamChat.of(context).user; final currentUser = StreamChat.of(context).currentUser;
final members = StreamChannel.of(context).channel.state?.members ?? []; final members = StreamChannel.of(context).channel.state?.members ?? [];
final currentUserMember = final currentUserMember =
members.firstWhere((e) => e.user!.id == currentUser!.id); members.firstWhere((e) => e.user!.id == currentUser!.id);
@@ -1130,7 +1168,7 @@ class _MessageListViewState extends State<MessageListView> {
_topPaginationActive = false; _topPaginationActive = false;
} }
if (event.message!.user!.id == if (event.message!.user!.id ==
streamChannel!.channel.client.state.user!.id) { streamChannel!.channel.client.state.currentUser!.id) {
WidgetsBinding.instance!.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
_scrollController?.jumpTo( _scrollController?.jumpTo(
index: 0, index: 0,
@@ -1212,8 +1250,8 @@ class _LoadingIndicator extends StatelessWidget {
initialData: false, initialData: false,
errorBuilder: (context, error) => Container( errorBuilder: (context, error) => Container(
color: streamTheme.colorTheme.accentError.withOpacity(.2), color: streamTheme.colorTheme.accentError.withOpacity(.2),
child: const Center( child: Center(
child: Text('Error loading messages'), child: Text(context.translations.loadingMessagesError),
), ),
), ),
builder: (context, data) { builder: (context, data) {
@@ -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/src/user_avatar.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Modal widget for displaying message reactions /// Modal widget for displaying message reactions
class MessageReactionsModal extends StatelessWidget { class MessageReactionsModal extends StatelessWidget {
@@ -42,7 +43,7 @@ class MessageReactionsModal extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final user = StreamChat.of(context).user; final user = StreamChat.of(context).currentUser;
final roughMaxSize = 2 * size.width / 3; final roughMaxSize = 2 * size.width / 3;
var messageTextLength = message.text!.length; var messageTextLength = message.text!.length;
@@ -154,7 +155,7 @@ class MessageReactionsModal extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
'Message Reactions', context.translations.messageReactionsLabel,
style: chatThemeData.textTheme.headlineBold, style: chatThemeData.textTheme.headlineBold,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// It shows the current [Message] preview. /// It shows the current [Message] preview.
/// ///
@@ -49,12 +50,14 @@ class MessageSearchItem extends StatelessWidget {
title: Row( title: Row(
children: [ children: [
Text( 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, style: chatThemeData.channelPreviewTheme.title,
), ),
if (channelName != null) ...[ if (channelName != null) ...[
Text( Text(
' in ', ' ${context.translations.inText} ',
style: chatThemeData.channelPreviewTheme.title?.copyWith( style: chatThemeData.channelPreviewTheme.title?.copyWith(
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
), ),
@@ -98,7 +101,7 @@ class MessageSearchItem extends StatelessWidget {
Widget _buildSubtitle(BuildContext context, Message message) { Widget _buildSubtitle(BuildContext context, Message message) {
var text = message.text; var text = message.text;
if (message.isDeleted) { if (message.isDeleted) {
text = 'This message was deleted.'; text = context.translations.messageDeletedText;
} else if (message.attachments.isNotEmpty) { } else if (message.attachments.isNotEmpty) {
final parts = <String>[ final parts = <String>[
...message.attachments.map((e) { ...message.attachments.map((e) {
@@ -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/src/message_search_item.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Callback called when tapping on a user /// Callback called when tapping on a user
typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
@@ -140,11 +141,13 @@ class MessageSearchListView extends StatefulWidget {
class _MessageSearchListViewState extends State<MessageSearchListView> { class _MessageSearchListViewState extends State<MessageSearchListView> {
late final _defaultController = MessageSearchListController(); late final _defaultController = MessageSearchListController();
MessageSearchListController get _messageSearchListController => MessageSearchListController get _messageSearchListController =>
widget.messageSearchListController ?? _defaultController; widget.messageSearchListController ?? _defaultController;
@override @override
Widget build(BuildContext context) => MessageSearchListCore( Widget build(BuildContext context) {
final messageSearchListCore = MessageSearchListCore(
filters: widget.filters, filters: widget.filters,
sortOptions: widget.sortOptions, sortOptions: widget.sortOptions,
messageQuery: widget.messageQuery, messageQuery: widget.messageQuery,
@@ -160,8 +163,8 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
constraints: BoxConstraints( constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight, minHeight: viewportConstraints.maxHeight,
), ),
child: const Center( child: Center(
child: Text('There are no messages currently'), child: Text(context.translations.emptyMessagesText),
), ),
), ),
), ),
@@ -175,7 +178,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
showMessage: widget.showErrorTile, showMessage: widget.showErrorTile,
tileAnchor: Alignment.topCenter, tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter,
message: 'An error occurred.', message: context.translations.genericErrorText,
child: Container(), child: Container(),
); );
}, },
@@ -197,6 +200,19 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
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( Widget _separatorBuilder(BuildContext context, int index) => Container(
height: 1, height: 1,
color: StreamChatTheme.of(context).colorTheme.borders, color: StreamChatTheme.of(context).colorTheme.borders,
@@ -226,10 +242,10 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
.colorTheme .colorTheme
.accentError .accentError
.withOpacity(.2), .withOpacity(.2),
child: const Padding( child: Padding(
padding: EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
child: Center( child: Center(
child: Text('Error loading messages'), child: Text(context.translations.loadingMessagesError),
), ),
), ),
); );
@@ -292,7 +308,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
horizontal: 8, horizontal: 8,
), ),
child: Text( child: Text(
'${items.length} results', context.translations.resultCountText(items.length),
style: TextStyle( style: TextStyle(
color: chatThemeData.colorTheme.textLowEmphasis, color: chatThemeData.colorTheme.textLowEmphasis,
), ),
@@ -29,11 +29,19 @@ class MessageText extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final text = _replaceMentions(message.text ?? '').replaceAll('\n', '\n\n'); final streamChat = StreamChat.of(context);
assert(streamChat.currentUser != null, '');
return BetterStreamBuilder<String>(
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); final themeData = Theme.of(context);
return MarkdownBody( return MarkdownBody(
data: text, data: messageText,
onTapLink: ( onTapLink: (
String link, String link,
String? href, String? href,
@@ -43,15 +51,10 @@ class MessageText extends StatelessWidget {
final mentionedUser = message.mentionedUsers.firstWhereOrNull( final mentionedUser = message.mentionedUsers.firstWhereOrNull(
(u) => '@${u.name}' == link, (u) => '@${u.name}' == link,
); );
if (mentionedUser == null) {
return;
}
if (onMentionTap != null) { if (mentionedUser == null) return;
onMentionTap!(mentionedUser);
} else { onMentionTap?.call(mentionedUser);
print('tap on ${mentionedUser.name}');
}
} else { } else {
if (onLinkTap != null) { if (onLinkTap != null) {
onLinkTap!(link); onLinkTap!(link);
@@ -75,14 +78,17 @@ class MessageText extends StatelessWidget {
p: messageTheme.messageText, p: messageTheme.messageText,
), ),
); );
},
);
} }
String _replaceMentions(String text) { String _replaceMentions(String text) {
message.mentionedUsers.map((u) => u.name).toSet().forEach((userName) { var messageTextToRender = text;
// ignore: parameter_assignments for (final user in message.mentionedUsers.toSet()) {
text = text.replaceAll( final userName = user.name;
messageTextToRender = messageTextToRender.replaceAll(
'@$userName', '[@$userName](@${userName.replaceAll(' ', '')})'); '@$userName', '[@$userName](@${userName.replaceAll(' ', '')})');
}); }
return text; return messageTextToRender;
} }
} }
@@ -92,6 +92,8 @@ class MessageWidget extends StatefulWidget {
this.userAvatarBuilder, this.userAvatarBuilder,
this.editMessageInputBuilder, this.editMessageInputBuilder,
this.textBuilder, this.textBuilder,
this.bottomRowBuilder,
this.deletedBottomRowBuilder,
this.onReturnAction, this.onReturnAction,
Map<String, AttachmentBuilder>? customAttachmentBuilders, Map<String, AttachmentBuilder>? customAttachmentBuilders,
this.readList, this.readList,
@@ -275,6 +277,12 @@ class MessageWidget extends StatefulWidget {
/// Function called on long press /// Function called on long press
final void Function(BuildContext, Message)? onMessageActions; 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 /// Widget builder for building user avatar
final Widget Function(BuildContext, User)? userAvatarBuilder; final Widget Function(BuildContext, User)? userAvatarBuilder;
@@ -410,6 +418,8 @@ class MessageWidget extends StatefulWidget {
Widget Function(BuildContext, Message)? editMessageInputBuilder, Widget Function(BuildContext, Message)? editMessageInputBuilder,
Widget Function(BuildContext, Message)? textBuilder, Widget Function(BuildContext, Message)? textBuilder,
Widget Function(BuildContext, Message)? usernameBuilder, Widget Function(BuildContext, Message)? usernameBuilder,
Widget Function(BuildContext, Message)? bottomRowBuilder,
Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
void Function(BuildContext, Message)? onMessageActions, void Function(BuildContext, Message)? onMessageActions,
Message? message, Message? message,
MessageTheme? messageTheme, MessageTheme? messageTheme,
@@ -463,6 +473,9 @@ class MessageWidget extends StatefulWidget {
editMessageInputBuilder ?? this.editMessageInputBuilder, editMessageInputBuilder ?? this.editMessageInputBuilder,
textBuilder: textBuilder ?? this.textBuilder, textBuilder: textBuilder ?? this.textBuilder,
usernameBuilder: usernameBuilder ?? this.usernameBuilder, usernameBuilder: usernameBuilder ?? this.usernameBuilder,
bottomRowBuilder: bottomRowBuilder ?? this.bottomRowBuilder,
deletedBottomRowBuilder:
deletedBottomRowBuilder ?? this.deletedBottomRowBuilder,
onMessageActions: onMessageActions ?? this.onMessageActions, onMessageActions: onMessageActions ?? this.onMessageActions,
message: message ?? this.message, message: message ?? this.message,
messageTheme: messageTheme ?? this.messageTheme, messageTheme: messageTheme ?? this.messageTheme,
@@ -782,7 +795,11 @@ class _MessageWidgetState extends State<MessageWidget>
bottom: bottom:
isPinned && widget.showPinHighlight ? 6.0 : 0.0, isPinned && widget.showPinHighlight ? 6.0 : 0.0,
), ),
child: _bottomRow, child: widget.bottomRowBuilder?.call(
context,
widget.message,
) ??
_bottomRow,
), ),
if (isFailedState) if (isFailedState)
Positioned( Positioned(
@@ -810,7 +827,7 @@ class _MessageWidgetState extends State<MessageWidget>
} }
Widget _buildQuotedMessage() { 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 && final onTap = widget.message.quotedMessage?.isDeleted != true &&
widget.onQuotedMessageTap != null widget.onQuotedMessageTap != null
? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId) ? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId)
@@ -830,22 +847,11 @@ class _MessageWidgetState extends State<MessageWidget>
Widget get _bottomRow { Widget get _bottomRow {
if (isDeleted) { if (isDeleted) {
final chatThemeData = _streamChatTheme; return widget.deletedBottomRowBuilder?.call(
return Row( context,
mainAxisSize: MainAxisSize.min, widget.message,
children: [ ) ??
StreamSvgIcon.eye( const Offstage();
color: chatThemeData.colorTheme.textLowEmphasis,
size: 16,
),
const SizedBox(width: 8),
Text(
'Only visible to you',
style: chatThemeData.textTheme.footnote
.copyWith(color: chatThemeData.colorTheme.textLowEmphasis),
),
],
);
} }
final children = <Widget>[]; final children = <Widget>[];
@@ -854,9 +860,9 @@ class _MessageWidgetState extends State<MessageWidget>
final showThreadParticipants = threadParticipants?.isNotEmpty == true; final showThreadParticipants = threadParticipants?.isNotEmpty == true;
final replyCount = widget.message.replyCount; final replyCount = widget.message.replyCount;
var msg = 'Thread Reply'; var msg = context.translations.threadReplyLabel;
if (showThreadReplyIndicator && replyCount! > 1) { if (showThreadReplyIndicator && replyCount! > 1) {
msg = '$replyCount Thread Replies'; msg = context.translations.threadReplyCountText(replyCount);
} }
// ignore: prefer_function_declarations_over_variables // ignore: prefer_function_declarations_over_variables
@@ -993,7 +999,7 @@ class _MessageWidgetState extends State<MessageWidget>
Widget _buildReactionIndicator( Widget _buildReactionIndicator(
BuildContext context, BuildContext context,
) { ) {
final ownId = _streamChat.user!.id; final ownId = _streamChat.currentUser!.id;
final reactionsMap = <String, Reaction>{}; final reactionsMap = <String, Reaction>{};
widget.message.latestReactions?.forEach((element) { widget.message.latestReactions?.forEach((element) {
if (!reactionsMap.containsKey(element.type) || if (!reactionsMap.containsKey(element.type) ||
@@ -1054,8 +1060,8 @@ class _MessageWidgetState extends State<MessageWidget>
showReactionPickerIndicator: widget.showReactions && showReactionPickerIndicator: widget.showReactions &&
(widget.message.status == MessageSendingStatus.sent), (widget.message.status == MessageSendingStatus.sent),
showPinHighlight: false, showPinHighlight: false,
showUserAvatar: showUserAvatar: widget.message.user!.id ==
widget.message.user!.id == channel.client.state.user!.id channel.client.state.currentUser!.id
? DisplayWidget.gone ? DisplayWidget.gone
: DisplayWidget.show, : DisplayWidget.show,
), ),
@@ -1118,7 +1124,7 @@ class _MessageWidgetState extends State<MessageWidget>
(widget.message.status == MessageSendingStatus.sent), (widget.message.status == MessageSendingStatus.sent),
showPinHighlight: false, showPinHighlight: false,
showUserAvatar: showUserAvatar:
widget.message.user!.id == channel.client.state.user!.id widget.message.user!.id == channel.client.state.currentUser!.id
? DisplayWidget.gone ? DisplayWidget.gone
: DisplayWidget.show, : DisplayWidget.show,
), ),
@@ -1201,7 +1207,10 @@ class _MessageWidgetState extends State<MessageWidget>
); );
} }
return Text( return Text(
'Uploading $uploadRemaining/$totalAttachments ...', context.translations.attachmentsUploadProgressText(
remaining: uploadRemaining,
total: totalAttachments,
),
style: style, style: style,
); );
} }
@@ -1275,8 +1284,8 @@ class _MessageWidgetState extends State<MessageWidget>
} }
Widget _buildPinnedMessage(Message message) { Widget _buildPinnedMessage(Message message) {
final pinnedBy = message.pinnedBy; final pinnedBy = message.pinnedBy!;
final pinnedByMe = _streamChat.user!.id == pinnedBy!.id; final currentUser = _streamChat.currentUser!;
return Padding( return Padding(
padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8), padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8),
@@ -1290,7 +1299,10 @@ class _MessageWidgetState extends State<MessageWidget>
width: 4, width: 4,
), ),
Text( Text(
'Pinned by ${pinnedByMe ? 'You' : pinnedBy.name}', context.translations.pinnedByUserText(
pinnedBy: pinnedBy,
currentUser: currentUser,
),
style: TextStyle( style: TextStyle(
color: _streamChatTheme.colorTheme.textLowEmphasis, color: _streamChatTheme.colorTheme.textLowEmphasis,
fontSize: 13, fontSize: 13,
@@ -123,7 +123,7 @@ class ReactionBubble extends StatelessWidget {
); );
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
final userId = StreamChat.of(context).user?.id; final userId = StreamChat.of(context).currentUser?.id;
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 4, horizontal: 4,
@@ -102,7 +102,6 @@ class StreamChatState extends State<StreamChat> {
data: materialTheme.copyWith( data: materialTheme.copyWith(
primaryIconTheme: streamTheme.primaryIconTheme, primaryIconTheme: streamTheme.primaryIconTheme,
accentColor: streamTheme.colorTheme.accentPrimary, accentColor: streamTheme.colorTheme.accentPrimary,
scaffoldBackgroundColor: streamTheme.colorTheme.barsBg,
), ),
child: StreamChatCore( child: StreamChatCore(
client: client, client: client,
@@ -127,21 +126,34 @@ class StreamChatState extends State<StreamChat> {
return defaultTheme.merge(themeData); return defaultTheme.merge(themeData);
} }
// coverage:ignore-start
/// The current user /// 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 /// The current user as a stream
Stream<User?> get userStream => widget.client.state.userStream; @Deprecated(
'Use `.currentUserStream` instead, Will be removed in future releases',
)
Stream<User?> get userStream => widget.client.state.currentUserStream;
@override // coverage:ignore-end
void initState() {
super.initState(); /// The current user
} User? get currentUser => widget.client.state.currentUser;
/// The current user as a stream
Stream<User?> get currentUserStream => widget.client.state.currentUserStream;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
final locale = ui.window.locale; 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(); super.didChangeDependencies();
} }
} }

Some files were not shown because too many files have changed in this diff Show More