Merge branch 'develop' of https://github.com/GetStream/stream-chat-flutter into mark-read-fix

This commit is contained in:
Deven Joshi
2021-10-08 17:45:06 +05:30
89 changed files with 1032 additions and 859 deletions
+73
View File
@@ -0,0 +1,73 @@
name: Dart Code Metrics
env:
flutter_version: "2.5.0"
folders: "lib, test"
on:
pull_request:
push:
branches:
- master
- develop
jobs:
check:
name: dart-code-metrics
runs-on: ubuntu-latest
steps:
- name: "Git Checkout"
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: "Cache Flutter dependencies"
uses: actions/cache@v2
with:
path: /opt/hostedtoolcache/flutter
key: ${{ env.flutter_version }}-flutter
- name: "Install Flutter"
uses: subosito/flutter-action@v1
with:
flutter-version: ${{ env.flutter_version }}
- name: "Install Tools"
run: flutter pub global activate melos 1.0.0-dev.3
- name: "Bootstrap Workspace"
run: melos bootstrap
- name: "Stream Chat Metrics"
uses: dart-code-checker/dart-code-metrics-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
relative_path: 'packages/stream_chat'
folders: ${{ env.folders }}
- name: "Stream Chat Flutter Core Metrics"
uses: dart-code-checker/dart-code-metrics-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
relative_path: 'packages/stream_chat_flutter_core'
folders: ${{ env.folders }}
- name: "Stream Chat Flutter Metrics"
uses: dart-code-checker/dart-code-metrics-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
relative_path: 'packages/stream_chat_flutter'
folders: ${{ env.folders }}
- name: "Stream Chat Localizations Metrics"
uses: dart-code-checker/dart-code-metrics-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
relative_path: 'packages/stream_chat_localizations'
folders: ${{ env.folders }}
- name: "Stream Chat Persistence Metrics"
uses: dart-code-checker/dart-code-metrics-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
relative_path: 'packages/stream_chat_persistence'
folders: ${{ env.folders }}
@@ -31,7 +31,7 @@ jobs:
flutter-version: ${{ env.flutter_version }} flutter-version: ${{ env.flutter_version }}
- name: "Install Tools" - name: "Install Tools"
run: | run: |
flutter pub global activate melos flutter pub global activate melos 1.0.0-dev.3
- name: "Bootstrap Workspace" - name: "Bootstrap Workspace"
run: melos bootstrap run: melos bootstrap
- name: "Dart Analyze" - name: "Dart Analyze"
+35 -2
View File
@@ -1,7 +1,9 @@
analyzer: analyzer:
plugins:
- dart_code_metrics
exclude: exclude:
- packages/*/lib/**/*.g.dart - packages/*/lib/**/*.g.dart
- packages/*/lib/src/emoji - packages/*/lib/src/emoji/**
- packages/*/lib/**/*.freezed.dart - packages/*/lib/**/*.freezed.dart
linter: linter:
@@ -143,4 +145,35 @@ linter:
- cast_nullable_to_non_nullable - cast_nullable_to_non_nullable
- unnecessary_null_checks - unnecessary_null_checks
- tighten_type_of_initializing_formals - tighten_type_of_initializing_formals
- null_check_on_nullable_type_parameter - null_check_on_nullable_type_parameter
# https://dartcodemetrics.dev/docs/getting-started/introduction
dart_code_metrics:
rules:
# Dart Specific
- binary-expression-operand-order
- double-literal-format
- prefer-match-file-name:
exclude:
- packages/*/test/**
- packages/*/example/**
- packages/**/util/**
- packages/**/utils.dart
- packages/stream_chat/lib/src/client/client.dart
- packages/stream_chat/lib/src/core/api/responses.dart
- packages/stream_chat/lib/src/core/api/requests.dart
- packages/stream_chat/lib/src/core/platform_detector/**
- packages/stream_chat_persistence/lib/src/db/shared/**
- packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart
- no-boolean-literal-compare
- no-equal-then-else
- no-empty-block:
exclude:
- packages/*/test/**
- prefer-trailing-comma:
exclude:
- packages/*/test/**
# Flutter specific
- always-remove-listener
- avoid-unnecessary-setstate
+18 -3
View File
@@ -11,6 +11,10 @@ scripts:
run: melos run analyze && melos run format run: melos run analyze && melos run format
description: Run all static analysis checks description: Run all static analysis checks
analyze:all:
run: melos run analyze && melos run metrics
description: Run all
analyze: analyze:
run: | run: |
melos exec -c 5 --ignore="*example*" -- \ melos exec -c 5 --ignore="*example*" -- \
@@ -24,6 +28,14 @@ scripts:
description: | description: |
Run `flutter format --set-exit-if-changed .` in all packages. Run `flutter format --set-exit-if-changed .` in all packages.
metrics:
run: |
melos exec -c 1 --ignore="*example*" -- \
flutter pub run dart_code_metrics:metrics analyze lib
description: |
Run `dart_code_metrics` in all packages.
- Note: you can also rely on your IDEs Dart Analysis / Issues window.
lint:pub: lint:pub:
run: | run: |
melos exec -c 5 --no-private --ignore="*example*" -- \ melos exec -c 5 --no-private --ignore="*example*" -- \
@@ -71,10 +83,13 @@ scripts:
docs: docs:
run: | run: |
npm install -g https://github.com/GetStream/stream-chat-docusaurus-cli && npm install -g https://github.com/GetStream/stream-chat-docusaurus-cli &&
npx stream-chat-docusaurus -i -s npx stream-chat-docusaurus -i -s
description: Runs the docusaurus documentation locally. description: Runs the docusaurus documentation locally.
dev_dependencies:
dart_code_metrics: ^4.4.0
environment: environment:
sdk: '>=2.12.0 <3.0.0' sdk: '>=2.12.0 <3.0.0'
flutter: '>=1.22.4 <2.0.0' flutter: '>=1.17.0 <2.0.0'
+2 -3
View File
@@ -1,4 +1,4 @@
name: example name: stream_chat_example
description: A new Flutter project. description: A new Flutter project.
publish_to: "none" publish_to: "none"
@@ -11,8 +11,7 @@ dependencies:
cupertino_icons: ^1.0.0 cupertino_icons: ^1.0.0
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat: stream_chat: ^2.2.1
path: ../
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -521,7 +521,7 @@ class Channel {
state!.addMessage(message); state!.addMessage(message);
try { try {
if (message.attachments.any((it) => !it.uploadState.isSuccess) == true) { if (message.attachments.any((it) => !it.uploadState.isSuccess)) {
final attachmentsUploadCompleter = Completer<Message>(); final attachmentsUploadCompleter = Completer<Message>();
_messageAttachmentsUploadCompleter[message.id] = _messageAttachmentsUploadCompleter[message.id] =
attachmentsUploadCompleter; attachmentsUploadCompleter;
@@ -849,12 +849,12 @@ class Channel {
..update(type, (value) { ..update(type, (value) {
if (enforceUnique) return value; if (enforceUnique) return value;
return value + 1; return value + 1;
}, ifAbsent: () => 1), }, ifAbsent: () => 1), // ignore: prefer-trailing-comma
reactionScores: {...message.reactionScores ?? <String, int>{}} reactionScores: {...message.reactionScores ?? <String, int>{}}
..update(type, (value) { ..update(type, (value) {
if (enforceUnique) return value; if (enforceUnique) return value;
return value + 1; return value + 1;
}, ifAbsent: () => 1), }, ifAbsent: () => 1), // ignore: prefer-trailing-comma
latestReactions: latestReactions, latestReactions: latestReactions,
ownReactions: ownReactions, ownReactions: ownReactions,
); );
@@ -878,7 +878,9 @@ class Channel {
/// Delete a reaction from this channel. /// Delete a reaction from this 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.currentUser; final user = _client.state.currentUser;
@@ -1336,7 +1338,7 @@ class Channel {
type, type,
clearHistory: clearHistory, clearHistory: clearHistory,
); );
if (clearHistory == true) { if (clearHistory) {
state!.truncate(); state!.truncate();
final cid = _cid; final cid = _cid;
if (cid != null) { if (cid != null) {
@@ -1501,28 +1503,27 @@ class ChannelClientState {
final expiredAttachmentMessagesId = channelState.messages final expiredAttachmentMessagesId = channelState.messages
.where((m) => .where((m) =>
!_updatedMessagesIds.contains(m.id) && !_updatedMessagesIds.contains(m.id) &&
m.attachments.isNotEmpty == true && m.attachments.isNotEmpty &&
m.attachments.any((e) { m.attachments.any((e) {
final url = e.imageUrl ?? e.assetUrl; final url = e.imageUrl ?? e.assetUrl;
if (url == null || !url.contains('')) { if (url == null || !url.contains('')) {
return false; return false;
} }
final uri = Uri.parse(url); final uri = Uri.parse(url);
if (!uri.host.endsWith('stream-io-cdn.com') || if (!uri.host.endsWith('stream-io-cdn.com') ||
uri.queryParameters['Expires'] == null) { uri.queryParameters['Expires'] == null) {
return false; return false;
} }
final secondsFromEpoch = final secondsFromEpoch =
int.parse(uri.queryParameters['Expires']!); int.parse(uri.queryParameters['Expires']!);
final expiration = DateTime.fromMillisecondsSinceEpoch( final expiration =
secondsFromEpoch * 1000); DateTime.fromMillisecondsSinceEpoch(secondsFromEpoch * 1000);
return expiration.isBefore(DateTime.now()); return expiration.isBefore(DateTime.now());
}) == }))
true)
.map((e) => e.id) .map((e) => e.id)
.toList(); .toList();
if (expiredAttachmentMessagesId.isNotEmpty == true) { if (expiredAttachmentMessagesId.isNotEmpty) {
await _channel._initializedCompleter.future; await _channel._initializedCompleter.future;
_updatedMessagesIds.addAll(expiredAttachmentMessagesId); _updatedMessagesIds.addAll(expiredAttachmentMessagesId);
_channel.getMessagesById(expiredAttachmentMessagesId); _channel.getMessagesById(expiredAttachmentMessagesId);
@@ -1546,7 +1547,8 @@ class ChannelClientState {
final user = e.user; final user = e.user;
updateChannelState(channelState.copyWith( updateChannelState(channelState.copyWith(
members: List.from( members: List.from(
channelState.members..removeWhere((m) => m.userId == user!.id)), channelState.members..removeWhere((m) => m.userId == user!.id),
),
)); ));
})); }));
} }
@@ -1647,7 +1649,7 @@ class ChannelClientState {
); );
addMessage(message); addMessage(message);
if (message.pinned == true) { if (message.pinned) {
_channelState = _channelState.copyWith( _channelState = _channelState.copyWith(
pinnedMessages: [ pinnedMessages: [
..._channelState.pinnedMessages, ..._channelState.pinnedMessages,
@@ -1794,9 +1796,8 @@ class ChannelClientState {
channelStateStream.map((cs) => cs.pinnedMessages.toList()); channelStateStream.map((cs) => cs.pinnedMessages.toList());
/// Get channel last message. /// Get channel last message.
Message? get lastMessage => _channelState.messages.isNotEmpty == true Message? get lastMessage =>
? _channelState.messages.last _channelState.messages.isNotEmpty ? _channelState.messages.last : null;
: null;
/// Get channel last message. /// Get channel last message.
Stream<Message?> get lastMessageStream => Stream<Message?> get lastMessageStream =>
@@ -1859,8 +1860,8 @@ class ChannelClientState {
(m) => m.user.id == message.user?.id, (m) => m.user.id == message.user?.id,
) != ) !=
null; null;
return message.silent != true && return !message.silent &&
message.shadowed != true && !message.shadowed &&
message.user?.id != userId && message.user?.id != userId &&
!userIsMuted; !userIsMuted;
} }
@@ -1898,9 +1899,7 @@ class ChannelClientState {
...updatedState.messages, ...updatedState.messages,
..._channelState.messages ..._channelState.messages
.where((m) => .where((m) =>
updatedState.messages !updatedState.messages.any((newMessage) => newMessage.id == m.id))
.any((newMessage) => newMessage.id == m.id) !=
true)
.toList(), .toList(),
]..sort(_sortByCreatedAt); ]..sort(_sortByCreatedAt);
@@ -1908,9 +1907,7 @@ class ChannelClientState {
...updatedState.watchers, ...updatedState.watchers,
..._channelState.watchers ..._channelState.watchers
.where((w) => .where((w) =>
updatedState.watchers !updatedState.watchers.any((newWatcher) => newWatcher.id == w.id))
.any((newWatcher) => newWatcher.id == w.id) !=
true)
.toList(), .toList(),
]; ];
@@ -1922,9 +1919,7 @@ class ChannelClientState {
...updatedState.read, ...updatedState.read,
..._channelState.read ..._channelState.read
.where((r) => .where((r) =>
updatedState.read !updatedState.read.any((newRead) => newRead.user.id == r.user.id))
.any((newRead) => newRead.user.id == r.user.id) !=
true)
.toList(), .toList(),
]; ];
@@ -2031,7 +2026,7 @@ class ChannelClientState {
.on() .on()
.where((event) => .where((event) =>
event.user != null && event.user != null &&
members.any((m) => m.userId == event.user!.id) == true) members.any((m) => m.userId == event.user!.id))
.listen( .listen(
(event) { (event) {
final newMembers = List<Member>.from(members); final newMembers = List<Member>.from(members);
@@ -75,7 +75,7 @@ class ChannelApi {
if (messageLimit != null) 'message_limit': messageLimit, if (messageLimit != null) 'message_limit': messageLimit,
// pagination // pagination
...paginationParams.toJson() ...paginationParams.toJson(),
}), }),
}, },
); );
@@ -7,7 +7,7 @@ enum PushProvider {
firebase, firebase,
/// Send notifications using Apple's Push Notification service /// Send notifications using Apple's Push Notification service
apn apn,
} }
/// Helper extension for [PushProvider] /// Helper extension for [PushProvider]
@@ -90,7 +90,7 @@ enum ChatErrorCode {
internalSystemError, internalSystemError,
/// No access to requested channels /// No access to requested channels
noAccessToChannels noAccessToChannels,
} }
const _errorCodeWithDescription = { const _errorCodeWithDescription = {
@@ -98,14 +98,20 @@ const _errorCodeWithDescription = {
MapEntry(1000, 'Unauthorised, token not defined'), MapEntry(1000, 'Unauthorised, token not defined'),
ChatErrorCode.inputError: ChatErrorCode.inputError:
MapEntry(4, 'Wrong data/parameter is sent to the API'), MapEntry(4, 'Wrong data/parameter is sent to the API'),
ChatErrorCode.duplicateUsername: MapEntry(6, ChatErrorCode.duplicateUsername: MapEntry(
'Duplicate username is sent while enforce_unique_usernames is enabled'), 6,
'Duplicate username is sent while enforce_unique_usernames is enabled',
),
ChatErrorCode.messageTooLong: MapEntry(20, 'Message is too long'), ChatErrorCode.messageTooLong: MapEntry(20, 'Message is too long'),
ChatErrorCode.eventNotSupported: MapEntry(18, 'Event is not supported'), ChatErrorCode.eventNotSupported: MapEntry(18, 'Event is not supported'),
ChatErrorCode.channelFeatureNotSupported: MapEntry(19, ChatErrorCode.channelFeatureNotSupported: MapEntry(
'The feature is currently disabled on the dashboard (i.e. Reactions & Replies)'), 19,
ChatErrorCode.multipleNestling: MapEntry(21, 'The feature is currently disabled on the dashboard (i.e. Reactions & Replies)',
'Multiple Levels Reply is not supported - the API only supports 1 level deep reply threads'), ),
ChatErrorCode.multipleNestling: MapEntry(
21,
'Multiple Levels Reply is not supported - the API only supports 1 level deep reply threads',
),
ChatErrorCode.customCommandEndpointCall: ChatErrorCode.customCommandEndpointCall:
MapEntry(45, 'Custom Command handler returned an error'), MapEntry(45, 'Custom Command handler returned an error'),
ChatErrorCode.customCommandEndpointMissing: ChatErrorCode.customCommandEndpointMissing:
@@ -35,7 +35,8 @@ class StreamWebSocketError extends StreamChatError {
/// ///
factory StreamWebSocketError.fromWebSocketChannelError( factory StreamWebSocketError.fromWebSocketChannelError(
WebSocketChannelException error) { WebSocketChannelException error,
) {
final message = error.message ?? ''; final message = error.message ?? '';
return StreamWebSocketError(message); return StreamWebSocketError(message);
} }
@@ -105,8 +105,11 @@ class LoggingInterceptor extends Interceptor {
final formDataMap = <String, dynamic>{} final formDataMap = <String, dynamic>{}
..addEntries(data.fields) ..addEntries(data.fields)
..addEntries(data.files); ..addEntries(data.files);
_printMapAsTable(_logPrintRequest, formDataMap, _printMapAsTable(
header: 'Form data | ${data.boundary}'); _logPrintRequest,
formDataMap,
header: 'Form data | ${data.boundary}',
);
} else { } else {
_printBlock(_logPrintRequest, data.toString()); _printBlock(_logPrintRequest, data.toString());
} }
@@ -201,14 +204,19 @@ class LoggingInterceptor extends Interceptor {
} }
void _printRequestHeader( void _printRequestHeader(
void Function(Object) logPrint, RequestOptions options) { void Function(Object) logPrint,
RequestOptions options,
) {
final uri = options.uri; final uri = options.uri;
final method = options.method; final method = options.method;
_printBoxed(logPrint, header: 'Request ║ $method ', text: uri.toString()); _printBoxed(logPrint, header: 'Request ║ $method ', text: uri.toString());
} }
void _printLine(void Function(Object) logPrint, void _printLine(
[String pre = '', String suf = '']) => void Function(Object) logPrint, [
String pre = '',
String suf = '',
]) =>
logPrint('$pre${'' * maxWidth}$suf'); logPrint('$pre${'' * maxWidth}$suf');
void _printKV(void Function(Object) logPrint, String? key, Object? v) { void _printKV(void Function(Object) logPrint, String? key, Object? v) {
@@ -227,8 +235,10 @@ class LoggingInterceptor extends Interceptor {
final lines = (msg.length / maxWidth).ceil(); final lines = (msg.length / maxWidth).ceil();
for (var i = 0; i < lines; ++i) { for (var i = 0; i < lines; ++i) {
logPrint((i >= 0 ? '' : '') + logPrint((i >= 0 ? '' : '') +
msg.substring(i * maxWidth, msg.substring(
math.min<int>(i * maxWidth + maxWidth, msg.length))); i * maxWidth,
math.min<int>(i * maxWidth + maxWidth, msg.length),
));
} }
} }
@@ -301,8 +311,13 @@ class LoggingInterceptor extends Interceptor {
if (compact) { if (compact) {
logPrint('${_indent(tabs)} $e${!isLast ? ',' : ''}'); logPrint('${_indent(tabs)} $e${!isLast ? ',' : ''}');
} else { } else {
_printPrettyMap(logPrint, e, _printPrettyMap(
tabs: tabs + 1, isListItem: true, isLast: isLast); logPrint,
e,
tabs: tabs + 1,
isListItem: true,
isLast: isLast,
);
} }
} else { } else {
logPrint('${_indent(tabs + 2)} $e${isLast ? '' : ','}'); logPrint('${_indent(tabs + 2)} $e${isLast ? '' : ','}');
@@ -56,12 +56,15 @@ class Attachment extends Equatable {
/// Create a new instance from a json /// Create a new instance from a json
factory Attachment.fromJson(Map<String, dynamic> json) => factory Attachment.fromJson(Map<String, dynamic> json) =>
_$AttachmentFromJson( _$AttachmentFromJson(
Serializer.moveToExtraDataFromRoot(json, topLevelFields)); Serializer.moveToExtraDataFromRoot(json, topLevelFields),
);
/// Create a new instance from a db data /// Create a new instance from a db data
factory Attachment.fromData(Map<String, dynamic> json) => factory Attachment.fromData(Map<String, dynamic> json) =>
_$AttachmentFromJson(Serializer.moveToExtraDataFromRoot( _$AttachmentFromJson(Serializer.moveToExtraDataFromRoot(
json, topLevelFields + dbSpecificTopLevelFields)); json,
topLevelFields + dbSpecificTopLevelFields,
));
///The attachment type based on the URL resource. This can be: audio, ///The attachment type based on the URL resource. This can be: audio,
///image or video ///image or video
@@ -11,54 +11,6 @@ part 'attachment_file.freezed.dart';
part 'attachment_file.g.dart'; part 'attachment_file.g.dart';
/// Union class to hold various [UploadState] of a attachment.
@freezed
class UploadState with _$UploadState {
/// Preparing state of the union
const factory UploadState.preparing() = Preparing;
/// InProgress state of the union
const factory UploadState.inProgress({
required int uploaded,
required int total,
}) = InProgress;
/// Success state of the union
const factory UploadState.success() = Success;
/// Failed state of the union
const factory UploadState.failed({required String error}) = Failed;
/// Creates a new instance from a json
factory UploadState.fromJson(Map<String, dynamic> json) =>
_$UploadStateFromJson(json);
}
/// Helper extension for UploadState
extension UploadStateX on UploadState? {
/// Returns true if state is [Preparing]
bool get isPreparing => this is Preparing;
/// Returns true if state is [InProgress]
bool get isInProgress => this is InProgress;
/// Returns true if state is [Success]
bool get isSuccess => this is Success;
/// Returns true if state is [Failed]
bool get isFailed => this is Failed;
}
Uint8List? _fromString(String? bytes) {
if (bytes == null) return null;
return Uint8List.fromList(bytes.codeUnits);
}
String? _toString(Uint8List? bytes) {
if (bytes == null) return null;
return String.fromCharCodes(bytes);
}
/// The class that contains the information about an attachment file /// The class that contains the information about an attachment file
@JsonSerializable() @JsonSerializable()
class AttachmentFile { class AttachmentFile {
@@ -135,3 +87,51 @@ class AttachmentFile {
return multiPartFile; return multiPartFile;
} }
} }
/// Union class to hold various [UploadState] of a attachment.
@freezed
class UploadState with _$UploadState {
/// Preparing state of the union
const factory UploadState.preparing() = Preparing;
/// InProgress state of the union
const factory UploadState.inProgress({
required int uploaded,
required int total,
}) = InProgress;
/// Success state of the union
const factory UploadState.success() = Success;
/// Failed state of the union
const factory UploadState.failed({required String error}) = Failed;
/// Creates a new instance from a json
factory UploadState.fromJson(Map<String, dynamic> json) =>
_$UploadStateFromJson(json);
}
/// Helper extension for UploadState
extension UploadStateX on UploadState? {
/// Returns true if state is [Preparing]
bool get isPreparing => this is Preparing;
/// Returns true if state is [InProgress]
bool get isInProgress => this is InProgress;
/// Returns true if state is [Success]
bool get isSuccess => this is Success;
/// Returns true if state is [Failed]
bool get isFailed => this is Failed;
}
Uint8List? _fromString(String? bytes) {
if (bytes == null) return null;
return Uint8List.fromList(bytes.codeUnits);
}
String? _toString(Uint8List? bytes) {
if (bytes == null) return null;
return String.fromCharCodes(bytes);
}
@@ -38,7 +38,8 @@ class ChannelModel {
/// Create a new instance from a json /// Create a new instance from a json
factory ChannelModel.fromJson(Map<String, dynamic> json) => factory ChannelModel.fromJson(Map<String, dynamic> json) =>
_$ChannelModelFromJson( _$ChannelModelFromJson(
Serializer.moveToExtraDataFromRoot(json, topLevelFields)); Serializer.moveToExtraDataFromRoot(json, topLevelFields),
);
/// The id of this channel /// The id of this channel
final String id; final String id;
@@ -81,7 +81,8 @@ class Message extends Equatable {
/// Create a new instance from a json /// Create a new instance from a json
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson( factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
Serializer.moveToExtraDataFromRoot(json, topLevelFields)); Serializer.moveToExtraDataFromRoot(json, topLevelFields),
);
/// The message ID. This is either created by Stream or set client side when /// The message ID. This is either created by Stream or set client side when
/// the message is added. /// the message is added.
@@ -48,7 +48,8 @@ class OwnUser extends User {
/// Create a new instance from json. /// Create a new instance from json.
factory OwnUser.fromJson(Map<String, dynamic> json) => _$OwnUserFromJson( factory OwnUser.fromJson(Map<String, dynamic> json) => _$OwnUserFromJson(
Serializer.moveToExtraDataFromRoot(json, topLevelFields)); Serializer.moveToExtraDataFromRoot(json, topLevelFields),
);
/// Create a new instance from [User] object. /// Create a new instance from [User] object.
factory OwnUser.fromUser(User user) => OwnUser( factory OwnUser.fromUser(User user) => OwnUser(
+2 -1
View File
@@ -27,7 +27,8 @@ dependencies:
dev_dependencies: dev_dependencies:
build_runner: ^2.0.1 build_runner: ^2.0.1
dart_code_metrics: ^4.4.0
freezed: ^0.14.1+3 freezed: ^0.14.1+3
json_serializable: ^5.0.2 json_serializable: ^5.0.2
mocktail: ^0.1.1 mocktail: ^0.1.1
test: ^1.18.2 test: ^1.17.12
@@ -291,7 +291,7 @@ void main() {
(index) => Attachment( (index) => Attachment(
id: 'test-attachment-id-$index', id: 'test-attachment-id-$index',
type: index.isEven ? 'image' : 'file', type: index.isEven ? 'image' : 'file',
file: AttachmentFile(size: 33 * index, path: 'test-file-path'), file: AttachmentFile(size: index * 33, path: 'test-file-path'),
), ),
); );
@@ -498,7 +498,7 @@ void main() {
(index) => Attachment( (index) => Attachment(
id: 'test-attachment-id-$index', id: 'test-attachment-id-$index',
type: index.isEven ? 'image' : 'file', type: index.isEven ? 'image' : 'file',
file: AttachmentFile(size: 33 * index, path: 'test-file-path'), file: AttachmentFile(size: index * 33, path: 'test-file-path'),
), ),
); );
@@ -34,7 +34,7 @@ Future<void> main() async {
await client.connectUser( await client.connectUser(
User(id: 'super-band-9'), User(id: 'super-band-9'),
'''eyJ0eXAiO«iJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
); );
runApp( runApp(
@@ -1,4 +1,4 @@
name: example name: stream_chat_flutter_example
description: A new Flutter project. description: A new Flutter project.
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
@@ -27,17 +27,9 @@ dependencies:
cupertino_icons: ^1.0.3 cupertino_icons: ^1.0.3
flutter: flutter:
sdk: flutter sdk: flutter
# stream_chat: stream_chat_flutter: ^2.2.1
# path: ../../stream_chat stream_chat_localizations: ^1.1.0
# stream_chat_flutter_core: stream_chat_persistence: ^2.2.0
# path: ../../stream_chat_flutter_core
stream_chat_flutter:
path: ../
stream_chat_localizations:
path: ../../stream_chat_localizations
stream_chat_persistence:
path: ../../stream_chat_persistence
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -141,7 +141,7 @@ class _PreparingState extends StatelessWidget {
uploaded: 0, uploaded: 0,
total: double.maxFinite.toInt(), total: double.maxFinite.toInt(),
), ),
) ),
], ],
); );
} }
@@ -181,7 +181,7 @@ class _InProgressState extends StatelessWidget {
uploaded: sent, uploaded: sent,
total: total, total: total,
), ),
) ),
], ],
); );
} }
@@ -234,7 +234,7 @@ class _FailedState extends StatelessWidget {
), ),
), ),
), ),
) ),
], ],
); );
} }
@@ -78,7 +78,7 @@ class AttachmentError extends StatelessWidget {
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.accentError .accentError
.withOpacity(.1), .withOpacity(0.1),
child: Center( child: Center(
child: Icon( child: Icon(
Icons.error_outline, Icons.error_outline,
@@ -253,7 +253,8 @@ class FileAttachment extends AttachmentWidget {
if (message.status == MessageSendingStatus.sent) { if (message.status == MessageSendingStatus.sent) {
trailingWidget = IconButton( trailingWidget = IconButton(
icon: StreamSvgIcon.cloudDownload( icon: StreamSvgIcon.cloudDownload(
color: theme.colorTheme.textHighEmphasis), color: theme.colorTheme.textHighEmphasis,
),
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
splashRadius: 16, splashRadius: 16,
onPressed: () { onPressed: () {
@@ -297,7 +297,7 @@ class GiphyAttachment extends AttachmentWidget {
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.textHighEmphasis .textHighEmphasis
.withOpacity(.5), .withOpacity(0.5),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
@@ -99,12 +99,14 @@ class AttachmentActionsModal extends StatelessWidget {
() { () {
final attachment = message.attachments[currentIndex]; final attachment = message.attachments[currentIndex];
final isImage = attachment.type == 'image'; final isImage = attachment.type == 'image';
final Future<String?> Function(Attachment, final Future<String?> Function(
{void Function(int, int) progressCallback}) Attachment, {
saveFile = fileDownloader ?? _downloadAttachment; void Function(int, int) progressCallback,
final Future<String?> Function(Attachment, }) saveFile = fileDownloader ?? _downloadAttachment;
{void Function(int, int) progressCallback}) final Future<String?> Function(
saveImage = imageDownloader ?? _downloadAttachment; Attachment, {
void Function(int, int) progressCallback,
}) saveImage = imageDownloader ?? _downloadAttachment;
final downloader = isImage ? saveImage : saveFile; final downloader = isImage ? saveImage : saveFile;
final progressNotifier = final progressNotifier =
@@ -183,7 +185,7 @@ class AttachmentActionsModal extends StatelessWidget {
), ),
), ),
), ),
) ),
], ],
); );
} }
@@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/src/unread_indicator.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Back button implementation /// Back button implementation
// ignore: prefer-match-file-name
class StreamBackButton extends StatelessWidget { class StreamBackButton extends StatelessWidget {
/// Constructor for creating back button /// Constructor for creating back button
const StreamBackButton({ const StreamBackButton({
@@ -77,7 +77,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
UserAvatar( UserAvatar(
user: members user: members
.firstWhere( .firstWhere(
(e) => e.user?.id != userAsMember.user?.id) (e) => e.user?.id != userAsMember.user?.id,
)
.user!, .user!,
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxHeight: 64, maxHeight: 64,
@@ -93,7 +94,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
Text( Text(
members members
.firstWhere( .firstWhere(
(e) => e.user?.id != userAsMember.user?.id) (e) => e.user?.id != userAsMember.user?.id,
)
.user .user
?.name ?? ?.name ??
'', '',
@@ -185,7 +185,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
onPressed: onNewChatButtonTap, onPressed: onNewChatButtonTap,
), ),
) ),
], ],
title: Column( title: Column(
children: [ children: [
@@ -565,7 +565,8 @@ class _ChannelListViewState extends State<ChannelListView> {
'owner', 'owner',
].contains(channel.state!.members ].contains(channel.state!.members
.firstWhereOrNull( .firstWhereOrNull(
(m) => m.userId == channel.client.state.currentUser?.id) (m) => m.userId == channel.client.state.currentUser?.id,
)
?.role)) ?.role))
IconSlideAction( IconSlideAction(
color: backgroundColor, color: backgroundColor,
@@ -72,85 +72,82 @@ class ChannelPreview extends StatelessWidget {
final channelPreviewTheme = ChannelPreviewTheme.of(context); final channelPreviewTheme = ChannelPreviewTheme.of(context);
final streamChatState = StreamChat.of(context); final streamChatState = StreamChat.of(context);
return BetterStreamBuilder<bool>( return BetterStreamBuilder<bool>(
stream: channel.isMutedStream, stream: channel.isMutedStream,
initialData: channel.isMuted, initialData: channel.isMuted,
builder: (context, data) => AnimatedOpacity( builder: (context, data) => AnimatedOpacity(
opacity: data ? 0.5 : 1, opacity: data ? 0.5 : 1,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: ListTile( child: ListTile(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
), ),
onTap: () => onTap?.call(channel), onTap: () => onTap?.call(channel),
onLongPress: () => onLongPress?.call(channel), onLongPress: () => onLongPress?.call(channel),
leading: leading ?? ChannelAvatar(onTap: onImageTap), leading: leading ?? ChannelAvatar(onTap: onImageTap),
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
child: title ?? child: title ??
ChannelName( ChannelName(
textStyle: channelPreviewTheme.titleStyle, textStyle: channelPreviewTheme.titleStyle,
),
), ),
BetterStreamBuilder<List<Member>>(
stream: channel.state?.membersStream,
initialData: channel.state?.members,
comparator: const ListEquality().equals,
builder: (context, members) {
if (members.isEmpty ||
members.any((Member e) =>
e.user!.id ==
channel.client.state.currentUser?.id) !=
true) {
return const SizedBox();
}
return UnreadIndicator(
cid: channel.cid,
);
},
),
],
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(child: subtitle ?? _buildSubtitle(context)),
sendingIndicator ??
Builder(
builder: (context) {
final lastMessage =
channel.state?.messages.lastWhereOrNull(
(m) => !m.isDeleted && m.shadowed != true,
);
if (lastMessage?.user?.id ==
streamChatState.currentUser?.id) {
return Padding(
padding: const EdgeInsets.only(right: 4),
child: SendingIndicator(
message: lastMessage!,
size: channelPreviewTheme.indicatorIconSize,
isMessageRead: channel.state!.read
.where((element) =>
element.user.id !=
channel
.client.state.currentUser!.id)
.where((element) => element.lastRead
.isAfter(lastMessage.createdAt))
.isNotEmpty ==
true,
),
);
}
return const SizedBox();
},
),
trailing ?? _buildDate(context),
],
),
), ),
)); BetterStreamBuilder<List<Member>>(
stream: channel.state?.membersStream,
initialData: channel.state?.members,
comparator: const ListEquality().equals,
builder: (context, members) {
if (members.isEmpty ||
!members.any((Member e) =>
e.user!.id == channel.client.state.currentUser?.id)) {
return const SizedBox();
}
return UnreadIndicator(
cid: channel.cid,
);
},
),
],
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(child: subtitle ?? _buildSubtitle(context)),
sendingIndicator ??
Builder(
builder: (context) {
final lastMessage =
channel.state?.messages.lastWhereOrNull(
(m) => !m.isDeleted && !m.shadowed,
);
if (lastMessage?.user?.id ==
streamChatState.currentUser?.id) {
return Padding(
padding: const EdgeInsets.only(right: 4),
child: SendingIndicator(
message: lastMessage!,
size: channelPreviewTheme.indicatorIconSize,
isMessageRead: channel.state!.read
.where((element) =>
element.user.id !=
channel.client.state.currentUser!.id)
.where((element) => element.lastRead
.isAfter(lastMessage.createdAt))
.isNotEmpty,
),
);
}
return const SizedBox();
},
),
trailing ?? _buildDate(context),
],
),
),
),
);
} }
Widget _buildDate(BuildContext context) => BetterStreamBuilder<DateTime>( Widget _buildDate(BuildContext context) => BetterStreamBuilder<DateTime>(
@@ -246,10 +243,11 @@ class ChannelPreview extends StatelessWidget {
lastMessage.mentionedUsers, lastMessage.mentionedUsers,
lastMessage.attachments, lastMessage.attachments,
channelPreviewTheme.subtitleStyle?.copyWith( channelPreviewTheme.subtitleStyle?.copyWith(
color: channelPreviewTheme.subtitleStyle?.color, color: channelPreviewTheme.subtitleStyle?.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic ? FontStyle.italic
: FontStyle.normal), : FontStyle.normal,
),
channelPreviewTheme.subtitleStyle?.copyWith( channelPreviewTheme.subtitleStyle?.copyWith(
color: channelPreviewTheme.subtitleStyle?.color, color: channelPreviewTheme.subtitleStyle?.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
@@ -49,8 +49,9 @@ class CommandsOverlay extends StatelessWidget {
child: Container( child: Container(
constraints: BoxConstraints.loose(size), constraints: BoxConstraints.loose(size),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.barsBg, color: _streamChatTheme.colorTheme.barsBg,
borderRadius: BorderRadius.circular(8)), borderRadius: BorderRadius.circular(8),
),
child: ListView( child: ListView(
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
shrinkWrap: true, shrinkWrap: true,
@@ -72,9 +73,9 @@ class CommandsOverlay extends StatelessWidget {
context.translations.instantCommandsLabel, context.translations.instantCommandsLabel,
style: TextStyle( style: TextStyle(
color: _streamChatTheme.colorTheme.textHighEmphasis color: _streamChatTheme.colorTheme.textHighEmphasis
.withOpacity(.5), .withOpacity(0.5),
), ),
) ),
], ],
), ),
), ),
@@ -102,7 +103,8 @@ class CommandsOverlay extends StatelessWidget {
TextSpan( TextSpan(
text: c.name.capitalize(), text: c.name.capitalize(),
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold,
),
children: [ children: [
TextSpan( TextSpan(
text: ' /${c.name} ${c.args}', text: ' /${c.name} ${c.args}',
@@ -132,7 +134,9 @@ class CommandsOverlay extends StatelessWidget {
} }
Widget _buildCommandIcon( Widget _buildCommandIcon(
StreamChatThemeData _streamChatTheme, String iconType) { StreamChatThemeData _streamChatTheme,
String iconType,
) {
switch (iconType) { switch (iconType) {
case 'giphy': case 'giphy':
return CircleAvatar( return CircleAvatar(
@@ -21,25 +21,16 @@ class DateDivider extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final createdAt = Jiffy(dateTime); final createdAt = Jiffy(dateTime);
final now = DateTime.now(); final now = Jiffy(DateTime.now());
String dayInfo; var dayInfo = createdAt.MMMd;
if (Jiffy(createdAt).isSame(now, Units.DAY)) { if (createdAt.isSame(now, Units.DAY)) {
dayInfo = context.translations.todayLabel; dayInfo = context.translations.todayLabel;
} else if (Jiffy(createdAt) } else if (createdAt.isSame(now.subtract(days: 1), Units.DAY)) {
.isSame(now.subtract(const Duration(days: 1)), Units.DAY)) {
dayInfo = context.translations.yesterdayLabel; dayInfo = context.translations.yesterdayLabel;
} else if (Jiffy(createdAt).isAfter( } else if (createdAt.isAfter(now.subtract(days: 7), Units.DAY)) {
now.subtract(const Duration(days: 7)),
Units.DAY,
)) {
dayInfo = createdAt.EEEE; dayInfo = createdAt.EEEE;
} else if (Jiffy(createdAt).isAfter( } else if (createdAt.isAfter(now.subtract(years: 1), Units.DAY)) {
Jiffy(now).subtract(years: 1),
Units.DAY,
)) {
dayInfo = createdAt.MMMd;
} else {
dayInfo = createdAt.MMMd; dayInfo = createdAt.MMMd;
} }
@@ -81,10 +81,10 @@ class EmojiOverlay extends StatelessWidget {
), ),
style: TextStyle( style: TextStyle(
color: _streamChatTheme.colorTheme.textHighEmphasis color: _streamChatTheme.colorTheme.textHighEmphasis
.withOpacity(.5), .withOpacity(0.5),
), ),
), ),
) ),
], ],
), ),
); );
@@ -127,7 +127,8 @@ extension FlipBorder on BorderRadius {
topLeft: topRight, topLeft: topRight,
topRight: topLeft, topRight: topLeft,
bottomLeft: bottomRight, bottomLeft: bottomRight,
bottomRight: bottomLeft) bottomRight: bottomLeft,
)
: this; : this;
} }
@@ -87,7 +87,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
await Future.wait(videoPackages.values.map( await Future.wait(videoPackages.values.map(
(it) => it.initialize(), (it) => it.initialize(),
)); ));
setState(() {}); setState(() {}); // ignore: no-empty-block
} }
@override @override
@@ -96,83 +96,83 @@ class _FullScreenMediaState extends State<FullScreenMedia>
body: Stack( body: Stack(
children: [ children: [
AnimatedBuilder( AnimatedBuilder(
animation: _controller, animation: _controller,
builder: (context, snapshot) => PageView.builder( builder: (context, snapshot) => PageView.builder(
controller: _pageController, controller: _pageController,
onPageChanged: (val) { onPageChanged: (val) {
setState(() {
_currentPage = val;
});
},
itemBuilder: (context, index) {
final attachment = widget.mediaAttachments[index];
if (attachment.type == 'image' ||
attachment.type == 'giphy') {
final imageUrl = attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl;
return PhotoView(
loadingBuilder: (context, image) => const Offstage(),
imageProvider: (imageUrl == null &&
attachment.localUri != null &&
attachment.file?.bytes != null)
? Image.memory(attachment.file!.bytes!).image
: CachedNetworkImageProvider(imageUrl!),
maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(
tag: widget.mediaAttachments,
),
backgroundDecoration: BoxDecoration(
color: ColorTween(
begin: ChannelHeaderTheme.of(context).color,
end: Colors.black,
).lerp(_controller.value),
),
onTapUp: (a, b, c) {
setState(() { setState(() {
_currentPage = val; _optionsShown = !_optionsShown;
}); });
}, if (_controller.isCompleted) {
itemBuilder: (context, index) { _controller.reverse();
final attachment = widget.mediaAttachments[index]; } else {
if (attachment.type == 'image' || _controller.forward();
attachment.type == 'giphy') {
final imageUrl = attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl;
return PhotoView(
loadingBuilder: (context, image) =>
const Offstage(),
imageProvider: (imageUrl == null &&
attachment.localUri != null &&
attachment.file?.bytes != null)
? Image.memory(attachment.file!.bytes!).image
: CachedNetworkImageProvider(imageUrl!),
maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(
tag: widget.mediaAttachments,
),
backgroundDecoration: BoxDecoration(
color: ColorTween(
begin: ChannelHeaderTheme.of(context).color,
end: Colors.black,
).lerp(_controller.value),
),
onTapUp: (a, b, c) {
setState(() {
_optionsShown = !_optionsShown;
});
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
);
} else if (attachment.type == 'video') {
final controller = videoPackages[attachment.id]!;
if (!controller.initialized) {
return const Center(
child: CircularProgressIndicator(),
);
}
return InkWell(
onTap: () {
setState(() {
_optionsShown = !_optionsShown;
});
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 50,
),
child: Chewie(
controller: controller.chewieController!,
),
),
);
} }
return Container();
}, },
itemCount: widget.mediaAttachments.length, );
)), } else if (attachment.type == 'video') {
final controller = videoPackages[attachment.id]!;
if (!controller.initialized) {
return const Center(
child: CircularProgressIndicator(),
);
}
return InkWell(
onTap: () {
setState(() {
_optionsShown = !_optionsShown;
});
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 50,
),
child: Chewie(
controller: controller.chewieController!,
),
),
);
}
return Container();
},
itemCount: widget.mediaAttachments.length,
),
),
AnimatedOpacity( AnimatedOpacity(
opacity: _optionsShown ? 1.0 : 0.0, opacity: _optionsShown ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
@@ -67,19 +66,6 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
} }
class _GalleryFooterState extends State<GalleryFooter> { class _GalleryFooterState extends State<GalleryFooter> {
final TextEditingController _messageController = TextEditingController();
final FocusNode _messageFocusNode = FocusNode();
final List<Channel> _selectedChannels = [];
@override
void initState() {
super.initState();
_messageFocusNode.addListener(() {
setState(() {});
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const showShareButton = !kIsWeb; const showShareButton = !kIsWeb;
@@ -143,8 +129,9 @@ class _GalleryFooterState extends State<GalleryFooter> {
children: <Widget>[ children: <Widget>[
Text( Text(
context.translations.galleryPaginationText( context.translations.galleryPaginationText(
currentPage: widget.currentPage, currentPage: widget.currentPage,
totalPages: widget.totalPages), totalPages: widget.totalPages,
),
style: galleryFooterThemeData.titleTextStyle, style: galleryFooterThemeData.titleTextStyle,
), ),
], ],
@@ -267,6 +254,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Container( child: Container(
padding: const EdgeInsets.all(2),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
@@ -280,7 +268,6 @@ class _GalleryFooterState extends State<GalleryFooter> {
), ),
], ],
), ),
padding: const EdgeInsets.all(2),
child: UserAvatar( child: UserAvatar(
user: widget.message.user!, user: widget.message.user!,
constraints: constraints:
@@ -301,25 +288,4 @@ class _GalleryFooterState extends State<GalleryFooter> {
}, },
); );
} }
/// Sends the current message
Future sendMessage() async {
final text = _messageController.text.trim();
final attachments = widget.message.attachments;
_messageController.clear();
for (final channel in _selectedChannels) {
final message = Message(
text: text,
attachments: [attachments[widget.currentPage]],
);
await channel.sendMessage(message);
}
_selectedChannels.clear();
Navigator.pop(context);
}
} }
@@ -106,7 +106,8 @@ class DemoPainter extends CustomPainter {
final p4 = pointsList.indexOf(off4); final p4 = pointsList.indexOf(off4);
squares.add( squares.add(
Offset4(p1, p2, p3, p4, i, j, rowCount, columnCount, gradient)); Offset4(p1, p2, p3, p4, i, j, rowCount, columnCount, gradient),
);
} }
} }
@@ -123,17 +124,18 @@ class DemoPainter extends CustomPainter {
final fontSize = username.length == 2 ? textSize : textSize * 1.5; final fontSize = username.length == 2 ? textSize : textSize * 1.5;
TextPainter( TextPainter(
text: TextSpan( text: TextSpan(
text: username, text: username,
style: TextStyle( style: TextStyle(
fontFamily: fontFamily, fontFamily: fontFamily,
fontSize: fontSize, fontSize: fontSize,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.white.withOpacity(0.7), color: Colors.white.withOpacity(0.7),
),
), ),
textAlign: TextAlign.center, ),
textDirection: TextDirection.ltr) textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
)
..layout(maxWidth: size.width) ..layout(maxWidth: size.width)
..paint( ..paint(
canvas, canvas,
@@ -168,8 +170,8 @@ class DemoPainter extends CustomPainter {
final sign1 = rand.nextInt(2) == 1 ? 1 : -1; final sign1 = rand.nextInt(2) == 1 ? 1 : -1;
final sign2 = 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 dx = sign1 * 0.6 * rand.nextInt(size.width ~/ columnCount);
final dy = 0.6 * sign2 * rand.nextInt(size.height ~/ rowCount); final dy = sign2 * 0.6 * rand.nextInt(size.height ~/ rowCount);
transformedList.add(Offset(orgDx + dx, orgDy + dy)); transformedList.add(Offset(orgDx + dx, orgDy + dy));
} }
@@ -223,8 +225,12 @@ class Offset4 {
/// Draw the polygon on canvas /// Draw the polygon on canvas
void draw(Canvas canvas, List<Offset> points) { void draw(Canvas canvas, List<Offset> points) {
final paint = Paint() final paint = Paint()
..color = Color.fromARGB(255, Random().nextInt(255), ..color = Color.fromARGB(
Random().nextInt(255), Random().nextInt(255)) 255,
Random().nextInt(255),
Random().nextInt(255),
Random().nextInt(255),
)
..shader = ui.Gradient.linear( ..shader = ui.Gradient.linear(
points[p1], points[p1],
points[p3], points[p3],
@@ -300,8 +300,10 @@ abstract class Translations {
String get youText; String get youText;
/// Gallery footer pagination text /// Gallery footer pagination text
String galleryPaginationText( String galleryPaginationText({
{required int currentPage, required int totalPages}); required int currentPage,
required int totalPages,
});
/// The text shown for "File" /// The text shown for "File"
String get fileText; String get fileText;
@@ -665,8 +667,10 @@ class DefaultTranslations implements Translations {
String get youText => 'You'; String get youText => 'You';
@override @override
String galleryPaginationText( String galleryPaginationText({
{required int currentPage, required int totalPages}) => required int currentPage,
required int totalPages,
}) =>
'${currentPage + 1} of $totalPages'; '${currentPage + 1} of $totalPages';
@override @override
@@ -129,7 +129,7 @@ class _MediaListViewState extends State<MediaListView> {
), ),
), ),
), ),
] ],
], ],
), ),
), ),
@@ -144,9 +144,9 @@ class _MediaListViewState extends State<MediaListView> {
_getMedia(); _getMedia();
} }
void _getMedia() async { Future<void> _getMedia() async {
final assetList = await PhotoManager.getAssetPathList().then((value) { final assetList = await PhotoManager.getAssetPathList().then((value) {
if (value.isNotEmpty == true) { if (value.isNotEmpty) {
return value.singleWhere((element) => element.isAll); return value.singleWhere((element) => element.isAll);
} }
}); });
@@ -178,7 +178,9 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
@override @override
ImageStreamCompleter load( ImageStreamCompleter load(
MediaThumbnailProvider key, DecoderCallback decode) => MediaThumbnailProvider key,
DecoderCallback decode,
) =>
MultiFrameImageStreamCompleter( MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode), codec: _loadAsync(key, decode),
scale: 1, scale: 1,
@@ -188,7 +190,9 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
); );
Future<ui.Codec> _loadAsync( Future<ui.Codec> _loadAsync(
MediaThumbnailProvider key, DecoderCallback decode) async { MediaThumbnailProvider key,
DecoderCallback decode,
) async {
assert(key == this, 'Checks MediaThumbnailProvider'); assert(key == this, 'Checks MediaThumbnailProvider');
final bytes = await media.thumbData; final bytes = await media.thumbData;
@@ -104,7 +104,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
final size = mediaQueryData.size; final size = mediaQueryData.size;
final user = StreamChat.of(context).currentUser; final user = StreamChat.of(context).currentUser;
final roughMaxSize = 2 * size.width / 3; final roughMaxSize = size.width * 2 / 3;
var messageTextLength = widget.message.text!.length; var messageTextLength = widget.message.text!.length;
if (widget.message.quotedMessage != null) { if (widget.message.quotedMessage != null) {
var quotedMessageLength = var quotedMessageLength =
@@ -119,7 +119,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
final roughSentenceSize = messageTextLength * final roughSentenceSize = messageTextLength *
(widget.messageTheme.messageTextStyle?.fontSize ?? 1) * (widget.messageTheme.messageTextStyle?.fontSize ?? 1) *
1.2; 1.2;
final divFactor = widget.message.attachments.isNotEmpty == true final divFactor = widget.message.attachments.isNotEmpty
? 1 ? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
@@ -142,14 +142,15 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
(widget.message.status == MessageSendingStatus.sent)) (widget.message.status == MessageSendingStatus.sent))
Align( Align(
alignment: Alignment( alignment: Alignment(
user?.id == widget.message.user?.id user?.id == widget.message.user?.id
? (divFactor >= 1.0 ? (divFactor >= 1.0
? -0.2 - shiftFactor ? -0.2 - shiftFactor
: (1.2 - divFactor)) : (1.2 - divFactor))
: (divFactor >= 1.0 : (divFactor >= 1.0
? 0.2 + shiftFactor ? shiftFactor + 0.2
: -(1.2 - divFactor)), : -(1.2 - divFactor)),
0), 0,
),
child: ReactionPicker( child: ReactionPicker(
message: widget.message, message: widget.message,
), ),
@@ -194,7 +195,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
.map((action) => _buildCustomAction( .map((action) => _buildCustomAction(
context, context,
action, action,
)) )),
].insertBetween( ].insertBetween(
Container( Container(
height: 1, height: 1,
@@ -17,7 +17,7 @@ import 'package:stream_chat_flutter/src/emoji_overlay.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/media_list_view.dart'; import 'package:stream_chat_flutter/src/media_list_view.dart';
import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart';
import 'package:stream_chat_flutter/src/overlays.dart'; import 'package:stream_chat_flutter/src/multi_overlay.dart';
import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; import 'package:stream_chat_flutter/src/quoted_message_widget.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/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
@@ -373,11 +373,13 @@ class MessageInputState extends State<MessageInput> {
_parseExistingMessage(widget.editMessage ?? widget.initialMessage!); _parseExistingMessage(widget.editMessage ?? widget.initialMessage!);
} }
textEditingController.addListener(_onChangedDebounced); textEditingController.addListener(_onChangedDebounced);
_focusNode.addListener(() { _focusNode.addListener(_focusNodeListener);
if (_focusNode.hasFocus) { }
_openFilePickerSection = false;
} void _focusNodeListener() {
}); if (_focusNode.hasFocus) {
_openFilePickerSection = false;
}
} }
int _timeOut = 0; int _timeOut = 0;
@@ -536,7 +538,7 @@ class MessageInputState extends State<MessageInput> {
? null ? null
: Border.all( : Border.all(
color: _streamChatTheme.colorTheme.textHighEmphasis color: _streamChatTheme.colorTheme.textHighEmphasis
.withOpacity(.5), .withOpacity(0.5),
width: 2, width: 2,
), ),
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
@@ -640,7 +642,7 @@ class MessageInputState extends State<MessageInput> {
), ),
secondChild: widget.disableAttachments && secondChild: widget.disableAttachments &&
!widget.showCommandsButton && !widget.showCommandsButton &&
widget.actions.isNotEmpty != true !widget.actions.isNotEmpty
? const Offstage() ? const Offstage()
: Wrap( : Wrap(
children: <Widget>[ children: <Widget>[
@@ -706,7 +708,7 @@ class MessageInputState extends State<MessageInput> {
decoration: _getInputDecoration(context), decoration: _getInputDecoration(context),
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
), ),
) ),
], ],
), ),
), ),
@@ -827,6 +829,7 @@ class MessageInputState extends State<MessageInput> {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
if (value.isNotEmpty) { if (value.isNotEmpty) {
// ignore: no-empty-block
channel.keyStroke(widget.parentMessage?.id).catchError((e) {}); channel.keyStroke(widget.parentMessage?.id).catchError((e) {});
} }
@@ -1028,8 +1031,10 @@ class MessageInputState extends State<MessageInput> {
_attachments.isNotEmpty) _attachments.isNotEmpty)
? null ? null
: () { : () {
pickFile(DefaultAttachmentTypes.image, pickFile(
camera: true); DefaultAttachmentTypes.image,
camera: true,
);
}, },
), ),
IconButton( IconButton(
@@ -1042,8 +1047,10 @@ class MessageInputState extends State<MessageInput> {
_attachments.isNotEmpty) _attachments.isNotEmpty)
? null ? null
: () { : () {
pickFile(DefaultAttachmentTypes.video, pickFile(
camera: true); DefaultAttachmentTypes.video,
camera: true,
);
}, },
), ),
], ],
@@ -1195,8 +1202,9 @@ class MessageInputState extends State<MessageInput> {
textEditingController.value = TextEditingValue( textEditingController.value = TextEditingValue(
text: rejoin + text: rejoin +
textEditingController.text textEditingController.text.substring(
.substring(textEditingController.selection.start), textEditingController.selection.start,
),
selection: TextSelection.collapsed( selection: TextSelection.collapsed(
offset: rejoin.length, offset: rejoin.length,
), ),
@@ -1254,8 +1262,7 @@ class MessageInputState extends State<MessageInput> {
Widget _buildReplyToMessage() { Widget _buildReplyToMessage() {
if (!_hasQuotedMessage) return const Offstage(); if (!_hasQuotedMessage) return const Offstage();
final containsUrl = widget.quotedMessage!.attachments final containsUrl = widget.quotedMessage!.attachments
.any((element) => element.titleLink != null) == .any((element) => element.titleLink != null);
true;
return QuotedMessageWidget( return QuotedMessageWidget(
reverse: true, reverse: true,
showBorder: !containsUrl, showBorder: !containsUrl,
@@ -1360,7 +1367,7 @@ class MessageInputState extends State<MessageInput> {
setState(() => _attachments.remove(attachment.id)); setState(() => _attachments.remove(attachment.id));
}, },
fillColor: fillColor:
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(.5), _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
child: Center( child: Center(
child: StreamSvgIcon.close( child: StreamSvgIcon.close(
size: 24, size: 24,
@@ -1839,10 +1846,11 @@ class MessageInputState extends State<MessageInput> {
backgroundColor: _streamChatTheme.colorTheme.barsBg, backgroundColor: _streamChatTheme.colorTheme.barsBg,
context: context, context: context,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
topRight: Radius.circular(16), topRight: Radius.circular(16),
)), ),
),
builder: (context) => Column( builder: (context) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -1875,7 +1883,7 @@ class MessageInputState extends State<MessageInput> {
), ),
Container( Container(
color: color:
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(.08), _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.08),
height: 1, height: 1,
), ),
Row( Row(
@@ -1908,6 +1916,7 @@ class MessageInputState extends State<MessageInput> {
@override @override
void dispose() { void dispose() {
textEditingController.dispose(); textEditingController.dispose();
_focusNode.removeListener(_focusNodeListener);
_stopSlowMode(); _stopSlowMode();
_onChangedDebounced.cancel(); _onChangedDebounced.cancel();
super.dispose(); super.dispose();
@@ -2018,7 +2027,8 @@ class _PickerWidgetState extends State<_PickerWidget> {
Text( Text(
context.translations.enablePhotoAndVideoAccessMessage, context.translations.enablePhotoAndVideoAccessMessage,
style: widget.streamChatTheme.textTheme.body.copyWith( style: widget.streamChatTheme.textTheme.body.copyWith(
color: widget.streamChatTheme.colorTheme.textLowEmphasis), color: widget.streamChatTheme.colorTheme.textLowEmphasis,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
@@ -60,6 +60,7 @@ typedef OnMessageTap = void Function(Message);
typedef ReplyTapCallback = void Function(Message); typedef ReplyTapCallback = void Function(Message);
/// Class for message details /// Class for message details
// ignore: prefer-match-file-name
class MessageDetails { class MessageDetails {
/// Constructor for creating [MessageDetails] /// Constructor for creating [MessageDetails]
MessageDetails( MessageDetails(
@@ -358,8 +359,9 @@ class _MessageListViewState extends State<MessageListView> {
child: Text( child: Text(
context.translations.emptyChatMessagesText, context.translations.emptyChatMessagesText,
style: _streamTheme.textTheme.footnote.copyWith( style: _streamTheme.textTheme.footnote.copyWith(
color: _streamTheme.colorTheme.textHighEmphasis color: _streamTheme.colorTheme.textHighEmphasis
.withOpacity(.5)), .withOpacity(0.5),
),
), ),
), ),
messageListBuilder: widget.messageListBuilder ?? messageListBuilder: widget.messageListBuilder ??
@@ -371,8 +373,9 @@ class _MessageListViewState extends State<MessageListView> {
child: Text( child: Text(
context.translations.genericErrorText, context.translations.genericErrorText,
style: _streamTheme.textTheme.footnote.copyWith( style: _streamTheme.textTheme.footnote.copyWith(
color: _streamTheme.colorTheme.textHighEmphasis color: _streamTheme.colorTheme.textHighEmphasis
.withOpacity(.5)), .withOpacity(0.5),
),
), ),
), ),
); );
@@ -383,7 +386,7 @@ class _MessageListViewState extends State<MessageListView> {
if (_messageListLength != null) { if (_messageListLength != null) {
if (_bottomPaginationActive || (_inBetweenList && _upToDate)) { if (_bottomPaginationActive || (_inBetweenList && _upToDate)) {
if (_itemPositionListener.itemPositions.value.isNotEmpty == true) { if (_itemPositionListener.itemPositions.value.isNotEmpty) {
final first = _itemPositionListener.itemPositions.value.first; final first = _itemPositionListener.itemPositions.value.first;
final diff = newMessagesListLength - _messageListLength!; final diff = newMessagesListLength - _messageListLength!;
if (diff > 0) { if (diff > 0) {
@@ -674,7 +677,8 @@ class _MessageListViewState extends State<MessageListView> {
child: BetterStreamBuilder<Iterable<ItemPosition>>( child: BetterStreamBuilder<Iterable<ItemPosition>>(
initialData: _itemPositionListener.itemPositions.value, initialData: _itemPositionListener.itemPositions.value,
stream: _valueListenableToStreamAdapter( stream: _valueListenableToStreamAdapter(
_itemPositionListener.itemPositions), _itemPositionListener.itemPositions,
),
comparator: (a, b) { comparator: (a, b) {
if (a == null || b == null) { if (a == null || b == null) {
return false; return false;
@@ -984,7 +988,7 @@ class _MessageListViewState extends State<MessageListView> {
final allRead = readList.length >= (channel.memberCount ?? 0) - 1; final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
final hasFileAttachment = final hasFileAttachment =
message.attachments.any((it) => it.type == 'file') == true; message.attachments.any((it) => it.type == 'file');
final isThreadMessage = final isThreadMessage =
message.parentId != null && message.showInChannel == true; message.parentId != null && message.showInChannel == true;
@@ -1016,7 +1020,7 @@ class _MessageListViewState extends State<MessageListView> {
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false; final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
final hasUrlAttachment = final hasUrlAttachment =
message.attachments.any((it) => it.titleLink != null) == true; message.attachments.any((it) => it.titleLink != null);
final borderSide = final borderSide =
isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment) isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment)
@@ -1259,10 +1263,11 @@ class _MessageListViewState extends State<MessageListView> {
if (widget.onThreadTap != null) { if (widget.onThreadTap != null) {
_onThreadTap = (Message message) { _onThreadTap = (Message message) {
widget.onThreadTap!( widget.onThreadTap!(
message, message,
widget.threadBuilder != null widget.threadBuilder != null
? widget.threadBuilder!(context, message) ? widget.threadBuilder!(context, message)
: null); : null,
);
}; };
} else if (widget.threadBuilder != null) { } else if (widget.threadBuilder != null) {
_onThreadTap = (Message message) { _onThreadTap = (Message message) {
@@ -1271,7 +1276,8 @@ class _MessageListViewState extends State<MessageListView> {
MaterialPageRoute( MaterialPageRoute(
builder: (_) => BetterStreamBuilder<Message>( builder: (_) => BetterStreamBuilder<Message>(
stream: streamChannel!.channel.state!.messagesStream.map( stream: streamChannel!.channel.state!.messagesStream.map(
(messages) => messages.firstWhere((m) => m.id == message.id)), (messages) => messages.firstWhere((m) => m.id == message.id),
),
initialData: message, initialData: message,
builder: (_, data) => StreamChannel( builder: (_, data) => StreamChannel(
channel: streamChannel!.channel, channel: streamChannel!.channel,
@@ -1320,7 +1326,7 @@ class _LoadingIndicator extends StatelessWidget {
stream: stream, stream: stream,
initialData: false, initialData: false,
errorBuilder: (context, error) => Container( errorBuilder: (context, error) => Container(
color: streamTheme.colorTheme.accentError.withOpacity(.2), color: streamTheme.colorTheme.accentError.withOpacity(0.2),
child: Center( child: Center(
child: Text(context.translations.loadingMessagesError), child: Text(context.translations.loadingMessagesError),
), ),
@@ -46,11 +46,11 @@ class MessageReactionsModal extends StatelessWidget {
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final user = StreamChat.of(context).currentUser; final user = StreamChat.of(context).currentUser;
final roughMaxSize = 2 * size.width / 3; final roughMaxSize = size.width * 2 / 3;
var messageTextLength = message.text!.length; var messageTextLength = message.text!.length;
if (message.quotedMessage != null) { if (message.quotedMessage != null) {
var quotedMessageLength = message.quotedMessage!.text!.length + 40; var quotedMessageLength = message.quotedMessage!.text!.length + 40;
if (message.quotedMessage!.attachments.isNotEmpty == true) { if (message.quotedMessage!.attachments.isNotEmpty) {
quotedMessageLength += 40; quotedMessageLength += 40;
} }
if (quotedMessageLength > messageTextLength) { if (quotedMessageLength > messageTextLength) {
@@ -60,7 +60,7 @@ class MessageReactionsModal extends StatelessWidget {
final roughSentenceSize = messageTextLength * final roughSentenceSize = messageTextLength *
(messageTheme.messageTextStyle?.fontSize ?? 1) * (messageTheme.messageTextStyle?.fontSize ?? 1) *
1.2; 1.2;
final divFactor = message.attachments.isNotEmpty == true final divFactor = message.attachments.isNotEmpty
? 1 ? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
@@ -80,14 +80,15 @@ class MessageReactionsModal extends StatelessWidget {
(message.status == MessageSendingStatus.sent)) (message.status == MessageSendingStatus.sent))
Align( Align(
alignment: Alignment( alignment: Alignment(
user!.id == message.user!.id user!.id == message.user!.id
? (divFactor >= 1.0 ? (divFactor >= 1.0
? -0.2 - shiftFactor ? -0.2 - shiftFactor
: (1.2 - divFactor)) : (1.2 - divFactor))
: (divFactor >= 1.0 : (divFactor >= 1.0
? 0.2 + shiftFactor ? shiftFactor + 0.2
: -(1.2 - divFactor)), : -(1.2 - divFactor)),
0), 0,
),
child: ReactionPicker( child: ReactionPicker(
message: message, message: message,
), ),
@@ -102,7 +103,7 @@ class MessageReactionsModal extends StatelessWidget {
context, context,
user, user,
), ),
] ],
], ],
), ),
), ),
@@ -146,11 +146,12 @@ class MessageSearchItem extends StatelessWidget {
} }
TextSpan _getDisplayText( TextSpan _getDisplayText(
String text, String text,
List<User> mentions, List<User> mentions,
List<Attachment> attachments, List<Attachment> attachments,
TextStyle? normalTextStyle, TextStyle? normalTextStyle,
TextStyle? mentionsTextStyle) { TextStyle? mentionsTextStyle,
) {
final textList = text.split(' '); final textList = text.split(' ');
final resList = <TextSpan>[]; final resList = <TextSpan>[];
for (final e in textList) { for (final e in textList) {
@@ -217,7 +217,9 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
); );
Widget _listItemBuilder( Widget _listItemBuilder(
BuildContext context, GetMessageResponse getMessageResponse) { BuildContext context,
GetMessageResponse getMessageResponse,
) {
if (widget.itemBuilder != null) { if (widget.itemBuilder != null) {
return widget.itemBuilder!(context, getMessageResponse); return widget.itemBuilder!(context, getMessageResponse);
} }
@@ -231,33 +233,34 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
final messageSearchBloc = MessageSearchBloc.of(context); final messageSearchBloc = MessageSearchBloc.of(context);
return StreamBuilder<bool>( return StreamBuilder<bool>(
stream: messageSearchBloc.queryMessagesLoading, stream: messageSearchBloc.queryMessagesLoading,
initialData: false, initialData: false,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return Container(
color: StreamChatTheme.of(context)
.colorTheme
.accentError
.withOpacity(.2),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text(context.translations.loadingMessagesError),
),
),
);
}
return Container( return Container(
height: 100, color: StreamChatTheme.of(context)
padding: const EdgeInsets.all(32), .colorTheme
child: Center( .accentError
child: snapshot.data! .withOpacity(0.2),
? const CircularProgressIndicator() child: Padding(
: Container(), padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text(context.translations.loadingMessagesError),
),
), ),
); );
}); }
return Container(
height: 100,
padding: const EdgeInsets.all(32),
child: Center(
child: snapshot.data!
? const CircularProgressIndicator()
: Container(),
),
);
},
);
} }
Widget _buildListView(List<GetMessageResponse> data) { Widget _buildListView(List<GetMessageResponse> data) {
@@ -88,7 +88,9 @@ class MessageText extends StatelessWidget {
for (final user in message.mentionedUsers.toSet()) { for (final user in message.mentionedUsers.toSet()) {
final userName = user.name; final userName = user.name;
messageTextToRender = messageTextToRender.replaceAll( messageTextToRender = messageTextToRender.replaceAll(
'@$userName', '[@$userName](@${userName.replaceAll(' ', '')})'); '@$userName',
'[@$userName](@${userName.replaceAll(' ', '')})',
);
} }
return messageTextToRender; return messageTextToRender;
} }
@@ -575,8 +575,7 @@ class _MessageWidgetState extends State<MessageWidget>
bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed; bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed;
bool get isGiphy => bool get isGiphy =>
widget.message.attachments.any((element) => element.type == 'giphy') == widget.message.attachments.any((element) => element.type == 'giphy');
true;
bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true; bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true;
@@ -596,7 +595,7 @@ class _MessageWidgetState extends State<MessageWidget>
isDeleted; isDeleted;
@override @override
bool get wantKeepAlive => widget.message.attachments.isNotEmpty == true; bool get wantKeepAlive => widget.message.attachments.isNotEmpty;
late StreamChatThemeData _streamChatTheme; late StreamChatThemeData _streamChatTheme;
late StreamChatState _streamChat; late StreamChatState _streamChat;
@@ -674,7 +673,10 @@ class _MessageWidgetState extends State<MessageWidget>
child: PortalEntry( child: PortalEntry(
portal: Container( portal: Container(
transform: Matrix4.translationValues( transform: Matrix4.translationValues(
widget.reverse ? 12 : -12, 0, 0), widget.reverse ? 12 : -12,
0,
0,
),
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxWidth: 22 * 6.0, maxWidth: 22 * 6.0,
), ),
@@ -704,13 +706,15 @@ class _MessageWidgetState extends State<MessageWidget>
? Container( ? Container(
// ignore: lines_longer_than_80_chars // ignore: lines_longer_than_80_chars
margin: EdgeInsets.symmetric( margin: EdgeInsets.symmetric(
horizontal: horizontal:
// ignore: lines_longer_than_80_chars // ignore: lines_longer_than_80_chars
widget.showUserAvatar == widget.showUserAvatar ==
// ignore: lines_longer_than_80_chars // ignore: lines_longer_than_80_chars
DisplayWidget.gone DisplayWidget
? 0 .gone
: 4.0), ? 0
: 4.0,
),
child: DeletedMessage( child: DeletedMessage(
borderRadiusGeometry: widget borderRadiusGeometry: widget
.borderRadiusGeometry, .borderRadiusGeometry,
@@ -794,7 +798,7 @@ class _MessageWidgetState extends State<MessageWidget>
widget.message.user != null) ...[ widget.message.user != null) ...[
_buildUserAvatar(), _buildUserAvatar(),
const SizedBox(width: 4), const SizedBox(width: 4),
] ],
], ],
), ),
if (showBottomRow) if (showBottomRow)
@@ -856,7 +860,11 @@ class _MessageWidgetState extends State<MessageWidget>
: chatThemeData.ownMessageTheme, : chatThemeData.ownMessageTheme,
reverse: widget.reverse, reverse: widget.reverse,
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: 8, left: 8, top: 8, bottom: hasNonUrlAttachments ? 8 : 0), right: 8,
left: 8,
top: 8,
bottom: hasNonUrlAttachments ? 8 : 0,
),
); );
} }
@@ -1054,64 +1062,64 @@ class _MessageWidgetState extends State<MessageWidget>
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
showDialog( showDialog(
useRootNavigator: false, useRootNavigator: false,
context: context, context: context,
barrierColor: _streamChatTheme.colorTheme.overlay, barrierColor: _streamChatTheme.colorTheme.overlay,
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageActionsModal( child: MessageActionsModal(
messageWidget: widget.copyWith( messageWidget: widget.copyWith(
key: const Key('MessageWidget'), key: const Key('MessageWidget'),
message: widget.message.copyWith( message: widget.message.copyWith(
text: (widget.message.text?.length ?? 0) > 200 text: (widget.message.text?.length ?? 0) > 200
? '${widget.message.text!.substring(0, 200)}...' ? '${widget.message.text!.substring(0, 200)}...'
: widget.message.text, : widget.message.text,
), ),
showReactions: false, showReactions: false,
showUsername: false, showUsername: false,
showTimestamp: false, showTimestamp: false,
translateUserAvatar: false, translateUserAvatar: false,
showSendingIndicator: false, showSendingIndicator: false,
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
showReactionPickerIndicator: widget.showReactions && showReactionPickerIndicator: widget.showReactions &&
(widget.message.status == MessageSendingStatus.sent), (widget.message.status == MessageSendingStatus.sent),
showPinHighlight: false, showPinHighlight: false,
showUserAvatar: widget.message.user!.id == showUserAvatar:
channel.client.state.currentUser!.id widget.message.user!.id == channel.client.state.currentUser!.id
? DisplayWidget.gone ? DisplayWidget.gone
: DisplayWidget.show, : DisplayWidget.show,
), ),
onCopyTap: (message) => onCopyTap: (message) =>
Clipboard.setData(ClipboardData(text: message.text)), Clipboard.setData(ClipboardData(text: message.text)),
messageTheme: widget.messageTheme, messageTheme: widget.messageTheme,
reverse: widget.reverse, reverse: widget.reverse,
showDeleteMessage: widget.showDeleteMessage || isDeleteFailed, showDeleteMessage: widget.showDeleteMessage || isDeleteFailed,
message: widget.message, message: widget.message,
editMessageInputBuilder: widget.editMessageInputBuilder, editMessageInputBuilder: widget.editMessageInputBuilder,
onReplyTap: widget.onReplyTap, onReplyTap: widget.onReplyTap,
onThreadReplyTap: widget.onThreadTap, onThreadReplyTap: widget.onThreadTap,
showResendMessage: widget.showResendMessage && showResendMessage:
(isSendFailed || isUpdateFailed), widget.showResendMessage && (isSendFailed || isUpdateFailed),
showCopyMessage: widget.showCopyMessage && showCopyMessage: widget.showCopyMessage &&
!isFailedState && !isFailedState &&
widget.message.text?.trim().isNotEmpty == true, widget.message.text?.trim().isNotEmpty == true,
showEditMessage: widget.showEditMessage && showEditMessage: widget.showEditMessage &&
!isDeleteFailed && !isDeleteFailed &&
widget.message.attachments !widget.message.attachments
.any((element) => element.type == 'giphy') != .any((element) => element.type == 'giphy'),
true, showReactions: widget.showReactions,
showReactions: widget.showReactions, showReplyMessage: widget.showReplyMessage &&
showReplyMessage: widget.showReplyMessage && !isFailedState &&
!isFailedState && widget.onReplyTap != null,
widget.onReplyTap != null, showThreadReplyMessage: widget.showThreadReplyMessage &&
showThreadReplyMessage: widget.showThreadReplyMessage && !isFailedState &&
!isFailedState && widget.onThreadTap != null,
widget.onThreadTap != null, showFlagButton: widget.showFlagButton,
showFlagButton: widget.showFlagButton, showPinButton: widget.showPinButton,
showPinButton: widget.showPinButton, customActions: widget.customActions,
customActions: widget.customActions, ),
), ),
)); );
} }
void _showMessageReactionsModalBottomSheet(BuildContext context) { void _showMessageReactionsModalBottomSheet(BuildContext context) {
@@ -1291,8 +1299,9 @@ class _MessageWidgetState extends State<MessageWidget>
? widget.messageTheme.copyWith( ? widget.messageTheme.copyWith(
messageTextStyle: messageTextStyle:
widget.messageTheme.messageTextStyle!.copyWith( widget.messageTheme.messageTextStyle!.copyWith(
fontSize: 42, fontSize: 42,
)) ),
)
: widget.messageTheme, : widget.messageTheme,
), ),
), ),
@@ -1326,7 +1335,7 @@ class _MessageWidgetState extends State<MessageWidget>
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
), ),
) ),
], ],
), ),
); );
@@ -1432,8 +1441,12 @@ class _ThreadReplyPainter extends CustomPainter {
final path = Path() final path = Path()
..moveTo(reverse ? size.width : 0, 0) ..moveTo(reverse ? size.width : 0, 0)
..quadraticBezierTo(reverse ? size.width : 0, size.height * 0.38, ..quadraticBezierTo(
reverse ? size.width : 0, size.height * 0.50) reverse ? size.width : 0,
size.height * 0.38,
reverse ? size.width : 0,
size.height * 0.5,
)
..quadraticBezierTo( ..quadraticBezierTo(
reverse ? size.width : 0, reverse ? size.width : 0,
size.height, size.height,
@@ -2,23 +2,6 @@ import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_portal/flutter_portal.dart';
/// Class that contains the parameters for building an overlay entry
class OverlayOptions {
/// Constructs a new overlay options object
/// [visible] - the visibility of the overlay
/// [widget] - the widget to be displayed
OverlayOptions({
required this.visible,
required this.widget,
});
/// the visibility of the overlay
final bool visible;
/// the widget to be displayed
final Widget widget;
}
/// Widget that renders a single overlay widget from a list of [overlayOptions] /// Widget that renders a single overlay widget from a list of [overlayOptions]
/// It shows the first one that is visible /// It shows the first one that is visible
class MultiOverlay extends StatelessWidget { class MultiOverlay extends StatelessWidget {
@@ -61,3 +44,20 @@ class MultiOverlay extends StatelessWidget {
); );
} }
} }
/// Class that contains the parameters for building an overlay entry
class OverlayOptions {
/// Constructs a new overlay options object
/// [visible] - the visibility of the overlay
/// [widget] - the widget to be displayed
OverlayOptions({
required this.visible,
required this.widget,
});
/// the visibility of the overlay
final bool visible;
/// the widget to be displayed
final Widget widget;
}
@@ -35,6 +35,7 @@ class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
super.initState(); super.initState();
_controller = VideoPlayerController.network(widget.attachment.assetUrl!) _controller = VideoPlayerController.network(widget.attachment.assetUrl!)
..initialize().then((_) { ..initialize().then((_) {
// ignore: no-empty-block
setState(() {}); //when your thumbnail will show. setState(() {}); //when your thumbnail will show.
}); });
} }
@@ -95,10 +96,10 @@ class QuotedMessageWidget extends StatelessWidget {
/// Callback for tap on widget /// Callback for tap on widget
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
bool get _hasAttachments => message.attachments.isNotEmpty == true; bool get _hasAttachments => message.attachments.isNotEmpty;
bool get _containsLinkAttachment => bool get _containsLinkAttachment =>
message.attachments.any((element) => element.titleLink != null) == true; message.attachments.any((element) => element.titleLink != null);
bool get _containsText => message.text?.isNotEmpty == true; bool get _containsText => message.text?.isNotEmpty == true;
@@ -140,12 +141,14 @@ class QuotedMessageWidget extends StatelessWidget {
messageTheme: isOnlyEmoji && _containsText messageTheme: isOnlyEmoji && _containsText
? messageTheme.copyWith( ? messageTheme.copyWith(
messageTextStyle: messageTheme.messageTextStyle?.copyWith( messageTextStyle: messageTheme.messageTextStyle?.copyWith(
fontSize: 32, fontSize: 32,
)) ),
)
: messageTheme.copyWith( : messageTheme.copyWith(
messageTextStyle: messageTheme.messageTextStyle?.copyWith( messageTextStyle: messageTheme.messageTextStyle?.copyWith(
fontSize: 12, fontSize: 12,
)), ),
),
), ),
), ),
].insertBetween(const SizedBox(width: 8)); ].insertBetween(const SizedBox(width: 8));
@@ -186,7 +189,7 @@ class QuotedMessageWidget extends StatelessWidget {
image: DecorationImage( image: DecorationImage(
fit: BoxFit.cover, fit: BoxFit.cover,
image: CachedNetworkImageProvider( image: CachedNetworkImageProvider(
attachment.imageUrl!, attachment.thumbUrl!,
), ),
), ),
), ),
@@ -275,7 +278,8 @@ class QuotedMessageWidget extends StatelessWidget {
height: 32, height: 32,
width: 32, width: 32,
child: getFileTypeImage( child: getFileTypeImage(
attachment.extraData['mime_type'] as String?), attachment.extraData['mime_type'] as String?,
),
), ),
}; };
@@ -142,7 +142,7 @@ class ReactionBubble extends StatelessWidget {
size: 16, size: 16,
color: (!highlightOwnReactions || reaction.user?.id == userId) color: (!highlightOwnReactions || reaction.user?.id == userId)
? chatThemeData.colorTheme.accentPrimary ? chatThemeData.colorTheme.accentPrimary
: chatThemeData.colorTheme.textHighEmphasis.withOpacity(.5), : chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.5),
), ),
); );
} }
@@ -62,10 +62,11 @@ class _ReactionPickerState extends State<ReactionPicker>
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: reactionIcons children: reactionIcons
.map<Widget>((reactionIcon) { .map<Widget>((reactionIcon) {
final ownReactionIndex = widget.message.ownReactions final ownReactionIndex =
?.indexWhere( widget.message.ownReactions?.indexWhere(
(reaction) => reaction.type == reactionIcon.type) ?? (reaction) => reaction.type == reactionIcon.type,
-1; ) ??
-1;
final index = reactionIcons.indexOf(reactionIcon); final index = reactionIcons.indexOf(reactionIcon);
final child = reactionIcon.builder( final child = reactionIcon.builder(
@@ -75,7 +75,8 @@ class StreamChat extends StatefulWidget {
if (streamChatState == null) { if (streamChatState == null) {
throw Exception( throw Exception(
'You must have a StreamChat widget at the top of your widget tree'); 'You must have a StreamChat widget at the top of your widget tree',
);
} }
return streamChatState; return streamChatState;
@@ -145,7 +145,7 @@ class StreamChatThemeData {
) { ) {
final accentColor = colorTheme.accentPrimary; final accentColor = colorTheme.accentPrimary;
final iconTheme = final iconTheme =
IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5)); IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(0.5));
final channelHeaderTheme = ChannelHeaderThemeData( final channelHeaderTheme = ChannelHeaderThemeData(
avatarTheme: AvatarThemeData( avatarTheme: AvatarThemeData(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
@@ -174,7 +174,7 @@ class StreamChatThemeData {
color: const Color(0xff7A7A7A), color: const Color(0xff7A7A7A),
), ),
lastMessageAtStyle: textTheme.footnote.copyWith( lastMessageAtStyle: textTheme.footnote.copyWith(
color: colorTheme.textHighEmphasis.withOpacity(.5), color: colorTheme.textHighEmphasis.withOpacity(0.5),
), ),
indicatorIconSize: 16, indicatorIconSize: 16,
); );
@@ -280,7 +280,7 @@ class StreamChatThemeData {
return StreamSvgIcon.loveReaction( return StreamSvgIcon.loveReaction(
color: highlighted color: highlighted
? theme.colorTheme.accentPrimary ? theme.colorTheme.accentPrimary
: theme.primaryIconTheme.color!.withOpacity(.5), : theme.primaryIconTheme.color!.withOpacity(0.5),
size: size, size: size,
); );
}, },
@@ -292,7 +292,7 @@ class StreamChatThemeData {
return StreamSvgIcon.thumbsUpReaction( return StreamSvgIcon.thumbsUpReaction(
color: highlighted color: highlighted
? theme.colorTheme.accentPrimary ? theme.colorTheme.accentPrimary
: theme.primaryIconTheme.color!.withOpacity(.5), : theme.primaryIconTheme.color!.withOpacity(0.5),
size: size, size: size,
); );
}, },
@@ -304,7 +304,7 @@ class StreamChatThemeData {
return StreamSvgIcon.thumbsDownReaction( return StreamSvgIcon.thumbsDownReaction(
color: highlighted color: highlighted
? theme.colorTheme.accentPrimary ? theme.colorTheme.accentPrimary
: theme.primaryIconTheme.color!.withOpacity(.5), : theme.primaryIconTheme.color!.withOpacity(0.5),
size: size, size: size,
); );
}, },
@@ -316,7 +316,7 @@ class StreamChatThemeData {
return StreamSvgIcon.lolReaction( return StreamSvgIcon.lolReaction(
color: highlighted color: highlighted
? theme.colorTheme.accentPrimary ? theme.colorTheme.accentPrimary
: theme.primaryIconTheme.color!.withOpacity(.5), : theme.primaryIconTheme.color!.withOpacity(0.5),
size: size, size: size,
); );
}, },
@@ -328,7 +328,7 @@ class StreamChatThemeData {
return StreamSvgIcon.wutReaction( return StreamSvgIcon.wutReaction(
color: highlighted color: highlighted
? theme.colorTheme.accentPrimary ? theme.colorTheme.accentPrimary
: theme.primaryIconTheme.color!.withOpacity(.5), : theme.primaryIconTheme.color!.withOpacity(0.5),
size: size, size: size,
); );
}, },
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// A style that overrides the default appearance of various avatar widgets. /// A style that overrides the default appearance of various avatar widgets.
// ignore: prefer-match-file-name
class AvatarThemeData with Diagnosticable { class AvatarThemeData with Diagnosticable {
/// Creates an [AvatarThemeData]. /// Creates an [AvatarThemeData].
const AvatarThemeData({ const AvatarThemeData({
@@ -25,13 +25,33 @@ class ColorTheme {
stops: [0, 1], stops: [0, 1],
), ),
this.borderTop = const Effect( this.borderTop = const Effect(
sigmaX: 0, sigmaY: -1, color: Color(0xff000000), blur: 0, alpha: 0.08), sigmaX: 0,
sigmaY: -1,
color: Color(0xff000000),
blur: 0,
alpha: 0.08,
),
this.borderBottom = const Effect( this.borderBottom = const Effect(
sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0, alpha: 0.08), sigmaX: 0,
sigmaY: 1,
color: Color(0xff000000),
blur: 0,
alpha: 0.08,
),
this.shadowIconButton = const Effect( this.shadowIconButton = const Effect(
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4), sigmaX: 0,
sigmaY: 2,
color: Color(0xff000000),
alpha: 0.5,
blur: 4,
),
this.modalShadow = const Effect( this.modalShadow = const Effect(
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8), sigmaX: 0,
sigmaY: 0,
color: Color(0xff000000),
alpha: 1,
blur: 8,
),
}) : brightness = Brightness.light; }) : brightness = Brightness.light;
/// Initialise with dark theme /// Initialise with dark theme
@@ -151,11 +151,20 @@ class GalleryFooterThemeData with Diagnosticable {
bottomSheetBarrierColor: bottomSheetBarrierColor:
Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t),
bottomSheetBackgroundColor: Color.lerp( bottomSheetBackgroundColor: Color.lerp(
a.bottomSheetBackgroundColor, b.bottomSheetBackgroundColor, t), a.bottomSheetBackgroundColor,
b.bottomSheetBackgroundColor,
t,
),
bottomSheetPhotosTextStyle: TextStyle.lerp( bottomSheetPhotosTextStyle: TextStyle.lerp(
a.bottomSheetPhotosTextStyle, b.bottomSheetPhotosTextStyle, t), a.bottomSheetPhotosTextStyle,
b.bottomSheetPhotosTextStyle,
t,
),
bottomSheetCloseIconColor: Color.lerp( bottomSheetCloseIconColor: Color.lerp(
a.bottomSheetCloseIconColor, b.bottomSheetCloseIconColor, t), a.bottomSheetCloseIconColor,
b.bottomSheetCloseIconColor,
t,
),
); );
/// Merges one [GalleryFooterThemeData] with another. /// Merges one [GalleryFooterThemeData] with another.
@@ -208,10 +217,16 @@ class GalleryFooterThemeData with Diagnosticable {
..add(ColorProperty('gridIconButtonColor', gridIconButtonColor)) ..add(ColorProperty('gridIconButtonColor', gridIconButtonColor))
..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor)) ..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor))
..add(ColorProperty( ..add(ColorProperty(
'bottomSheetBackgroundColor', bottomSheetBackgroundColor)) 'bottomSheetBackgroundColor',
bottomSheetBackgroundColor,
))
..add(DiagnosticsProperty( ..add(DiagnosticsProperty(
'bottomSheetPhotosTextStyle', bottomSheetPhotosTextStyle)) 'bottomSheetPhotosTextStyle',
bottomSheetPhotosTextStyle,
))
..add(ColorProperty( ..add(ColorProperty(
'bottomSheetCloseIconColor', bottomSheetCloseIconColor)); 'bottomSheetCloseIconColor',
bottomSheetCloseIconColor,
));
} }
} }
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart';
/// Class for getting message theme /// Class for getting message theme
// ignore: prefer-match-file-name
class MessageThemeData with Diagnosticable { class MessageThemeData with Diagnosticable {
/// Creates a [MessageThemeData]. /// Creates a [MessageThemeData].
const MessageThemeData({ const MessageThemeData({
@@ -105,7 +106,10 @@ class MessageThemeData with Diagnosticable {
messageTextStyle: messageTextStyle:
TextStyle.lerp(a.messageTextStyle, b.messageTextStyle, t), TextStyle.lerp(a.messageTextStyle, b.messageTextStyle, t),
reactionsBackgroundColor: Color.lerp( reactionsBackgroundColor: Color.lerp(
a.reactionsBackgroundColor, b.reactionsBackgroundColor, t), a.reactionsBackgroundColor,
b.reactionsBackgroundColor,
t,
),
reactionsBorderColor: reactionsBorderColor:
Color.lerp(a.messageBorderColor, b.reactionsBorderColor, t), Color.lerp(a.messageBorderColor, b.reactionsBorderColor, t),
reactionsMaskColor: reactionsMaskColor:
@@ -48,7 +48,7 @@ class TypingIndicator extends StatelessWidget {
.map((e) => e.key)), .map((e) => e.key)),
builder: (context, data) => AnimatedSwitcher( builder: (context, data) => AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: data.isNotEmpty == true child: data.isNotEmpty
? Padding( ? Padding(
key: const Key('main'), key: const Key('main'),
padding: padding, padding: padding,
@@ -92,7 +92,9 @@ class UserAvatar extends StatelessWidget {
streamChatTheme streamChatTheme
.ownMessageTheme.avatarTheme?.borderRadius, .ownMessageTheme.avatarTheme?.borderRadius,
image: DecorationImage( image: DecorationImage(
image: imageProvider, fit: BoxFit.cover), image: imageProvider,
fit: BoxFit.cover,
),
), ),
), ),
) )
@@ -90,12 +90,13 @@ class UserItem extends StatelessWidget {
Widget _buildLastActive(BuildContext context) { Widget _buildLastActive(BuildContext context) {
final chatTheme = StreamChatTheme.of(context); final chatTheme = StreamChatTheme.of(context);
return Text( return Text(
user.online == true user.online
? context.translations.userOnlineText ? context.translations.userOnlineText
: '${context.translations.userLastOnlineText} ' : '${context.translations.userLastOnlineText} '
'${Jiffy(user.lastActive).fromNow()}', '${Jiffy(user.lastActive).fromNow()}',
style: chatTheme.textTheme.footnote.copyWith( style: chatTheme.textTheme.footnote.copyWith(
color: chatTheme.colorTheme.textHighEmphasis.withOpacity(.5)), color: chatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
),
); );
} }
} }
@@ -73,7 +73,7 @@ class UserListView extends StatefulWidget {
this.listBuilder, this.listBuilder,
this.userListController, this.userListController,
}) : assert( }) : assert(
crossAxisCount == 1 || groupAlphabetically == false, crossAxisCount == 1 || !groupAlphabetically,
'Cannot group alphabetically when crossAxisCount > 1', 'Cannot group alphabetically when crossAxisCount > 1',
), ),
limit = limit ?? pagination?.limit ?? 30, limit = limit ?? pagination?.limit ?? 30,
@@ -407,33 +407,34 @@ class _UserListViewState extends State<UserListView>
Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) => Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) =>
StreamBuilder<bool>( StreamBuilder<bool>(
stream: usersProvider.queryUsersLoading, stream: usersProvider.queryUsersLoading,
initialData: false, initialData: false,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return Container(
color: StreamChatTheme.of(context)
.colorTheme
.accentError
.withOpacity(.2),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text(context.translations.loadingUsersError),
),
),
);
}
return Container( return Container(
height: 100, color: StreamChatTheme.of(context)
padding: const EdgeInsets.all(32), .colorTheme
child: Center( .accentError
child: snapshot.data! .withOpacity(0.2),
? const CircularProgressIndicator() child: Padding(
: Container(), padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text(context.translations.loadingUsersError),
),
), ),
); );
}); }
return Container(
height: 100,
padding: const EdgeInsets.all(32),
child: Center(
child: snapshot.data!
? const CircularProgressIndicator()
: Container(),
),
);
},
);
Widget _separatorBuilder(context, i) => Container( Widget _separatorBuilder(context, i) => Container(
height: 1, height: 1,
@@ -1,60 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Displays a list of users who reacted
class UserReactionDisplay extends StatelessWidget {
/// Constructor for creating a [UserReactionDisplay]
const UserReactionDisplay({
Key? key,
required this.reactionToEmoji,
required this.message,
this.size = 30,
}) : super(key: key);
/// Reaction map
final Map<String, String> reactionToEmoji;
/// Message which is reacted to
final Message message;
/// Size of Icon
final double size;
@override
Widget build(BuildContext context) => Container(
color: Colors.black87,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: reactionToEmoji.keys.map((reactionType) {
final firstUserReaction = message.latestReactions!
.firstWhere((element) => element.type == reactionType,
//ignore: unnecessary_parenthesis
orElse: (() => null) as Reaction Function()?);
if (firstUserReaction.user == null) {
return IconButton(
iconSize: size,
icon: Container(),
onPressed: null,
);
}
return IconButton(
iconSize: size,
icon: UserAvatar(
user: firstUserReaction.user!,
constraints: BoxConstraints(
maxHeight: size - 5,
maxWidth: size - 5,
),
onTap: (user) {},
),
onPressed: () {},
);
}).toList(),
),
);
}
+67 -62
View File
@@ -29,78 +29,82 @@ Future<bool?> showConfirmationDialog(
}) { }) {
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
return showModalBottomSheet( return showModalBottomSheet(
useRootNavigator: false, useRootNavigator: false,
backgroundColor: chatThemeData.colorTheme.barsBg, backgroundColor: chatThemeData.colorTheme.barsBg,
context: context, context: context,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
topRight: Radius.circular(16), topRight: Radius.circular(16),
)), ),
builder: (context) { ),
final effect = chatThemeData.colorTheme.borderTop; builder: (context) {
return SafeArea( final effect = chatThemeData.colorTheme.borderTop;
child: Column( return SafeArea(
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
const SizedBox(height: 26), children: [
if (icon != null) icon, const SizedBox(height: 26),
const SizedBox(height: 26), if (icon != null) icon,
const SizedBox(height: 26),
Text(
title,
style: chatThemeData.textTheme.headlineBold,
),
const SizedBox(height: 7),
if (question != null)
Text( Text(
title, question,
style: chatThemeData.textTheme.headlineBold, textAlign: TextAlign.center,
), ),
const SizedBox(height: 7), const SizedBox(height: 36),
if (question != null) Container(
Text( color: effect.color!.withOpacity(effect.alpha ?? 1),
question, height: 1,
textAlign: TextAlign.center, ),
), Row(
const SizedBox(height: 36), children: [
Container( if (cancelText != null)
color: effect.color!.withOpacity(effect.alpha ?? 1),
height: 1,
),
Row(
children: [
if (cancelText != null)
Flexible(
child: Container(
alignment: Alignment.center,
child: TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(
cancelText,
style: chatThemeData.textTheme.bodyBold.copyWith(
color: chatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5)),
),
),
),
),
Flexible( Flexible(
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
child: TextButton( child: TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context, true); Navigator.of(context).pop(false);
}, },
child: Text( child: Text(
okText, cancelText,
style: chatThemeData.textTheme.bodyBold.copyWith( style: chatThemeData.textTheme.bodyBold.copyWith(
color: chatThemeData.colorTheme.accentError), color: chatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5),
),
), ),
), ),
), ),
), ),
], Flexible(
), child: Container(
], alignment: Alignment.center,
), child: TextButton(
); onPressed: () {
}); Navigator.pop(context, true);
},
child: Text(
okText,
style: chatThemeData.textTheme.bodyBold.copyWith(
color: chatThemeData.colorTheme.accentError,
),
),
),
),
),
],
),
],
),
);
},
);
} }
/// Shows info dialog /// Shows info dialog
@@ -119,10 +123,11 @@ Future<bool?> showInfoDialog(
theme?.colorTheme.barsBg ?? chatThemeData.colorTheme.barsBg, theme?.colorTheme.barsBg ?? chatThemeData.colorTheme.barsBg,
context: context, context: context,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
topRight: Radius.circular(16), topRight: Radius.circular(16),
)), ),
),
builder: (context) => SafeArea( builder: (context) => SafeArea(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -147,8 +152,8 @@ Future<bool?> showInfoDialog(
height: 36, height: 36,
), ),
Container( Container(
color: theme?.colorTheme.textHighEmphasis.withOpacity(.08) ?? color: theme?.colorTheme.textHighEmphasis.withOpacity(0.08) ??
chatThemeData.colorTheme.textHighEmphasis.withOpacity(.08), chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.08),
height: 1, height: 1,
), ),
Center( Center(
@@ -6,11 +6,12 @@ import 'package:video_compress/video_compress.dart';
import 'package:video_thumbnail/video_thumbnail.dart'; import 'package:video_thumbnail/video_thumbnail.dart';
/// ///
class IVideoService { // ignore: prefer-match-file-name
IVideoService._(); class _IVideoService {
_IVideoService._();
/// Singleton instance of [IVideoService] /// Singleton instance of [_IVideoService]
static final IVideoService instance = IVideoService._(); static final _IVideoService instance = _IVideoService._();
final _lock = Lock(); final _lock = Lock();
/// compress video from [path] /// compress video from [path]
@@ -64,6 +65,6 @@ class IVideoService {
); );
} }
/// Get instance of [IVideoService] /// Get instance of [_IVideoService]
// ignore: non_constant_identifier_names // ignore: non_constant_identifier_names
IVideoService get VideoService => IVideoService.instance; _IVideoService get VideoService => _IVideoService.instance;
+1 -2
View File
@@ -55,10 +55,9 @@ flutter:
uses-material-design: true uses-material-design: true
dev_dependencies: dev_dependencies:
dart_code_metrics: ^4.4.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
golden_toolkit: ^0.10.0 golden_toolkit: ^0.10.0
mocktail: ^0.1.2 mocktail: ^0.1.2
path: ^1.8.0 path: ^1.8.0
pedantic: ^1.11.0
@@ -59,7 +59,7 @@ final _channelPreviewThemeControl = ChannelPreviewThemeData(
color: const Color(0xff7A7A7A), color: const Color(0xff7A7A7A),
), ),
lastMessageAtStyle: TextTheme.light().footnote.copyWith( lastMessageAtStyle: TextTheme.light().footnote.copyWith(
color: ColorTheme.light().textHighEmphasis.withOpacity(.5), color: ColorTheme.light().textHighEmphasis.withOpacity(0.5),
), ),
indicatorIconSize: 16, indicatorIconSize: 16,
); );
@@ -83,7 +83,7 @@ final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData(
fontSize: 12, fontSize: 12,
), ),
lastMessageAtStyle: TextTheme.light().footnote.copyWith( lastMessageAtStyle: TextTheme.light().footnote.copyWith(
color: const Color(0x807f7f7f).withOpacity(.5), color: const Color(0x807f7f7f).withOpacity(0.5),
), ),
indicatorIconSize: 16, indicatorIconSize: 16,
); );
@@ -102,7 +102,7 @@ final _channelPreviewThemeControlDark = ChannelPreviewThemeData(
color: const Color(0xff7A7A7A), color: const Color(0xff7A7A7A),
), ),
lastMessageAtStyle: TextTheme.dark().footnote.copyWith( lastMessageAtStyle: TextTheme.dark().footnote.copyWith(
color: ColorTheme.dark().textHighEmphasis.withOpacity(.5), color: ColorTheme.dark().textHighEmphasis.withOpacity(0.5),
), ),
indicatorIconSize: 16, indicatorIconSize: 16,
); );
@@ -1,4 +1,4 @@
name: example name: stream_chat_flutter_core_example
description: Example app for testing stream_chat_flutter_core description: Example app for testing stream_chat_flutter_core
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
@@ -26,9 +26,7 @@ dependencies:
cupertino_icons: ^1.0.3 cupertino_icons: ^1.0.3
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat_flutter_core: stream_chat_flutter_core: ^2.2.1
path: ../
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -94,6 +94,7 @@ class _BetterStreamBuilderState<T extends Object>
if (widget.errorBuilder != null && error != _lastError) { if (widget.errorBuilder != null && error != _lastError) {
_lastError = error; _lastError = error;
if (mounted) { if (mounted) {
// ignore: no-empty-block
setState(() {}); setState(() {});
} }
} }
@@ -106,7 +107,7 @@ class _BetterStreamBuilderState<T extends Object>
if (!isEqual) { if (!isEqual) {
_lastEvent = event; _lastEvent = event;
if (mounted) { if (mounted) {
setState(() {}); setState(() {}); // ignore: no-empty-block
} }
} }
} }
@@ -111,8 +111,7 @@ class ChannelsBlocState extends State<ChannelsBloc>
_paginationEnded = false; _paginationEnded = false;
} }
if ((!clear && _paginationEnded) || if ((!clear && _paginationEnded) || _queryChannelsLoadingController.value) {
_queryChannelsLoadingController.value == true) {
return; return;
} }
@@ -221,7 +220,8 @@ class ChannelsBlocState extends State<ChannelsBloc>
.listen((e) { .listen((e) {
final channel = e.channel; final channel = e.channel;
_channelsController.add(List.from( _channelsController.add(List.from(
(channels ?? [])..removeWhere((c) => c.cid == channel?.cid))); (channels ?? [])..removeWhere((c) => c.cid == channel?.cid),
));
})); }));
} }
@@ -90,8 +90,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
_paginationEnded = false; _paginationEnded = false;
} }
if ((!clear && _paginationEnded) || if ((!clear && _paginationEnded) || _queryMessagesLoadingController.value) {
_queryMessagesLoadingController.value == true) {
return; return;
} }
@@ -88,7 +88,7 @@ class StreamChannelState extends State<StreamChannel> {
bool preferOffline = false, bool preferOffline = false,
}) async { }) async {
if (_topPaginationEnded || if (_topPaginationEnded ||
_queryTopMessagesController.value == true || _queryTopMessagesController.value ||
channel.state == null) { channel.state == null) {
return; return;
} }
@@ -120,9 +120,9 @@ class StreamChannelState extends State<StreamChannel> {
bool preferOffline = false, bool preferOffline = false,
}) async { }) async {
if (_bottomPaginationEnded || if (_bottomPaginationEnded ||
_queryBottomMessagesController.value == true || _queryBottomMessagesController.value ||
channel.state == null || channel.state == null ||
channel.state!.isUpToDate == true) return; channel.state!.isUpToDate) return;
_queryBottomMessagesController.add(true); _queryBottomMessagesController.add(true);
if (channel.state!.messages.isEmpty) { if (channel.state!.messages.isEmpty) {
@@ -164,7 +164,7 @@ class StreamChannelState extends State<StreamChannel> {
bool preferOffline = false, bool preferOffline = false,
}) async { }) async {
if (_topPaginationEnded || if (_topPaginationEnded ||
_queryTopMessagesController.value == true || _queryTopMessagesController.value ||
channel.state == null) return; channel.state == null) return;
_queryTopMessagesController.add(true); _queryTopMessagesController.add(true);
@@ -77,8 +77,7 @@ class UsersBlocState extends State<UsersBloc>
_paginationEnded = false; _paginationEnded = false;
} }
if ((!clear && _paginationEnded) || if ((!clear && _paginationEnded) || _queryUsersLoadingController.value) {
_queryUsersLoadingController.value == true) {
return; return;
} }
@@ -19,6 +19,7 @@ dependencies:
stream_chat: ^3.1.1 stream_chat: ^3.1.1
dev_dependencies: dev_dependencies:
dart_code_metrics: ^4.4.0
fake_async: ^1.2.0 fake_async: ^1.2.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@@ -1,4 +1,4 @@
name: example name: stream_chat_localizations_example
description: A new Flutter project. description: A new Flutter project.
publish_to: 'none' publish_to: 'none'
@@ -11,12 +11,8 @@ dependencies:
cupertino_icons: ^1.0.3 cupertino_icons: ^1.0.3
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat_localizations: stream_chat_flutter: ^2.2.1
path: ../ stream_chat_localizations: ^1.1.0
dependency_overrides:
stream_chat_flutter:
path: ../../stream_chat_flutter
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -32,7 +32,7 @@ const kStreamChatSupportedLanguages = {
'it', 'it',
'es', 'es',
'ja', 'ja',
'ko' 'ko',
}; };
/// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`. /// Creates a [GlobalStreamChatLocalizations] instance for the given `locale`.
@@ -350,8 +350,10 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations {
String get youText => 'You'; String get youText => 'You';
@override @override
String galleryPaginationText( String galleryPaginationText({
{required int currentPage, required int totalPages}) => required int currentPage,
required int totalPages,
}) =>
'${currentPage + 1} of $totalPages'; '${currentPage + 1} of $totalPages';
@override @override
@@ -355,8 +355,10 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations {
String get youText => 'Usted'; String get youText => 'Usted';
@override @override
String galleryPaginationText( String galleryPaginationText({
{required int currentPage, required int totalPages}) => required int currentPage,
required int totalPages,
}) =>
'${currentPage + 1} de $totalPages'; '${currentPage + 1} de $totalPages';
@override @override
@@ -354,8 +354,10 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations {
String get youText => 'Vous'; String get youText => 'Vous';
@override @override
String galleryPaginationText( String galleryPaginationText({
{required int currentPage, required int totalPages}) => required int currentPage,
required int totalPages,
}) =>
'${currentPage + 1} de $totalPages'; '${currentPage + 1} de $totalPages';
@override @override
@@ -349,8 +349,10 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations {
String get youText => 'आप'; String get youText => 'आप';
@override @override
String galleryPaginationText( String galleryPaginationText({
{required int currentPage, required int totalPages}) => required int currentPage,
required int totalPages,
}) =>
'${currentPage + 1} ऑफ़ $totalPages'; '${currentPage + 1} ऑफ़ $totalPages';
@override @override
@@ -351,8 +351,10 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.''';
String get youText => 'te'; String get youText => 'te';
@override @override
String galleryPaginationText( String galleryPaginationText({
{required int currentPage, required int totalPages}) => required int currentPage,
required int totalPages,
}) =>
'${currentPage + 1} di $totalPages'; '${currentPage + 1} di $totalPages';
@override @override
@@ -334,9 +334,12 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations {
String get youText => '당신'; String get youText => '당신';
@override @override
String galleryPaginationText( String galleryPaginationText({
{required int currentPage, required int totalPages}) => required int currentPage,
required int totalPages,
}) =>
'${currentPage + 1} / $totalPages'; '${currentPage + 1} / $totalPages';
//3 / 11 //3 / 11
@override @override
@@ -17,5 +17,6 @@ dependencies:
stream_chat_flutter: ^3.0.0 stream_chat_flutter: ^3.0.0
dev_dependencies: dev_dependencies:
dart_code_metrics: ^4.4.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@@ -1,4 +1,4 @@
name: example name: stream_chat_persistence_example
description: A new Flutter project. description: A new Flutter project.
publish_to: 'none' publish_to: 'none'
@@ -11,12 +11,8 @@ dependencies:
cupertino_icons: ^1.0.3 cupertino_icons: ^1.0.3
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat_persistence: stream_chat: ^2.2.1
path: ../ stream_chat_persistence: ^2.2.0
dependency_overrides:
stream_chat:
path: ../../stream_chat
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -62,8 +62,10 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
Future<Message?> getMessageById(String id) async => Future<Message?> getMessageById(String id) async =>
await (select(messages).join([ await (select(messages).join([
leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), leftOuterJoin(_users, messages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers, leftOuterJoin(
messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), _pinnedByUsers,
messages.pinnedByUserId.equalsExp(_pinnedByUsers.id),
),
]) ])
..where(messages.id.equals(id))) ..where(messages.id.equals(id)))
.map(_messageFromJoinRow) .map(_messageFromJoinRow)
@@ -74,8 +76,10 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
Future<List<Message>> getThreadMessages(String cid) async => Future<List<Message>> getThreadMessages(String cid) async =>
Future.wait(await (select(messages).join([ Future.wait(await (select(messages).join([
leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), leftOuterJoin(_users, messages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers, leftOuterJoin(
messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), _pinnedByUsers,
messages.pinnedByUserId.equalsExp(_pinnedByUsers.id),
),
]) ])
..where(messages.channelCid.equals(cid)) ..where(messages.channelCid.equals(cid))
..where(messages.parentId.isNotNull()) ..where(messages.parentId.isNotNull())
@@ -92,7 +96,9 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
final msgList = await Future.wait(await (select(messages).join([ final msgList = await Future.wait(await (select(messages).join([
leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), leftOuterJoin(_users, messages.userId.equalsExp(_users.id)),
leftOuterJoin( leftOuterJoin(
_pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), _pinnedByUsers,
messages.pinnedByUserId.equalsExp(_pinnedByUsers.id),
),
]) ])
..where(messages.parentId.isNotNull()) ..where(messages.parentId.isNotNull())
..where(messages.parentId.equals(parentId)) ..where(messages.parentId.equals(parentId))
@@ -134,7 +140,9 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
final msgList = await Future.wait(await (select(messages).join([ final msgList = await Future.wait(await (select(messages).join([
leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), leftOuterJoin(_users, messages.userId.equalsExp(_users.id)),
leftOuterJoin( leftOuterJoin(
_pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), _pinnedByUsers,
messages.pinnedByUserId.equalsExp(_pinnedByUsers.id),
),
]) ])
..where(messages.channelCid.equals(cid)) ..where(messages.channelCid.equals(cid))
..where( ..where(
@@ -64,8 +64,10 @@ class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
Future<Message?> getMessageById(String id) async => Future<Message?> getMessageById(String id) async =>
await (select(pinnedMessages).join([ await (select(pinnedMessages).join([
leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers, leftOuterJoin(
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), _pinnedByUsers,
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id),
),
]) ])
..where(pinnedMessages.id.equals(id))) ..where(pinnedMessages.id.equals(id)))
.map(_messageFromJoinRow) .map(_messageFromJoinRow)
@@ -76,8 +78,10 @@ class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
Future<List<Message>> getThreadMessages(String cid) async => Future<List<Message>> getThreadMessages(String cid) async =>
Future.wait(await (select(pinnedMessages).join([ Future.wait(await (select(pinnedMessages).join([
leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers, leftOuterJoin(
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), _pinnedByUsers,
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id),
),
]) ])
..where(pinnedMessages.channelCid.equals(cid)) ..where(pinnedMessages.channelCid.equals(cid))
..where(pinnedMessages.parentId.isNotNull()) ..where(pinnedMessages.parentId.isNotNull())
@@ -93,8 +97,10 @@ class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
}) async { }) async {
final msgList = await Future.wait(await (select(pinnedMessages).join([ final msgList = await Future.wait(await (select(pinnedMessages).join([
leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers, leftOuterJoin(
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), _pinnedByUsers,
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id),
),
]) ])
..where(pinnedMessages.parentId.isNotNull()) ..where(pinnedMessages.parentId.isNotNull())
..where(pinnedMessages.parentId.equals(parentId)) ..where(pinnedMessages.parentId.equals(parentId))
@@ -134,8 +140,10 @@ class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
}) async { }) async {
final msgList = await Future.wait(await (select(pinnedMessages).join([ final msgList = await Future.wait(await (select(pinnedMessages).join([
leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers, leftOuterJoin(
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), _pinnedByUsers,
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id),
),
]) ])
..where(pinnedMessages.channelCid.equals(cid)) ..where(pinnedMessages.channelCid.equals(cid))
..where(pinnedMessages.parentId.isNull() | ..where(pinnedMessages.parentId.isNull() |
@@ -10,29 +10,32 @@ export 'shared/shared_db.dart';
part 'moor_chat_database.g.dart'; part 'moor_chat_database.g.dart';
/// A chat database implemented using moor /// A chat database implemented using moor
@UseMoor(tables: [ @UseMoor(
Channels, tables: [
Messages, Channels,
PinnedMessages, Messages,
PinnedMessageReactions, PinnedMessages,
Reactions, PinnedMessageReactions,
Users, Reactions,
Members, Users,
Reads, Members,
ChannelQueries, Reads,
ConnectionEvents, ChannelQueries,
], daos: [ ConnectionEvents,
UserDao, ],
ChannelDao, daos: [
MessageDao, UserDao,
PinnedMessageDao, ChannelDao,
PinnedMessageReactionDao, MessageDao,
MemberDao, PinnedMessageDao,
ReactionDao, PinnedMessageReactionDao,
ReadDao, MemberDao,
ChannelQueryDao, ReactionDao,
ConnectionEventDao, ReadDao,
]) ChannelQueryDao,
ConnectionEventDao,
],
)
class MoorChatDatabase extends _$MoorChatDatabase { class MoorChatDatabase extends _$MoorChatDatabase {
/// Creates a new moor chat database instance /// Creates a new moor chat database instance
MoorChatDatabase( MoorChatDatabase(
@@ -229,7 +229,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
final parentId = message.parentId!; final parentId = message.parentId!;
messageByParentIdDictionary[parentId] = [ messageByParentIdDictionary[parentId] = [
...messageByParentIdDictionary[parentId] ?? [], ...messageByParentIdDictionary[parentId] ?? [],
message message,
]; ];
} }
return messageByParentIdDictionary; return messageByParentIdDictionary;
@@ -7,6 +7,7 @@ issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
environment: environment:
sdk: ">=2.12.0 <3.0.0" sdk: ">=2.12.0 <3.0.0"
flutter: ">=1.17.0"
dependencies: dependencies:
flutter: flutter:
@@ -22,8 +23,8 @@ dependencies:
dev_dependencies: dev_dependencies:
build_runner: ^2.0.1 build_runner: ^2.0.1
dart_code_metrics: ^4.4.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
mocktail: ^0.1.1 mocktail: ^0.1.1
moor_generator: ^4.2.1 moor_generator: ^4.2.1
pedantic: ^1.11.0
@@ -82,9 +82,9 @@ void main() {
cid: cids[index], cid: cids[index],
createdBy: users[index], createdBy: users[index],
config: ChannelConfig(), config: ChannelConfig(),
extraData: {'test_custom_field': 3 + index}, extraData: {'test_custom_field': index + 3},
createdAt: now, createdAt: now,
memberCount: 3 + index, memberCount: index + 3,
lastMessageAt: now.add(Duration(hours: index)), lastMessageAt: now.add(Duration(hours: index)),
), ),
).reversed.toList(growable: false); ).reversed.toList(growable: false);