diff --git a/.github/workflows/pana.yml b/.github/workflows/pana.yml index 4341e480..bef2ec03 100644 --- a/.github/workflows/pana.yml +++ b/.github/workflows/pana.yml @@ -95,7 +95,7 @@ jobs: TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} run: | PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX )) - if (( $PERCENTAGE < 90 )) + if (( $PERCENTAGE < 80 )) then echo Score too low! exit 1 diff --git a/.github/workflows/pr_title.yml b/.github/workflows/pr_title.yml index 6f4ad786..2a53261b 100644 --- a/.github/workflows/pr_title.yml +++ b/.github/workflows/pr_title.yml @@ -5,6 +5,8 @@ on: - opened - edited - synchronize + branches: + - develop jobs: main: diff --git a/.github/workflows/scripts/install-tools.sh b/.github/workflows/scripts/install-tools.sh index 1f77f519..087cfecb 100755 --- a/.github/workflows/scripts/install-tools.sh +++ b/.github/workflows/scripts/install-tools.sh @@ -1,6 +1,6 @@ #!/bin/bash -flutter pub global activate melos 0.4.0+1 +flutter pub global activate melos echo "::add-path::$HOME/.pub-cache/bin" echo "::add-path::$GITHUB_WORKSPACE/_flutter/.pub-cache/bin" echo "::add-path::$GITHUB_WORKSPACE/_flutter/bin/cache/dart-sdk/bin" \ No newline at end of file diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 25dab1f4..52a504fe 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -13,7 +13,6 @@ on: jobs: analyze: - if: github.base_ref == 'master' timeout-minutes: 15 runs-on: ubuntu-latest steps: @@ -48,7 +47,6 @@ jobs: - name: 'Install Tools' run: | ./.github/workflows/scripts/install-tools.sh - curl -sL https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar -o $HOME/google-java-format.jar - name: 'Bootstrap Workspace' run: melos bootstrap - name: 'Dart' @@ -56,20 +54,8 @@ jobs: melos exec -c 1 -- \ flutter format . ./.github/workflows/scripts/validate-formatting.sh - - name: 'Objective-C' - if: ${{ success() || failure() }} - run: | - melos exec -c 4 --ignore="*platform_interface*" --ignore="*web*" -- \ - find . -maxdepth 3 -name "*.h" -o -name "*.m" -print0 \| xargs -0 clang-format -i --style=Google --verbose - ./.github/workflows/scripts/validate-formatting.sh - - name: 'Java' - if: ${{ success() || failure() }} - run: | - melos exec -c 4 --ignore="*platform_interface*" --ignore="*web*" -- \ - find . -maxdepth 12 -name "*.java" -print0 \| xargs -0 java -jar $HOME/google-java-format.jar --replace - ./.github/workflows/scripts/validate-formatting.sh - test_dart: + test: runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -79,26 +65,35 @@ jobs: - name: 'Install Flutter' run: ./.github/workflows/scripts/install-flutter.sh stable - name: 'Install Tools' - run: ./.github/workflows/scripts/install-tools.sh - - name: 'Bootstrap Workspace' - run: melos bootstrap - - name: 'Flutter Test' - run: cd packages/stream_chat && flutter pub run test - - test_flutter: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: 'Install Flutter' - run: ./.github/workflows/scripts/install-flutter.sh stable - - name: 'Install Tools' - run: ./.github/workflows/scripts/install-tools.sh + run: | + ./.github/workflows/scripts/install-tools.sh + flutter pub global activate coverage - name: 'Bootstrap Workspace' run: melos bootstrap + - name: 'Dart Test' + run: | + cd packages/stream_chat + flutter pub run test --coverage coverage/ + format_coverage --lcov --in=coverage/ --out=lcov.info --packages=.packages --report-on=lib - name: 'Flutter Test' run: | melos exec -c 3 --flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \ - flutter test \ No newline at end of file + flutter test --coverage + - name: CodeCov + run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }} + - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + with: + path: packages/stream_chat/lcov.info + min_coverage: 50 + - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + with: + path: packages/stream_chat_persistence/coverage/lcov.info + min_coverage: 0.2 + - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + with: + path: packages/stream_chat_flutter_core/coverage/lcov.info + min_coverage: 4.5 + - uses: VeryGoodOpenSource/very_good_coverage@v1.1.1 + with: + path: packages/stream_chat_flutter/coverage/lcov.info + min_coverage: 16 diff --git a/.gitignore b/.gitignore index c75573b2..8f4fc629 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ .atom/ .idea/ .vscode/ - +**/lcov.info +coverage .packages .pub/ .dart_tool/ diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 4e30d452..8cf0fad1 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.5.0 + +- Minor fixes and improvements + ## 1.4.0-beta - Improved attachment uploading diff --git a/packages/stream_chat/analysis_options.yaml b/packages/stream_chat/analysis_options.yaml index 7e4b00fa..26001160 100644 --- a/packages/stream_chat/analysis_options.yaml +++ b/packages/stream_chat/analysis_options.yaml @@ -44,7 +44,6 @@ linter: - avoid_private_typedef_functions - avoid_redundant_argument_values - avoid_return_types_on_setters - - avoid_returning_null - avoid_returning_null_for_void - avoid_shadowing_type_parameters - avoid_single_cascade_in_expression_statements diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index 76209bfd..1701fed8 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -7,11 +7,11 @@ import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/api/retry_queue.dart'; import 'package:stream_chat/src/event_type.dart'; +import 'package:stream_chat/src/extensions/rate_limit.dart'; import 'package:stream_chat/src/models/attachment_file.dart'; import 'package:stream_chat/src/models/channel_state.dart'; import 'package:stream_chat/src/models/user.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat/src/extensions/rate_limit.dart'; /// This a the class that manages a specific channel. class Channel { @@ -1209,9 +1209,10 @@ class ChannelClientState { ChannelClientState( this._channel, ChannelState channelState, - ) : _debouncedUpdatePersistenceChannelState = _channel - ?._client?.chatPersistenceClient?.updateChannelState - ?.debounced(const Duration(seconds: 1)) { + //ignore: unnecessary_parenthesis + ) : _debouncedUpdatePersistenceChannelState = ((ChannelState state) { + _channel?._client?.chatPersistenceClient?.updateChannelState(state); + }).debounced(const Duration(seconds: 1)) { retryQueue = RetryQueue( channel: _channel, logger: Logger('RETRY QUEUE ${_channel.cid}'), @@ -1463,10 +1464,16 @@ class ChannelClientState { void addMessage(Message message) { if (message.parentId == null || message.showInChannel == true) { final newMessages = List.from(_channelState.messages); - final oldIndex = newMessages.indexWhere((m) => m.id == message.id); if (oldIndex != -1) { - newMessages[oldIndex] = message; + Message m; + if (message.quotedMessageId != null && message.quotedMessage == null) { + final oldMessage = newMessages[oldIndex]; + m = message.copyWith( + quotedMessage: oldMessage.quotedMessage, + ); + } + newMessages[oldIndex] = m ?? message; } else { newMessages.add(message); } diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client.dart index d422dba1..11619b35 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client.dart @@ -1,6 +1,7 @@ +// ignore_for_file: unnecessary_getters_setters + import 'dart:async'; import 'dart:convert'; -import 'package:stream_chat/src/extensions/map_extension.dart'; import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; @@ -16,6 +17,7 @@ import 'package:stream_chat/src/attachment_file_uploader.dart'; import 'package:stream_chat/src/db/chat_persistence_client.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/exceptions.dart'; +import 'package:stream_chat/src/extensions/map_extension.dart'; import 'package:stream_chat/src/models/attachment_file.dart'; import 'package:stream_chat/src/models/channel_model.dart'; import 'package:stream_chat/src/models/channel_state.dart'; @@ -106,14 +108,22 @@ class StreamChatClient { logger.info('instantiating new client'); } + set chatPersistenceClient(ChatPersistenceClient value) { + _originalChatPersistenceClient = value; + } + + ChatPersistenceClient _originalChatPersistenceClient; + /// Chat persistence client - ChatPersistenceClient chatPersistenceClient; + ChatPersistenceClient get chatPersistenceClient => _chatPersistenceClient; + + ChatPersistenceClient _chatPersistenceClient; /// Attachment uploader AttachmentFileUploader attachmentFileUploader; /// Whether the chat persistence is available or not - bool get persistenceEnabled => chatPersistenceClient != null; + bool get persistenceEnabled => _chatPersistenceClient != null; RetryPolicy _retryPolicy; @@ -357,7 +367,7 @@ class StreamChatClient { /// Call this function to dispose the client void dispose() async { - await chatPersistenceClient?.disconnect(); + await _chatPersistenceClient?.disconnect(); await _disconnect(); httpClient.close(); await _controller.close(); @@ -446,8 +456,8 @@ class StreamChatClient { if (!event.isLocal) { if (_synced && event.createdAt != null) { - await chatPersistenceClient?.updateConnectionInfo(event); - await chatPersistenceClient?.updateLastSyncAt(event.createdAt); + await _chatPersistenceClient?.updateConnectionInfo(event); + await _chatPersistenceClient?.updateLastSyncAt(event.createdAt); } } @@ -478,8 +488,9 @@ class StreamChatClient { _wsConnectionStatus = ConnectionStatus.connecting; - if (persistenceEnabled) { - await chatPersistenceClient.connect(state.user.id); + if (_originalChatPersistenceClient != null) { + _chatPersistenceClient = _originalChatPersistenceClient; + await _chatPersistenceClient.connect(state.user.id); } _ws = WebSocket( @@ -508,34 +519,35 @@ class StreamChatClient { ), ); - if (status == ConnectionStatus.connected && - state.channels?.isNotEmpty == true) { - // ignore: unawaited_futures - queryChannelsOnline(filter: { - 'cid': { - '\$in': state.channels.keys.toList(), - }, - }).then( - (_) async { - await resync(); - handleEvent(Event( - type: EventType.connectionRecovered, - online: true, - )); - }, - ); - } else { - _synced = false; + if (status == ConnectionStatus.connected) { + handleEvent(Event( + type: EventType.connectionRecovered, + online: true, + )); + if (state.channels?.isNotEmpty == true) { + // ignore: unawaited_futures + queryChannelsOnline(filter: { + 'cid': { + '\$in': state.channels.keys.toList(), + }, + }).then( + (_) async { + await resync(); + }, + ); + } else { + _synced = false; + } } }; _connectionStatusSubscription = _ws.connectionStatusStream.listen(_connectionStatusHandler); - var event = await chatPersistenceClient?.getConnectionInfo(); + var event = await _chatPersistenceClient?.getConnectionInfo(); await _ws.connect().then((e) async { - await chatPersistenceClient?.updateConnectionInfo(e); + await _chatPersistenceClient?.updateConnectionInfo(e); event = e; await resync(); }).catchError((err, stacktrace) { @@ -551,14 +563,14 @@ class StreamChatClient { /// Get the events missed while offline to sync the offline storage Future resync([List cids]) async { - final lastSyncAt = await chatPersistenceClient?.getLastSyncAt(); + final lastSyncAt = await _chatPersistenceClient?.getLastSyncAt(); if (lastSyncAt == null) { _synced = true; return; } - cids ??= await chatPersistenceClient?.getChannelCids(); + cids ??= await _chatPersistenceClient?.getChannelCids(); if (cids?.isEmpty == true) { return; @@ -586,7 +598,7 @@ class StreamChatClient { res.events.forEach(handleEvent); - await chatPersistenceClient?.updateLastSyncAt(DateTime.now()); + await _chatPersistenceClient?.updateLastSyncAt(DateTime.now()); _synced = true; } catch (error) { logger.severe('Error during resync $error'); @@ -723,7 +735,7 @@ class StreamChatClient { final updateData = _mapChannelStateToChannel(channels); - await chatPersistenceClient?.updateChannelQueries( + await _chatPersistenceClient?.updateChannelQueries( filter, channels.map((c) => c.channel.cid).toList(), paginationParams?.offset == null || paginationParams.offset == 0, @@ -739,7 +751,7 @@ class StreamChatClient { @required List> sort, PaginationParams paginationParams = const PaginationParams(), }) async { - final offlineChannels = await chatPersistenceClient?.getChannelStates( + final offlineChannels = await _chatPersistenceClient?.getChannelStates( filter: filter, sort: sort, paginationParams: paginationParams, @@ -960,8 +972,8 @@ class StreamChatClient { logger.info('Disconnecting flushOfflineStorage: $flushChatPersistence; ' 'clearUser: $clearUser'); - await chatPersistenceClient?.disconnect(flush: flushChatPersistence); - chatPersistenceClient = null; + await _chatPersistenceClient?.disconnect(flush: flushChatPersistence); + _chatPersistenceClient = null; _connectCompleter = null; diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index 207ba25e..c593eede 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '1.4.0-beta'; +const PACKAGE_VERSION = '1.5.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 410cc561..81aef23e 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 1.4.0-beta +version: 1.5.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index d432f441..6faf9d77 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.5.0 + +- Fixed swipeable visible on navigation back +- Fixed video upload +- `MessageInput`: added more actions locations, merge actions and add `showCommandsButton` property +- 🛑 **BREAKING** Updated AttachmentBuilder signature +- Fixed image reloading on reaction.new + ## 1.4.0-beta - Unfocus `MessageInput` only when sending commands @@ -10,7 +18,7 @@ - Added `MessageListView.onAttachmentTap` callback - Fixed message newline issue - Fixed `MessageListView` scroll keyboard behaviour -- Minor fixes and improveqments +- Minor fixes and improvements ## 1.3.2-beta diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index 0c7ac24d..8a5270df 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -35,6 +35,14 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.0 +dependency_overrides: + stream_chat: + path: ../../stream_chat + stream_chat_flutter_core: + path: ../../stream_chat_flutter_core + stream_chat_persistence: + path: ../../stream_chat_persistence + dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index 6a34e4f0..1003c767 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -13,6 +13,7 @@ import 'attachment_widget.dart'; class FileAttachment extends AttachmentWidget { final Widget title; final Widget trailing; + final VoidCallback onAttachmentTap; const FileAttachment({ Key key, @@ -21,6 +22,7 @@ class FileAttachment extends AttachmentWidget { Size size, this.title, this.trailing, + this.onAttachmentTap, }) : super(key: key, message: message, attachment: attachment, size: size); bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video'; @@ -30,45 +32,48 @@ class FileAttachment extends AttachmentWidget { @override Widget build(BuildContext context) { return Material( - child: Container( - width: size?.width ?? 100, - height: 56.0, - decoration: BoxDecoration( - color: StreamChatTheme.of(context).colorTheme.white, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + child: GestureDetector( + onTap: onAttachmentTap, + child: Container( + width: size?.width ?? 100, + height: 56.0, + decoration: BoxDecoration( + color: StreamChatTheme.of(context).colorTheme.white, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: StreamChatTheme.of(context).colorTheme.greyWhisper, + ), ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 40.0, - width: 33.33, - margin: EdgeInsets.all(8.0), - child: _getFileTypeImage(context), - ), - SizedBox(width: 8.0), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - attachment?.title ?? 'File', - style: StreamChatTheme.of(context).textTheme.bodyBold, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - SizedBox(height: 3.0), - _buildSubtitle(context), - ], + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 40.0, + width: 33.33, + margin: EdgeInsets.all(8.0), + child: _getFileTypeImage(context), ), - ), - SizedBox(width: 8.0), - _buildTrailing(context), - ], + SizedBox(width: 8.0), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + attachment?.title ?? 'File', + style: StreamChatTheme.of(context).textTheme.bodyBold, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 3.0), + _buildSubtitle(context), + ], + ), + ), + SizedBox(width: 8.0), + _buildTrailing(context), + ], + ), ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index 392ada7c..5bb810b4 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -12,6 +12,7 @@ class GiphyAttachment extends AttachmentWidget { final MessageTheme messageTheme; final ShowMessageCallback onShowMessage; final ValueChanged onReturnAction; + final VoidCallback onAttachmentTap; const GiphyAttachment({ Key key, @@ -21,6 +22,7 @@ class GiphyAttachment extends AttachmentWidget { this.messageTheme, this.onShowMessage, this.onReturnAction, + this.onAttachmentTap, }) : super(key: key, message: message, attachment: attachment, size: size); @override @@ -62,7 +64,7 @@ class GiphyAttachment extends AttachmentWidget { Padding( padding: const EdgeInsets.all(8.0), child: GestureDetector( - onTap: () => _onImageTap(context), + onTap: () => onAttachmentTap ?? _onImageTap(context), child: ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(8), diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index 98509bb9..aa8d1246 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -4,9 +4,9 @@ import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'attachment_title.dart'; import '../full_screen_media.dart'; import '../stream_chat_theme.dart'; +import 'attachment_title.dart'; import 'attachment_widget.dart'; class ImageAttachment extends AttachmentWidget { @@ -81,6 +81,7 @@ class ImageAttachment extends AttachmentWidget { return _buildImageAttachment( context, CachedNetworkImage( + cacheKey: imageUri.path, height: size?.height, width: size?.width, placeholder: (_, __) { diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 883210ba..c997d09c 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -5,10 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/message_action.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/src/reaction_picker.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/utils.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'extension.dart'; import 'message_input.dart'; @@ -34,6 +34,7 @@ class MessageActionsModal extends StatefulWidget { final ShapeBorder messageShape; final ShapeBorder attachmentShape; final DisplayWidget showUserAvatar; + final BorderRadius attachmentBorderRadiusGeometry; /// List of custom actions final List customActions; @@ -58,6 +59,7 @@ class MessageActionsModal extends StatefulWidget { this.attachmentShape, this.reverse = false, this.customActions = const [], + this.attachmentBorderRadiusGeometry, }) : super(key: key); @override @@ -153,6 +155,8 @@ class _MessageActionsModalState extends State { child: MessageWidget( key: Key('MessageWidget'), reverse: widget.reverse, + attachmentBorderRadiusGeometry: + widget.attachmentBorderRadiusGeometry, message: widget.message.copyWith( text: widget.message.text.length > 200 ? '${widget.message.text.substring(0, 200)}...' diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 21dda0f4..4a18bf02 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -12,12 +12,12 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:shimmer/shimmer.dart'; -import 'package:stream_chat_flutter/src/video_service.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/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; +import 'package:stream_chat_flutter/src/video_service.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:substring_highlight/substring_highlight.dart'; @@ -35,6 +35,8 @@ typedef AttachmentThumbnailBuilder = Widget Function( enum ActionsLocation { left, right, + leftInside, + rightInside, } enum DefaultAttachmentTypes { @@ -122,6 +124,7 @@ class MessageInput extends StatefulWidget { this.hideSendAsDm = false, this.idleSendButton, this.activeSendButton, + this.showCommandsButton = true, }) : super(key: key); /// Message to edit @@ -149,6 +152,9 @@ class MessageInput extends StatefulWidget { /// If true the attachments button will not be displayed final bool disableAttachments; + /// Use this property to hide/show the commands button + final bool showCommandsButton; + /// Hide send as dm checkbox final bool hideSendAsDm; @@ -308,7 +314,7 @@ class MessageInputState extends State { ), ), Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric(vertical: 8.0), child: _buildTextField(context), ), if (widget.parentMessage != null && !widget.hideSendAsDm) @@ -336,12 +342,11 @@ class MessageInputState extends State { direction: Axis.horizontal, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (!_commandEnabled) _buildExpandActionsButton(), - if (widget.actionsLocation == ActionsLocation.left) - ...widget.actions ?? [], + if (!_commandEnabled && widget.actionsLocation == ActionsLocation.left) + _buildExpandActionsButton(), _buildTextInput(context), - if (widget.actionsLocation == ActionsLocation.right) - ...widget.actions ?? [], + if (!_commandEnabled && widget.actionsLocation == ActionsLocation.right) + _buildExpandActionsButton(), if (widget.sendButtonLocation == SendButtonLocation.outside) _animateSendButton(context), ], @@ -421,32 +426,36 @@ class MessageInputState extends State { child: widget.activeSendButton, ) : _buildSendButton(context); - return Padding( - padding: const EdgeInsets.all(8.0), - child: AnimatedCrossFade( - crossFadeState: (_messageIsPresent || _attachments.isNotEmpty) - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: sendButton, - secondChild: widget.idleSendButton ?? _buildIdleSendButton(context), - duration: - StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration, - alignment: Alignment.center, - ), + return AnimatedCrossFade( + crossFadeState: (_messageIsPresent || _attachments.isNotEmpty) + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: sendButton, + secondChild: widget.idleSendButton ?? _buildIdleSendButton(context), + duration: + StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration, + alignment: Alignment.center, ); } Widget _buildExpandActionsButton() { return Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: AnimatedCrossFade( crossFadeState: _actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: IconButton( onPressed: () => setState(() => _actionsShrunk = false), - icon: StreamSvgIcon.emptyCircleLeft( - color: StreamChatTheme.of(context).colorTheme.accentBlue, + icon: Transform.rotate( + alignment: Alignment.center, + angle: (widget.actionsLocation == ActionsLocation.right || + widget.actionsLocation == ActionsLocation.rightInside) + ? pi + : 0, + child: StreamSvgIcon.emptyCircleLeft( + color: StreamChatTheme.of(context).colorTheme.accentBlue, + ), ), padding: const EdgeInsets.all(0), constraints: BoxConstraints.tightFor( @@ -457,10 +466,12 @@ class MessageInputState extends State { ), secondChild: FittedBox( child: Row( + crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ + children: [ if (!widget.disableAttachments) _buildAttachmentButton(), - if (widget.editMessage == null && + if (widget.showCommandsButton && + widget.editMessage == null && StreamChannel.of(context) .channel ?.config @@ -468,6 +479,7 @@ class MessageInputState extends State { ?.isNotEmpty == true) _buildCommandButton(), + ...widget.actions ?? [], ].insertBetween(const SizedBox(width: 8)), ), ), @@ -565,51 +577,75 @@ class MessageInputState extends State { ), ), contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11), - prefixIconConstraints: BoxConstraints.tight(Size(78, 24)), - suffixIconConstraints: BoxConstraints.tight(Size(40, 40)), prefixIcon: _commandEnabled - ? Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: theme.colorTheme.accentBlue, - ), - margin: const EdgeInsets.only(right: 4, left: 8), - alignment: Alignment.center, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - StreamSvgIcon.lightning( - color: Colors.white, - size: 16.0, - ), - Text( - _chosenCommand?.name?.toUpperCase() ?? '', - style: StreamChatTheme.of(context) - .textTheme - .footnoteBold - .copyWith( + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + constraints: BoxConstraints.tight(Size(64, 24)), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: theme.colorTheme.accentBlue, + ), + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + StreamSvgIcon.lightning( color: Colors.white, + size: 16.0, ), + Text( + _chosenCommand?.name?.toUpperCase() ?? '', + style: StreamChatTheme.of(context) + .textTheme + .footnoteBold + .copyWith( + color: Colors.white, + ), + ), + ], + ), ), - ], - ), + ), + ], ) - : null, + : (widget.actionsLocation == ActionsLocation.leftInside + ? Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildExpandActionsButton(), + ], + ) + : null), + suffixIconConstraints: BoxConstraints.tightFor(height: 40), + prefixIconConstraints: BoxConstraints.tightFor(height: 40), suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, children: [ if (_commandEnabled) - IconButton( - icon: StreamSvgIcon.closeSmall(), - splashRadius: 24, - padding: const EdgeInsets.all(0), - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: IconButton( + icon: StreamSvgIcon.closeSmall(), + splashRadius: 24, + padding: const EdgeInsets.all(0), + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + onPressed: () { + setState(() => _commandEnabled = false); + }, ), - onPressed: () { - setState(() => _commandEnabled = false); - }, ), + if (!_commandEnabled && + widget.actionsLocation == ActionsLocation.rightInside) + _buildExpandActionsButton(), if (widget.sendButtonLocation == SendButtonLocation.inside) _animateSendButton(context), ], @@ -619,7 +655,13 @@ class MessageInputState extends State { Timer _debounce; + String _previousValue; void _onChanged(BuildContext context, String s) { + if (s == _previousValue) { + return; + } + _previousValue = s; + if (_debounce?.isActive == true) _debounce.cancel(); _debounce = Timer( const Duration(milliseconds: 350), @@ -631,7 +673,11 @@ class MessageInputState extends State { setState(() { _messageIsPresent = s.trim().isNotEmpty; - _actionsShrunk = s.trim().isNotEmpty; + _actionsShrunk = s.trim().isNotEmpty && + ((widget.actions?.length ?? 0) + + (widget.showCommandsButton ? 1 : 0) + + (widget.disableAttachments ? 0 : 1) > + 1); }); _commandsOverlay?.remove(); @@ -1684,13 +1730,19 @@ class MessageInputState extends State { } Widget _buildCommandButton() { + final s = textEditingController.text.trim(); + return IconButton( icon: StreamSvgIcon.lightning( - color: _commandsOverlay != null - ? StreamChatTheme.of(context).messageInputTheme.actionButtonColor - : StreamChatTheme.of(context) - .messageInputTheme - .actionButtonIdleColor, + color: s.isNotEmpty + ? StreamChatTheme.of(context).colorTheme.greyGainsboro + : (_commandsOverlay != null + ? StreamChatTheme.of(context) + .messageInputTheme + .actionButtonColor + : StreamChatTheme.of(context) + .messageInputTheme + .actionButtonIdleColor), ), padding: const EdgeInsets.all(0), constraints: BoxConstraints.tightFor( @@ -1711,7 +1763,9 @@ class MessageInputState extends State { if (_commandsOverlay == null) { setState(() { _commandsOverlay = _buildCommandsOverlayEntry(); - Overlay.of(context).insert(_commandsOverlay); + if (_commandsOverlay != null) { + Overlay.of(context).insert(_commandsOverlay); + } }); } else { setState(() { @@ -1904,11 +1958,11 @@ class MessageInputState extends State { if (file == null) return; - final mimeType = file.name?.mimeType; + final mimeType = file.name?.mimeType ?? file.path.split('/').last.mimeType; final extraDataMap = {}; - if (mimeType.type == 'video' || mimeType.type == 'image') { + if (mimeType?.type == 'video' || mimeType?.type == 'image') { attachmentType = mimeType.type; } else { attachmentType = 'file'; @@ -1965,24 +2019,31 @@ class MessageInputState extends State { } Widget _buildIdleSendButton(BuildContext context) { - return StreamSvgIcon( - assetName: _getIdleSendIcon(), - color: StreamChatTheme.of(context).messageInputTheme.sendButtonIdleColor, + return Padding( + padding: const EdgeInsets.all(8.0), + child: StreamSvgIcon( + assetName: _getIdleSendIcon(), + color: + StreamChatTheme.of(context).messageInputTheme.sendButtonIdleColor, + ), ); } Widget _buildSendButton(BuildContext context) { - return IconButton( - onPressed: sendMessage, - padding: const EdgeInsets.all(0), - splashRadius: 24, - constraints: BoxConstraints.tightFor( - height: 24, - width: 24, - ), - icon: StreamSvgIcon( - assetName: _getSendIcon(), - color: StreamChatTheme.of(context).messageInputTheme.sendButtonColor, + return Padding( + padding: const EdgeInsets.all(8.0), + child: IconButton( + onPressed: sendMessage, + padding: const EdgeInsets.all(0), + splashRadius: 24, + constraints: BoxConstraints.tightFor( + height: 24, + width: 24, + ), + icon: StreamSvgIcon( + assetName: _getSendIcon(), + color: StreamChatTheme.of(context).messageInputTheme.sendButtonColor, + ), ), ); } diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index e6c1e93b..6c5199b5 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1021,15 +1021,19 @@ class _MessageListViewState extends State { !message.isSystem && !message.isEphemeral && widget.onMessageSwiped != null) { - child = Swipeable( - onSwipeEnd: () { - FocusScope.of(context).unfocus(); - widget.onMessageSwiped(message); - }, - backgroundIcon: StreamSvgIcon.reply( - color: StreamChatTheme.of(context).colorTheme.accentBlue, + child = Container( + decoration: BoxDecoration(), + clipBehavior: Clip.hardEdge, + child: Swipeable( + onSwipeEnd: () { + FocusScope.of(context).unfocus(); + widget.onMessageSwiped(message); + }, + backgroundIcon: StreamSvgIcon.reply( + color: StreamChatTheme.of(context).colorTheme.accentBlue, + ), + child: child, ), - child: child, ); } diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 1d8dbae7..c2a13ae7 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -1,16 +1,16 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/src/reaction_picker.dart'; import 'package:stream_chat_flutter/src/stream_chat.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'extension.dart'; import 'message_widget.dart'; import 'stream_chat_theme.dart'; -import 'extension.dart'; class MessageReactionsModal extends StatelessWidget { final Widget Function(BuildContext, Message) editMessageInputBuilder; @@ -23,6 +23,7 @@ class MessageReactionsModal extends StatelessWidget { final ShapeBorder messageShape; final ShapeBorder attachmentShape; final void Function(User) onUserAvatarTap; + final BorderRadius attachmentBorderRadiusGeometry; const MessageReactionsModal({ Key key, @@ -36,6 +37,7 @@ class MessageReactionsModal extends StatelessWidget { this.reverse = false, this.showUserAvatar = DisplayWidget.show, this.onUserAvatarTap, + this.attachmentBorderRadiusGeometry, }) : super(key: key); @override @@ -132,6 +134,8 @@ class MessageReactionsModal extends StatelessWidget { shape: messageShape, attachmentShape: attachmentShape, padding: const EdgeInsets.all(0), + attachmentBorderRadiusGeometry: + attachmentBorderRadiusGeometry, attachmentPadding: EdgeInsets.all( hasFileAttachment ? 4 : 2, ), diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 7f2c83eb..a0c1201a 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -20,7 +20,11 @@ import 'extension.dart'; import 'image_group.dart'; import 'message_text.dart'; -typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment); +typedef AttachmentBuilder = Widget Function( + BuildContext, + Message, + List, +); typedef OnQuotedMessageTap = void Function(String); /// The display behaviour of a widget @@ -205,63 +209,152 @@ class MessageWidget extends StatefulWidget { this.customActions = const [], this.onAttachmentTap, }) : attachmentBuilders = { - 'image': (context, message, attachment) { - return ImageAttachment( - attachment: attachment, - message: message, - messageTheme: messageTheme, - size: Size( - MediaQuery.of(context).size.width * 0.8, - MediaQuery.of(context).size.height * 0.3, + 'image': (context, message, attachments) { + var border = RoundedRectangleBorder( + side: BorderSide.none, + borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, + ); + + if (attachments.length > 1) { + return Padding( + padding: attachmentPadding, + child: wrapAttachmentWidget( + context, + Material( + color: messageTheme.messageBackgroundColor, + child: ImageGroup( + size: Size( + MediaQuery.of(context).size.width * 0.8, + MediaQuery.of(context).size.height * 0.3, + ), + images: attachments, + message: message, + messageTheme: messageTheme, + onShowMessage: onShowMessage, + ), + ), + border, + reverse, + attachmentBorderRadiusGeometry ?? BorderRadius.zero, + ), + ); + } + + return wrapAttachmentWidget( + context, + ImageAttachment( + attachment: attachments[0], + message: message, + messageTheme: messageTheme, + size: Size( + MediaQuery.of(context).size.width * 0.8, + MediaQuery.of(context).size.height * 0.3, + ), + onShowMessage: onShowMessage, + onReturnAction: onReturnAction, + onAttachmentTap: onAttachmentTap != null + ? () { + onAttachmentTap?.call(message, attachments[0]); + } + : null, ), - onShowMessage: onShowMessage, - onReturnAction: onReturnAction, - onAttachmentTap: onAttachmentTap != null - ? () { - onAttachmentTap?.call(message, attachment); - } - : null, + border, + reverse, + attachmentBorderRadiusGeometry ?? BorderRadius.zero, ); }, - 'video': (context, message, attachment) { - return VideoAttachment( - attachment: attachment, - messageTheme: messageTheme, - size: Size( - MediaQuery.of(context).size.width * 0.8, - MediaQuery.of(context).size.height * 0.3, + 'video': (context, message, attachments) { + var border = RoundedRectangleBorder( + side: BorderSide.none, + borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, + ); + + return wrapAttachmentWidget( + context, + Column( + children: attachments.map((attachment) { + return VideoAttachment( + attachment: attachment, + messageTheme: messageTheme, + size: Size( + MediaQuery.of(context).size.width * 0.8, + MediaQuery.of(context).size.height * 0.3, + ), + message: message, + onShowMessage: onShowMessage, + onReturnAction: onReturnAction, + onAttachmentTap: onAttachmentTap != null + ? () { + onAttachmentTap?.call(message, attachment); + } + : null, + ); + }).toList(), ), - message: message, - onShowMessage: onShowMessage, - onReturnAction: onReturnAction, - onAttachmentTap: onAttachmentTap != null - ? () { - onAttachmentTap?.call(message, attachment); - } - : null, + border, + reverse, + attachmentBorderRadiusGeometry ?? BorderRadius.zero, ); }, - 'giphy': (context, message, attachment) { - return GiphyAttachment( - attachment: attachment, - messageTheme: messageTheme, - message: message, - size: Size( - MediaQuery.of(context).size.width * 0.8, - MediaQuery.of(context).size.height * 0.3, + 'giphy': (context, message, attachments) { + var border = RoundedRectangleBorder( + side: BorderSide.none, + borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, + ); + + return wrapAttachmentWidget( + context, + Column( + children: attachments.map((attachment) { + return GiphyAttachment( + attachment: attachment, + messageTheme: messageTheme, + message: message, + size: Size( + MediaQuery.of(context).size.width * 0.8, + MediaQuery.of(context).size.height * 0.3, + ), + onShowMessage: onShowMessage, + onReturnAction: onReturnAction, + ); + }).toList(), ), - onShowMessage: onShowMessage, - onReturnAction: onReturnAction, + border, + reverse, + attachmentBorderRadiusGeometry ?? BorderRadius.zero, ); }, - 'file': (context, message, attachment) { - return FileAttachment( - message: message, - attachment: attachment, - size: Size( - MediaQuery.of(context).size.width * 0.8, - MediaQuery.of(context).size.height * 0.3, - ), + 'file': (context, message, attachments) { + var border = RoundedRectangleBorder( + side: attachmentBorderSide ?? + BorderSide( + color: StreamChatTheme.of(context).colorTheme.greyWhisper, + ), + borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero, + ); + + return Column( + children: attachments + .map((attachment) { + return wrapAttachmentWidget( + context, + FileAttachment( + message: message, + attachment: attachment, + size: Size( + MediaQuery.of(context).size.width * 0.8, + MediaQuery.of(context).size.height * 0.3, + ), + ), + border, + reverse, + attachmentBorderRadiusGeometry ?? BorderRadius.zero, + ); + }) + .insertBetween(SizedBox( + height: attachmentPadding.vertical / 2, + )) + .toList(), ); }, }..addAll(customAttachmentBuilders ?? {}), @@ -739,6 +832,8 @@ class _MessageWidgetState extends State return StreamChannel( channel: channel, child: MessageActionsModal( + attachmentBorderRadiusGeometry: + widget.attachmentBorderRadiusGeometry, showUserAvatar: widget.message.user.id == channel.client.state.user.id ? DisplayWidget.gone @@ -786,6 +881,8 @@ class _MessageWidgetState extends State return StreamChannel( channel: channel, child: MessageReactionsModal( + attachmentBorderRadiusGeometry: + widget.attachmentBorderRadiusGeometry, showUserAvatar: widget.message.user.id == channel.client.state.user.id ? DisplayWidget.gone @@ -830,55 +927,37 @@ class _MessageWidgetState extends State } Widget _parseAttachments() { - final images = widget.message.attachments - ?.where((element) => - element.type == 'image' && element.ogScrapeUrl == null) - ?.toList() ?? - []; + final attachmentGroups = >{}; - if (images.length > 1) { - return Padding( - padding: widget.attachmentPadding, - child: wrapAttachmentWidget( - context, - Material( - color: widget.messageTheme.messageBackgroundColor, - child: ImageGroup( - size: Size( - MediaQuery.of(context).size.width * 0.8, - MediaQuery.of(context).size.height * 0.3, - ), - images: images, - message: widget.message, - messageTheme: widget.messageTheme, - onShowMessage: widget.onShowMessage, - ), - ), - ), + widget.message.attachments + .where((element) => element.ogScrapeUrl == null) + .forEach((e) { + if (attachmentGroups[e.type] == null) { + attachmentGroups[e.type] = []; + } + + attachmentGroups[e.type].add(e); + }); + + final attachmentList = []; + + attachmentGroups.forEach((type, attachments) { + final attachmentBuilder = widget.attachmentBuilders[type]; + + if (attachmentBuilder == null) return SizedBox(); + final attachmentWidget = attachmentBuilder( + context, + widget.message, + attachments, ); - } + attachmentList.add(attachmentWidget); + }); return Padding( padding: widget.attachmentPadding, child: Column( mainAxisSize: MainAxisSize.min, - children: widget.message.attachments - ?.where((element) => element.ogScrapeUrl == null) - ?.map((attachment) { - final attachmentBuilder = - widget.attachmentBuilders[attachment.type]; - - if (attachmentBuilder == null) return SizedBox(); - final attachmentWidget = attachmentBuilder( - context, - widget.message, - attachment, - ); - return wrapAttachmentWidget( - context, - attachmentWidget, - ); - })?.insertBetween(SizedBox( + children: attachmentList?.insertBetween(SizedBox( height: widget.attachmentPadding.vertical / 2, )) ?? [], @@ -886,24 +965,6 @@ class _MessageWidgetState extends State ); } - Widget wrapAttachmentWidget( - BuildContext context, - Widget attachmentWidget, - ) { - final attachmentShape = - widget.attachmentShape ?? _getDefaultAttachmentShape(context); - return Material( - clipBehavior: Clip.antiAlias, - shape: attachmentShape, - type: MaterialType.transparency, - child: Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: attachmentWidget, - ), - ); - } - void onLongPress(BuildContext context) { if (widget.message.isEphemeral || widget.message.status == MessageSendingStatus.sending) { diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index 8d59757e..8950a77c 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -348,3 +350,25 @@ StreamSvgIcon getFileTypeImage(String type) { break; } } + +Widget wrapAttachmentWidget( + BuildContext context, + Widget attachmentWidget, + ShapeBorder attachmentShape, + bool reverse, + BorderRadius borderRadius, +) { + return ClipRRect( + borderRadius: borderRadius, + child: Material( + clipBehavior: Clip.antiAlias, + shape: attachmentShape, + type: MaterialType.transparency, + child: Transform( + transform: Matrix4.rotationY(reverse ? pi : 0), + alignment: Alignment.center, + child: attachmentWidget, + ), + ), + ); +} diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 9cc6fe50..24f83efe 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 1.4.0-beta +version: 1.5.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -11,7 +11,7 @@ environment: dependencies: flutter: sdk: flutter - stream_chat_flutter_core: ^1.4.0-beta + stream_chat_flutter_core: ^1.5.0 photo_view: ^0.11.0 rxdart: ^0.25.0 scrollable_positioned_list: ^0.1.8 diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 0bad0c4e..ac1fc93c 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.5.0 + +* Minor fixes and improvements + ## 1.4.0-beta * Added `MessageListCore.messageFilter` to filter messages locally diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 82b02b72..5a5fc7da 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 1.4.0-beta +version: 1.5.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -10,7 +10,7 @@ environment: flutter: ">=1.17.0" dependencies: - stream_chat: ^1.4.0-beta + stream_chat: ^1.5.0 flutter: sdk: flutter rxdart: ^0.25.0 diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 161f2d59..9c844ee3 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.5.0 + +* Update llc dependency +* Wait for all operations to finish before disconnecting + ## 1.4.0-beta * Update llc dependency diff --git a/packages/stream_chat_persistence/analysis_options.yaml b/packages/stream_chat_persistence/analysis_options.yaml new file mode 100644 index 00000000..26001160 --- /dev/null +++ b/packages/stream_chat_persistence/analysis_options.yaml @@ -0,0 +1,146 @@ +analyzer: + exclude: + - lib/**/*.g.dart + - lib/**/*.freezed.dart + - example/* + - test/* +linter: + rules: + - always_use_package_imports + - avoid_empty_else + - avoid_relative_lib_imports + - avoid_slow_async_io + - avoid_types_as_parameter_names + - cancel_subscriptions + - close_sinks + - control_flow_in_finally + - diagnostic_describe_all_properties + - empty_statements + - hash_and_equals + - invariant_booleans + - iterable_contains_unrelated_type + - list_remove_unrelated_type + - literal_only_boolean_expressions + - no_adjacent_strings_in_list + - no_duplicate_case_values + - no_logic_in_create_state + - prefer_void_to_null + - test_types_in_equals + - throw_in_finally + - unnecessary_statements + - unrelated_type_equality_checks + - omit_local_variable_types + - use_key_in_widget_constructors + - valid_regexps + - always_declare_return_types + - always_put_required_named_parameters_first + - always_require_non_null_named_parameters + - annotate_overrides + - avoid_bool_literals_in_conditional_expressions + - avoid_catching_errors + - avoid_init_to_null + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - await_only_futures + - camel_case_extensions + - camel_case_types + - cascade_invocations + + - constant_identifier_names + - curly_braces_in_flow_control_structures + - directives_ordering + - empty_catches + - empty_constructor_bodies + - exhaustive_cases + - file_names + - implementation_imports + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_names + - library_prefixes + - lines_longer_than_80_chars + - missing_whitespace_between_adjacent_strings + - non_constant_identifier_names + - null_closures + - one_member_abstracts + - only_throw_errors + - package_api_docs + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_equal_for_default_values + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_null_aware_operators + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + - sized_box_for_whitespace + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_unnamed_constructors_first + + - type_annotate_public_apis + - type_init_formals + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_const + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - use_is_even_rather_than_modulo + - use_late_for_private_fields_and_variables + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_to_and_as_if_applicable + - package_names + - sort_pub_dependencies + + # To be added when null-safe: + # - cast_nullable_to_non_nullable + #- unnecessary_null_checks + # - tighten_type_of_initializing_formals + # - null_check_on_nullable_type_parameter \ No newline at end of file diff --git a/packages/stream_chat_persistence/lib/src/converter/list_converter.dart b/packages/stream_chat_persistence/lib/src/converter/list_converter.dart index d14642ce..cbb8ec05 100644 --- a/packages/stream_chat_persistence/lib/src/converter/list_converter.dart +++ b/packages/stream_chat_persistence/lib/src/converter/list_converter.dart @@ -6,7 +6,7 @@ import 'package:moor/moor.dart'; /// by the sqlite backend. class ListConverter extends TypeConverter, String> { @override - List mapToDart(fromDb) { + List mapToDart(String fromDb) { if (fromDb == null) { return null; } @@ -14,7 +14,7 @@ class ListConverter extends TypeConverter, String> { } @override - String mapToSql(value) { + String mapToSql(List value) { if (value == null) { return null; } diff --git a/packages/stream_chat_persistence/lib/src/converter/map_converter.dart b/packages/stream_chat_persistence/lib/src/converter/map_converter.dart index 6e7a648d..b11eb5b8 100644 --- a/packages/stream_chat_persistence/lib/src/converter/map_converter.dart +++ b/packages/stream_chat_persistence/lib/src/converter/map_converter.dart @@ -6,7 +6,7 @@ import 'package:moor/moor.dart'; /// by the sqlite backend. class MapConverter extends TypeConverter, String> { @override - Map mapToDart(fromDb) { + Map mapToDart(String fromDb) { if (fromDb == null) { return null; } @@ -14,7 +14,7 @@ class MapConverter extends TypeConverter, String> { } @override - String mapToSql(value) { + String mapToSql(Map value) { if (value == null) { return null; } diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart index 57ec95d3..e75cead6 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart @@ -3,7 +3,7 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/channels.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; -import '../mapper/mapper.dart'; +import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'channel_dao.g.dart'; @@ -15,15 +15,14 @@ class ChannelDao extends DatabaseAccessor ChannelDao(MoorChatDatabase db) : super(db); /// Get channel by cid - Future getChannelByCid(String cid) async { - return (select(channels)..where((c) => c.cid.equals(cid))).join([ - leftOuterJoin(users, channels.createdById.equalsExp(users.id)), - ]).map((rows) { - final channel = rows.readTable(channels); - final createdBy = rows.readTable(users); - return channel.toChannelModel(createdBy: createdBy?.toUser()); - }).getSingle(); - } + Future getChannelByCid(String cid) async => + (select(channels)..where((c) => c.cid.equals(cid))).join([ + leftOuterJoin(users, channels.createdById.equalsExp(users.id)), + ]).map((rows) { + final channel = rows.readTable(channels); + final createdBy = rows.readTable(users); + return channel.toChannelModel(createdBy: createdBy?.toUser()); + }).getSingle(); /// Delete all channels by matching cid in [cids] /// @@ -31,27 +30,22 @@ class ChannelDao extends DatabaseAccessor /// 1. Channel Reads /// 2. Channel Members /// 3. Channel Messages -> Messages Reactions - Future deleteChannelByCids(List cids) async { - return (delete(channels)..where((tbl) => tbl.cid.isIn(cids))).go(); - } + Future deleteChannelByCids(List cids) async => + (delete(channels)..where((tbl) => tbl.cid.isIn(cids))).go(); /// Get the channel cids saved in the storage - Future> get cids { - return (select(channels) - ..orderBy([(c) => OrderingTerm.desc(c.lastMessageAt)]) - ..limit(250)) - .map((c) => c.cid) - .get(); - } + Future> get cids => (select(channels) + ..orderBy([(c) => OrderingTerm.desc(c.lastMessageAt)]) + ..limit(250)) + .map((c) => c.cid) + .get(); /// Updates all the channels using the new [channelList] data - Future updateChannels(List channelList) { - return batch( - (it) => it.insertAll( - channels, - channelList.map((c) => c.toEntity()).toList(), - mode: InsertMode.insertOrReplace, - ), - ); - } + Future updateChannels(List channelList) => batch( + (it) => it.insertAll( + channels, + channelList.map((c) => c.toEntity()).toList(), + mode: InsertMode.insertOrReplace, + ), + ); } diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart index 3f27cfd5..785b5946 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart @@ -6,7 +6,8 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/channel_queries.dart'; import 'package:stream_chat_persistence/src/entity/channels.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; -import '../mapper/mapper.dart'; + +import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'channel_query_dao.g.dart'; @@ -30,29 +31,31 @@ class ChannelQueryDao extends DatabaseAccessor /// the list of matching rows will be deleted Future updateChannelQueries( Map filter, - List cids, + List cids, { bool clearQueryCache, - ) async { - final hash = _computeHash(filter); - if (clearQueryCache) { - await batch((it) { - it.deleteWhere( - channelQueries, - (c) => c.queryHash.equals(hash), - ); - }); - } + }) async => + transaction(() async { + final hash = _computeHash(filter); + if (clearQueryCache) { + await batch((it) { + it.deleteWhere( + channelQueries, + (c) => c.queryHash.equals(hash), + ); + }); + } - return batch((it) { - it.insertAll( - channelQueries, - cids.map((cid) { - return ChannelQueryEntity(queryHash: hash, channelCid: cid); - }).toList(), - mode: InsertMode.insertOrReplace, - ); - }); - } + await batch((it) { + it.insertAll( + channelQueries, + cids + .map((cid) => + ChannelQueryEntity(queryHash: hash, channelCid: cid)) + .toList(), + mode: InsertMode.insertOrReplace, + ); + }); + }); /// Get list of channels by filter, sort and paginationParams Future> getChannels({ @@ -67,7 +70,7 @@ class ChannelQueryDao extends DatabaseAccessor ); } return true; - }()); + }(), ''); final hash = _computeHash(filter); final cachedChannelCids = await (select(channelQueries) @@ -86,10 +89,11 @@ class ChannelQueryDao extends DatabaseAccessor })).get(); final possibleSortingFields = cachedChannels.fold>( - ChannelModel.topLevelFields, (previousValue, element) { - return {...previousValue, ...element.extraData.keys}.toList(); - }); + ChannelModel.topLevelFields, + (previousValue, element) => + {...previousValue, ...element.extraData.keys}.toList()); + // ignore: parameter_assignments sort = sort ?.where((s) => possibleSortingFields.contains(s.field)) ?.toList(growable: false); @@ -117,7 +121,7 @@ class ChannelQueryDao extends DatabaseAccessor cachedChannels.sort(chainedComparator); - if (paginationParams?.offset != null) { + if (paginationParams?.offset != null && cachedChannels.isNotEmpty) { cachedChannels.removeRange(0, paginationParams.offset); } diff --git a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart index 8d2033b2..94b0d333 100644 --- a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart @@ -3,7 +3,8 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/connection_events.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; -import '../mapper/mapper.dart'; + +import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'connection_event_dao.g.dart'; @@ -15,40 +16,38 @@ class ConnectionEventDao extends DatabaseAccessor ConnectionEventDao(MoorChatDatabase db) : super(db); /// Get the latest stored connection event - Future get connectionEvent { - return select(connectionEvents).map((eventEntity) { - return eventEntity.toEvent(); - }).getSingle(); - } + Future get connectionEvent => select(connectionEvents) + .map((eventEntity) => eventEntity.toEvent()) + .getSingle(); /// Get the latest stored lastSyncAt - Future get lastSyncAt { - return select(connectionEvents).getSingle().then((r) => r?.lastSyncAt); - } + Future get lastSyncAt => + select(connectionEvents).getSingle().then((r) => r?.lastSyncAt); /// Update stored connection event with latest data - Future updateConnectionEvent(Event event) async { - final connectionInfo = await select(connectionEvents).getSingle(); - return into(connectionEvents).insert( - ConnectionEventEntity( - id: 1, - lastSyncAt: connectionInfo?.lastSyncAt, - lastEventAt: event.createdAt ?? connectionInfo?.lastEventAt, - totalUnreadCount: - event.totalUnreadCount ?? connectionInfo?.totalUnreadCount, - ownUser: event.me?.toJson() ?? connectionInfo?.ownUser, - unreadChannels: event.unreadChannels ?? connectionInfo?.unreadChannels, - ), - mode: InsertMode.insertOrReplace, - ); - } + Future updateConnectionEvent(Event event) async => + transaction(() async { + final connectionInfo = await select(connectionEvents).getSingle(); + await into(connectionEvents).insert( + ConnectionEventEntity( + id: 1, + lastSyncAt: connectionInfo?.lastSyncAt, + lastEventAt: event.createdAt ?? connectionInfo?.lastEventAt, + totalUnreadCount: + event.totalUnreadCount ?? connectionInfo?.totalUnreadCount, + ownUser: event.me?.toJson() ?? connectionInfo?.ownUser, + unreadChannels: + event.unreadChannels ?? connectionInfo?.unreadChannels, + ), + mode: InsertMode.insertOrReplace, + ); + }); /// Update stored lastSyncAt with latest data - Future updateLastSyncAt(DateTime lastSyncAt) async { - return (update(connectionEvents)..where((tbl) => tbl.id.equals(1))).write( - ConnectionEventsCompanion( - lastSyncAt: Value(lastSyncAt), - ), - ); - } + Future updateLastSyncAt(DateTime lastSyncAt) async => + (update(connectionEvents)..where((tbl) => tbl.id.equals(1))).write( + ConnectionEventsCompanion( + lastSyncAt: Value(lastSyncAt), + ), + ); } diff --git a/packages/stream_chat_persistence/lib/src/dao/dao.dart b/packages/stream_chat_persistence/lib/src/dao/dao.dart index 31953630..53c2f372 100644 --- a/packages/stream_chat_persistence/lib/src/dao/dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/dao.dart @@ -1,9 +1,9 @@ -export 'user_dao.dart'; export 'channel_dao.dart'; +export 'channel_query_dao.dart'; +export 'connection_event_dao.dart'; +export 'member_dao.dart'; export 'message_dao.dart'; export 'pinned_message_dao.dart'; -export 'member_dao.dart'; -export 'connection_event_dao.dart'; export 'reaction_dao.dart'; export 'read_dao.dart'; -export 'channel_query_dao.dart'; +export 'user_dao.dart'; diff --git a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart index ed30a931..e6c03996 100644 --- a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart @@ -5,7 +5,7 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/members.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; -import '../mapper/mapper.dart'; +import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'member_dao.g.dart'; @@ -17,37 +17,33 @@ class MemberDao extends DatabaseAccessor MemberDao(MoorChatDatabase db) : super(db); /// Get all members where [Members.channelCid] matches [cid] - Future> getMembersByCid(String cid) async { - return (select(members).join([ - leftOuterJoin(users, members.userId.equalsExp(users.id)), - ]) - ..where(members.channelCid.equals(cid)) - ..orderBy([OrderingTerm.asc(members.createdAt)])) - .map((row) { - final userEntity = row.readTable(users); - final memberEntity = row.readTable(members); - return memberEntity.toMember(user: userEntity?.toUser()); - }).get(); - } + Future> getMembersByCid(String cid) async => + (select(members).join([ + leftOuterJoin(users, members.userId.equalsExp(users.id)), + ]) + ..where(members.channelCid.equals(cid)) + ..orderBy([OrderingTerm.asc(members.createdAt)])) + .map((row) { + final userEntity = row.readTable(users); + final memberEntity = row.readTable(members); + return memberEntity.toMember(user: userEntity?.toUser()); + }).get(); /// Updates all the members using the new [memberList] data - Future updateMembers(String cid, List memberList) async { - return batch( - (it) => it.insertAll( - members, - memberList.map((m) => m.toEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, - ), - ); - } + Future updateMembers(String cid, List memberList) async => + batch( + (it) => it.insertAll( + members, + memberList.map((m) => m.toEntity(cid: cid)).toList(), + mode: InsertMode.insertOrReplace, + ), + ); /// Deletes all the members whose [Members.channelCid] is present in [cids] - Future deleteMemberByCids(List cids) async { - return batch((it) { - it.deleteWhere( - members, - (m) => m.channelCid.isIn(cids), - ); - }); - } + Future deleteMemberByCids(List cids) async => batch((it) { + it.deleteWhere( + members, + (m) => m.channelCid.isIn(cids), + ); + }); } diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart index 01e5c889..0be4446b 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -4,7 +4,7 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/messages.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; -import '../mapper/mapper.dart'; +import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'message_dao.g.dart'; @@ -25,17 +25,15 @@ class MessageDao extends DatabaseAccessor /// /// This will automatically delete the following linked records /// 1. Message Reactions - Future deleteMessageByIds(List messageIds) { - return (delete(messages)..where((tbl) => tbl.id.isIn(messageIds))).go(); - } + Future deleteMessageByIds(List messageIds) => + (delete(messages)..where((tbl) => tbl.id.isIn(messageIds))).go(); /// Removes all the messages by matching [Messages.channelCid] in [cids] /// /// This will automatically delete the following linked records /// 1. Message Reactions - Future deleteMessageByCids(List cids) async { - return (delete(messages)..where((tbl) => tbl.channelCid.isIn(cids))).go(); - } + Future deleteMessageByCids(List cids) async => + (delete(messages)..where((tbl) => tbl.channelCid.isIn(cids))).go(); Future _messageFromJoinRow(TypedResult rows) async { final userEntity = rows.readTable(_users); @@ -60,31 +58,29 @@ class MessageDao extends DatabaseAccessor } /// Returns a single message by matching the [Messages.id] with [id] - Future getMessageById(String id) async { - return await (select(messages).join([ - leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), - leftOuterJoin( - _pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), - ]) - ..where(messages.id.equals(id))) - .map(_messageFromJoinRow) - .getSingle(); - } + Future getMessageById(String id) async => + await (select(messages).join([ + leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), + leftOuterJoin(_pinnedByUsers, + messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), + ]) + ..where(messages.id.equals(id))) + .map(_messageFromJoinRow) + .getSingle(); /// Returns all the messages of a particular thread by matching /// [Messages.channelCid] with [cid] - Future> getThreadMessages(String cid) async { - return Future.wait(await (select(messages).join([ - leftOuterJoin(users, messages.userId.equalsExp(_users.id)), - leftOuterJoin( - _pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), - ]) - ..where(messages.channelCid.equals(cid)) - ..where(isNotNull(messages.parentId)) - ..orderBy([OrderingTerm.asc(messages.createdAt)])) - .map(_messageFromJoinRow) - .get()); - } + Future> getThreadMessages(String cid) async => + Future.wait(await (select(messages).join([ + leftOuterJoin(users, messages.userId.equalsExp(_users.id)), + leftOuterJoin(_pinnedByUsers, + messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), + ]) + ..where(messages.channelCid.equals(cid)) + ..where(isNotNull(messages.parentId)) + ..orderBy([OrderingTerm.asc(messages.createdAt)])) + .map(_messageFromJoinRow) + .get()); /// Returns all the messages of a particular thread by matching /// [Messages.parentId] with [parentId] @@ -102,7 +98,7 @@ class MessageDao extends DatabaseAccessor .map(_messageFromJoinRow) .get()); - if (options?.lessThan != null) { + if (options?.lessThan != null && msgList.isNotEmpty) { final lessThanIndex = msgList.indexWhere((m) => m.id == options.lessThan); msgList.removeRange(lessThanIndex, msgList.length); } diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart index edf8e438..5e0ae721 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart @@ -4,7 +4,7 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/pinned_messages.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; -import '../mapper/mapper.dart'; +import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'pinned_message_dao.g.dart'; @@ -25,19 +25,15 @@ class PinnedMessageDao extends DatabaseAccessor /// /// This will automatically delete the following linked records /// 1. Message Reactions - Future deleteMessageByIds(List messageIds) { - return (delete(pinnedMessages)..where((tbl) => tbl.id.isIn(messageIds))) - .go(); - } + Future deleteMessageByIds(List messageIds) => + (delete(pinnedMessages)..where((tbl) => tbl.id.isIn(messageIds))).go(); /// Removes all the messages by matching [PinnedMessages.channelCid] in [cids] /// /// This will automatically delete the following linked records /// 1. Message Reactions - Future deleteMessageByCids(List cids) async { - return (delete(pinnedMessages)..where((tbl) => tbl.channelCid.isIn(cids))) - .go(); - } + Future deleteMessageByCids(List cids) async => + (delete(pinnedMessages)..where((tbl) => tbl.channelCid.isIn(cids))).go(); Future _messageFromJoinRow(TypedResult rows) async { final userEntity = rows.readTable(users); @@ -62,31 +58,29 @@ class PinnedMessageDao extends DatabaseAccessor } /// Returns a single message by matching the [PinnedMessages.id] with [id] - Future getMessageById(String id) async { - return await (select(pinnedMessages).join([ - leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), - leftOuterJoin(_pinnedByUsers, - pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), - ]) - ..where(pinnedMessages.id.equals(id))) - .map(_messageFromJoinRow) - .getSingle(); - } + Future getMessageById(String id) async => + await (select(pinnedMessages).join([ + leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), + leftOuterJoin(_pinnedByUsers, + pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), + ]) + ..where(pinnedMessages.id.equals(id))) + .map(_messageFromJoinRow) + .getSingle(); /// Returns all the messages of a particular thread by matching /// [PinnedMessages.channelCid] with [cid] - Future> getThreadMessages(String cid) async { - return Future.wait(await (select(pinnedMessages).join([ - leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), - leftOuterJoin(_pinnedByUsers, - pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), - ]) - ..where(pinnedMessages.channelCid.equals(cid)) - ..where(isNotNull(pinnedMessages.parentId)) - ..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)])) - .map(_messageFromJoinRow) - .get()); - } + Future> getThreadMessages(String cid) async => + Future.wait(await (select(pinnedMessages).join([ + leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), + leftOuterJoin(_pinnedByUsers, + pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), + ]) + ..where(pinnedMessages.channelCid.equals(cid)) + ..where(isNotNull(pinnedMessages.parentId)) + ..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)])) + .map(_messageFromJoinRow) + .get()); /// Returns all the messages of a particular thread by matching /// [PinnedMessages.parentId] with [parentId] diff --git a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart index 99053fca..ef06326a 100644 --- a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart @@ -3,7 +3,7 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/reactions.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; -import '../mapper/mapper.dart'; +import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'reaction_dao.g.dart'; @@ -16,18 +16,17 @@ class ReactionDao extends DatabaseAccessor /// Returns all the reactions of a particular message by matching /// [Reactions.messageId] with [messageId] - Future> getReactions(String messageId) { - return (select(reactions).join([ - leftOuterJoin(users, reactions.userId.equalsExp(users.id)), - ]) - ..where(reactions.messageId.equals(messageId)) - ..orderBy([OrderingTerm.asc(reactions.createdAt)])) - .map((rows) { - final userEntity = rows.readTable(users); - final reactionEntity = rows.readTable(reactions); - return reactionEntity.toReaction(user: userEntity?.toUser()); - }).get(); - } + Future> getReactions(String messageId) => + (select(reactions).join([ + leftOuterJoin(users, reactions.userId.equalsExp(users.id)), + ]) + ..where(reactions.messageId.equals(messageId)) + ..orderBy([OrderingTerm.asc(reactions.createdAt)])) + .map((rows) { + final userEntity = rows.readTable(users); + final reactionEntity = rows.readTable(reactions); + return reactionEntity.toReaction(user: userEntity?.toUser()); + }).get(); /// Returns all the reactions of a particular message /// added by a particular user by matching @@ -42,23 +41,21 @@ class ReactionDao extends DatabaseAccessor } /// Updates the reactions data with the new [reactionList] data - Future updateReactions(List reactionList) { - return batch((it) { - it.insertAll( - reactions, - reactionList.map((r) => r.toEntity()).toList(), - mode: InsertMode.insertOrReplace, - ); - }); - } + Future updateReactions(List reactionList) => batch((it) { + it.insertAll( + reactions, + reactionList.map((r) => r.toEntity()).toList(), + mode: InsertMode.insertOrReplace, + ); + }); - /// Deletes all the reactions whose [Reactions.messageId] is present in [messageIds] - Future deleteReactionsByMessageIds(List messageIds) { - return batch((it) { - it.deleteWhere( - reactions, - (r) => r.messageId.isIn(messageIds), - ); - }); - } + /// Deletes all the reactions whose [Reactions.messageId] is + /// present in [messageIds] + Future deleteReactionsByMessageIds(List messageIds) => + batch((it) { + it.deleteWhere( + reactions, + (r) => r.messageId.isIn(messageIds), + ); + }); } diff --git a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart index c78cab07..95e23d8b 100644 --- a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart @@ -3,7 +3,7 @@ import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/reads.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; -import '../mapper/mapper.dart'; +import 'package:stream_chat_persistence/src/mapper/mapper.dart'; part 'read_dao.g.dart'; @@ -14,30 +14,26 @@ class ReadDao extends DatabaseAccessor with _$ReadDaoMixin { ReadDao(MoorChatDatabase db) : super(db); /// Get all reads where [Reads.channelCid] matches [cid] - Future> getReadsByCid(String cid) async { - return (select(reads).join([ - leftOuterJoin(users, reads.userId.equalsExp(users.id)), - ]) - ..where(reads.channelCid.equals(cid)) - ..orderBy([ - OrderingTerm.asc(reads.lastRead), - ])) - .map((row) { - final userEntity = row.readTable(users); - final readEntity = row.readTable(reads); - return readEntity.toRead(user: userEntity?.toUser()); - }).get(); - } + Future> getReadsByCid(String cid) async => (select(reads).join([ + leftOuterJoin(users, reads.userId.equalsExp(users.id)), + ]) + ..where(reads.channelCid.equals(cid)) + ..orderBy([ + OrderingTerm.asc(reads.lastRead), + ])) + .map((row) { + final userEntity = row.readTable(users); + final readEntity = row.readTable(reads); + return readEntity.toRead(user: userEntity?.toUser()); + }).get(); /// Updates the read data of a particular channel with /// the new [readList] data - Future updateReads(String cid, List readList) { - return batch( - (it) => it.insertAll( - reads, - readList.map((r) => r.toEntity(cid: cid)).toList(), - mode: InsertMode.insertOrReplace, - ), - ); - } + Future updateReads(String cid, List readList) => batch( + (it) => it.insertAll( + reads, + readList.map((r) => r.toEntity(cid: cid)).toList(), + mode: InsertMode.insertOrReplace, + ), + ); } diff --git a/packages/stream_chat_persistence/lib/src/dao/user_dao.dart b/packages/stream_chat_persistence/lib/src/dao/user_dao.dart index 795c7968..65ce1521 100644 --- a/packages/stream_chat_persistence/lib/src/dao/user_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/user_dao.dart @@ -2,7 +2,7 @@ import 'package:moor/moor.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/entity/users.dart'; -import '../mapper/user_mapper.dart'; +import 'package:stream_chat_persistence/src/mapper/user_mapper.dart'; part 'user_dao.g.dart'; @@ -13,13 +13,11 @@ class UserDao extends DatabaseAccessor with _$UserDaoMixin { UserDao(MoorChatDatabase db) : super(db); /// Updates the users data with the new [userList] data - Future updateUsers(List userList) { - return batch( - (it) => it.insertAll( - users, - userList.map((u) => u.toEntity()).toList(), - mode: InsertMode.insertOrReplace, - ), - ); - } + Future updateUsers(List userList) => batch( + (it) => it.insertAll( + users, + userList.map((u) => u.toEntity()).toList(), + mode: InsertMode.insertOrReplace, + ), + ); } diff --git a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart index eae1abc6..65686cb1 100644 --- a/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/moor_chat_database.dart @@ -1,25 +1,22 @@ -import 'package:moor/isolate.dart'; import 'package:moor/moor.dart'; import 'package:stream_chat/stream_chat.dart'; - -import '../entity/entity.dart'; -import '../dao/dao.dart'; -import '../converter/converter.dart'; -import 'shared/shared_db.dart'; +import 'package:stream_chat_persistence/src/converter/converter.dart'; +import 'package:stream_chat_persistence/src/dao/dao.dart'; +import 'package:stream_chat_persistence/src/db/shared/shared_db.dart'; +import 'package:stream_chat_persistence/src/entity/entity.dart'; part 'moor_chat_database.g.dart'; LazyDatabase _openConnection( String userId, { - logStatements = false, -}) { - return LazyDatabase(() async { - return await SharedDB.constructDatabase( - userId, - logStatements: logStatements, - ); - }); -} + bool logStatements = false, + bool persistOnDisk = true, +}) => + LazyDatabase(() async => SharedDB.constructDatabase( + userId, + logStatements: logStatements, + persistOnDisk: persistOnDisk, + )); /// A chat database implemented using moor @UseMoor(tables: [ @@ -48,15 +45,16 @@ class MoorChatDatabase extends _$MoorChatDatabase { MoorChatDatabase( this._userId, { logStatements = false, + bool persistOnDisk = true, }) : super(_openConnection( _userId, logStatements: logStatements, + persistOnDisk: persistOnDisk, )); /// Instantiate a new database instance MoorChatDatabase.connect( this._userId, - this._isolate, DatabaseConnection connection, ) : super.connect(connection); @@ -65,8 +63,6 @@ class MoorChatDatabase extends _$MoorChatDatabase { /// User id to which the database is connected String get userId => _userId; - MoorIsolate _isolate; - // you should bump this number whenever you change or add a table definition. @override int get schemaVersion => 2; @@ -85,8 +81,5 @@ class MoorChatDatabase extends _$MoorChatDatabase { ); /// Closes the database instance - Future disconnect() async { - await _isolate?.shutdownAll(); - await close(); - } + Future disconnect() => close(); } diff --git a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart index 74cf91f8..7592247e 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'dart:isolate'; + import 'package:moor/ffi.dart'; import 'package:moor/isolate.dart'; import 'package:moor/moor.dart'; @@ -8,7 +9,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; import 'package:stream_chat_persistence/stream_chat_persistence.dart'; -import '../moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; /// A Helper class to construct new instances of [MoorChatDatabase] specifically /// for native platform applications @@ -20,28 +21,29 @@ class SharedDB { static Future constructDatabase( String userId, { bool logStatements = false, + bool persistOnDisk = true, }) async { final dbName = 'db_$userId'; - if (Platform.isIOS || Platform.isAndroid) { - final dir = await getApplicationDocumentsDirectory(); - final path = join(dir.path, '$dbName.sqlite'); - final file = File(path); - return VmDatabase(file, logStatements: logStatements); - } - if (Platform.isMacOS || Platform.isLinux) { - final file = File('$dbName.sqlite'); - return VmDatabase(file, logStatements: logStatements); + if (persistOnDisk) { + if (Platform.isIOS || Platform.isAndroid) { + final dir = await getApplicationDocumentsDirectory(); + final path = join(dir.path, '$dbName.sqlite'); + final file = File(path); + return VmDatabase(file, logStatements: logStatements); + } + if (Platform.isMacOS || Platform.isLinux) { + final file = File('$dbName.sqlite'); + return VmDatabase(file, logStatements: logStatements); + } } return VmDatabase.memory(logStatements: logStatements); } static void _startBackground(_IsolateStartRequest request) { - final executor = LazyDatabase(() async { - return VmDatabase( - File(request.targetPath), - logStatements: request.logStatements, - ); - }); + final executor = LazyDatabase(() async => VmDatabase( + File(request.targetPath), + logStatements: request.logStatements, + )); final moorIsolate = MoorIsolate.inCurrent( () => DatabaseConnection.fromExecutor(executor), ); @@ -65,35 +67,39 @@ class SharedDB { ), ); - return (await receivePort.first as MoorIsolate); + return await receivePort.first as MoorIsolate; } /// Returns a new instance of [MoorChatDatabase] using the factory constructor /// [MoorChatDatabase.connect] created on a background isolate. /// /// Generally used with [ConnectionMode.background]. - static Future constructMoorChatDatabase( + static MoorChatDatabase constructMoorChatDatabase( String userId, { bool logStatements = false, - }) async { + }) { final dbName = 'db_$userId'; - final isolate = await _createMoorIsolate( - dbName, - logStatements: logStatements, + return MoorChatDatabase.connect( + userId, + DatabaseConnection.delayed(Future(() async { + final isolate = await _createMoorIsolate( + dbName, + logStatements: logStatements, + ); + return isolate.connect(); + })), ); - final connection = await isolate.connect(); - return MoorChatDatabase.connect(userId, isolate, connection); } } class _IsolateStartRequest { - final SendPort sendMoorIsolate; - final String targetPath; - final bool logStatements; - const _IsolateStartRequest( this.sendMoorIsolate, this.targetPath, { this.logStatements = false, }); + + final SendPort sendMoorIsolate; + final String targetPath; + final bool logStatements; } diff --git a/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart index 303d925f..3a764d24 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/unsupported_db.dart @@ -1,3 +1,5 @@ +import 'package:moor/backends.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/stream_chat_persistence.dart'; /// A Helper class to construct new instances of [MoorChatDatabase] @@ -5,20 +7,23 @@ class SharedDB { /// Returns a new instance of database. /// /// Generally used with [ConnectionMode.regular]. - static dynamic constructDatabase( + static Future constructDatabase( String userId, { bool logStatements = false, + bool persistOnDisk = true, }) { - throw 'Unsupported Platform'; + throw UnsupportedError( + 'No implementation of the constructDatabase api provided'); } /// Return a new instance of moor chat database. /// /// Generally used with [ConnectionMode.background]. - static dynamic constructMoorChatDatabase( + static MoorChatDatabase constructMoorChatDatabase( String userId, { bool logStatements = false, }) { - throw 'Unsupported Platform'; + throw UnsupportedError( + 'No implementation of the constructMoorChatDatabase api provided'); } } diff --git a/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart index 6f4360c0..32b108ba 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart @@ -1,7 +1,7 @@ import 'package:moor/moor_web.dart'; import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; -import '../moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; /// A Helper class to construct new instances of [MoorChatDatabase] specifically /// for Web applications @@ -12,6 +12,7 @@ class SharedDB { static Future constructDatabase( String userId, { bool logStatements = false, + bool persistOnDisk = true, // ignored on web }) async { final dbName = 'db_$userId'; return WebDatabase(dbName, logStatements: logStatements); @@ -21,10 +22,10 @@ class SharedDB { /// default constructor. /// /// Generally used with [ConnectionMode.background]. - static Future constructMoorChatDatabase( + static MoorChatDatabase constructMoorChatDatabase( String userId, { bool logStatements = false, - }) async { + }) { final dbName = 'db_$userId'; return MoorChatDatabase(dbName, logStatements: logStatements); } diff --git a/packages/stream_chat_persistence/lib/src/entity/channels.dart b/packages/stream_chat_persistence/lib/src/entity/channels.dart index 38a40081..7e32906e 100644 --- a/packages/stream_chat_persistence/lib/src/entity/channels.dart +++ b/packages/stream_chat_persistence/lib/src/entity/channels.dart @@ -17,7 +17,7 @@ class Channels extends Table { TextColumn get config => text().map(MapConverter())(); /// True if this channel entity is frozen - BoolColumn get frozen => boolean().withDefault(Constant(false))(); + BoolColumn get frozen => boolean().withDefault(const Constant(false))(); /// The date of the last message DateTimeColumn get lastMessageAt => dateTime().nullable()(); diff --git a/packages/stream_chat_persistence/lib/src/entity/entity.dart b/packages/stream_chat_persistence/lib/src/entity/entity.dart index ca9d04cb..d5218768 100644 --- a/packages/stream_chat_persistence/lib/src/entity/entity.dart +++ b/packages/stream_chat_persistence/lib/src/entity/entity.dart @@ -1,9 +1,9 @@ +export 'channel_queries.dart'; export 'channels.dart'; +export 'connection_events.dart'; +export 'members.dart'; export 'messages.dart'; export 'pinned_messages.dart'; export 'reactions.dart'; -export 'users.dart'; -export 'members.dart'; export 'reads.dart'; -export 'channel_queries.dart'; -export 'connection_events.dart'; +export 'users.dart'; diff --git a/packages/stream_chat_persistence/lib/src/entity/pinned_messages.dart b/packages/stream_chat_persistence/lib/src/entity/pinned_messages.dart index f6956f54..de081552 100644 --- a/packages/stream_chat_persistence/lib/src/entity/pinned_messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/pinned_messages.dart @@ -1,6 +1,6 @@ import 'package:moor/moor.dart'; -import 'messages.dart'; +import 'package:stream_chat_persistence/src/entity/messages.dart'; /// Represents a [PinnedMessages] table in [MoorChatDatabase]. @DataClassName('PinnedMessageEntity') diff --git a/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart index 3e2520fb..5c8e3e2a 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart @@ -29,34 +29,31 @@ extension ChannelEntityX on ChannelEntity { List reads, List messages, List pinnedMessages, - }) { - return ChannelState( - members: members, - read: reads, - messages: messages, - pinnedMessages: pinnedMessages, - channel: toChannelModel(createdBy: createdBy), - ); - } + }) => + ChannelState( + members: members, + read: reads, + messages: messages, + pinnedMessages: pinnedMessages, + channel: toChannelModel(createdBy: createdBy), + ); } /// Useful mapping functions for [ChannelModel] extension ChannelModelX on ChannelModel { /// Maps a [ChannelModel] into [ChannelEntity] - ChannelEntity toEntity() { - return ChannelEntity( - id: id, - type: type, - cid: cid, - config: config.toJson(), - frozen: frozen, - lastMessageAt: lastMessageAt, - createdAt: createdAt, - updatedAt: updatedAt, - deletedAt: deletedAt, - memberCount: memberCount, - createdById: createdBy.id, - extraData: extraData, - ); - } + ChannelEntity toEntity() => ChannelEntity( + id: id, + type: type, + cid: cid, + config: config.toJson(), + frozen: frozen, + lastMessageAt: lastMessageAt, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + memberCount: memberCount, + createdById: createdBy.id, + extraData: extraData, + ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart index 7bc1739c..fc2a57ba 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart @@ -4,11 +4,9 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; /// Useful mapping functions for [ConnectionEventEntity] extension ConnectionEventX on ConnectionEventEntity { /// Maps a [ConnectionEventEntity] into [Event] - Event toEvent() { - return Event( - me: ownUser != null ? OwnUser.fromJson(ownUser) : null, - totalUnreadCount: totalUnreadCount, - unreadChannels: unreadChannels, - ); - } + Event toEvent() => Event( + me: ownUser != null ? OwnUser.fromJson(ownUser) : null, + totalUnreadCount: totalUnreadCount, + unreadChannels: unreadChannels, + ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/mapper.dart index cdc31e05..66bf3cda 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/mapper.dart @@ -1,8 +1,8 @@ -export 'user_mapper.dart'; -export 'reaction_mapper.dart'; export 'channel_mapper.dart'; export 'event_mapper.dart'; export 'member_mapper.dart'; -export 'read_mapper.dart'; export 'message_mapper.dart'; export 'pinned_message_mapper.dart'; +export 'reaction_mapper.dart'; +export 'read_mapper.dart'; +export 'user_mapper.dart'; diff --git a/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart index 2d32c9f1..cbdcc297 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart @@ -4,39 +4,35 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; /// Useful mapping functions for [MemberEntity] extension MemberEntityX on MemberEntity { /// Maps a [MemberEntity] into [Member] - Member toMember({User user}) { - return Member( - user: user, - userId: userId, - banned: banned, - shadowBanned: shadowBanned, - updatedAt: updatedAt, - createdAt: createdAt, - role: role, - inviteAcceptedAt: inviteAcceptedAt, - invited: invited, - inviteRejectedAt: inviteRejectedAt, - isModerator: isModerator, - ); - } + Member toMember({User user}) => Member( + user: user, + userId: userId, + banned: banned, + shadowBanned: shadowBanned, + updatedAt: updatedAt, + createdAt: createdAt, + role: role, + inviteAcceptedAt: inviteAcceptedAt, + invited: invited, + inviteRejectedAt: inviteRejectedAt, + isModerator: isModerator, + ); } /// Useful mapping functions for [Member] extension MemberX on Member { /// Maps a [Member] into [MemberEntity] - MemberEntity toEntity({String cid}) { - return MemberEntity( - userId: user?.id, - banned: banned, - shadowBanned: shadowBanned, - channelCid: cid, - createdAt: createdAt, - isModerator: isModerator, - inviteRejectedAt: inviteRejectedAt, - invited: invited, - inviteAcceptedAt: inviteAcceptedAt, - role: role, - updatedAt: updatedAt, - ); - } + MemberEntity toEntity({String cid}) => MemberEntity( + userId: user?.id, + banned: banned, + shadowBanned: shadowBanned, + channelCid: cid, + createdAt: createdAt, + isModerator: isModerator, + inviteRejectedAt: inviteRejectedAt, + invited: invited, + inviteAcceptedAt: inviteAcceptedAt, + role: role, + updatedAt: updatedAt, + ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart index 5acf1a84..fdf93cc5 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart @@ -12,70 +12,66 @@ extension MessageEntityX on MessageEntity { List latestReactions, List ownReactions, Message quotedMessage, - }) { - return Message( - shadowed: shadowed, - latestReactions: latestReactions, - ownReactions: ownReactions, - attachments: attachments?.map((it) { - final json = jsonDecode(it); - return Attachment.fromData(json); - })?.toList(), - createdAt: createdAt, - extraData: extraData, - updatedAt: updatedAt, - id: id, - type: type, - status: status, - command: command, - parentId: parentId, - quotedMessageId: quotedMessageId, - quotedMessage: quotedMessage, - reactionCounts: reactionCounts, - reactionScores: reactionScores, - replyCount: replyCount, - showInChannel: showInChannel, - text: messageText, - user: user, - deletedAt: deletedAt, - pinned: pinned, - pinnedAt: pinnedAt, - pinExpires: pinExpires, - pinnedBy: pinnedBy, - ); - } + }) => + Message( + shadowed: shadowed, + latestReactions: latestReactions, + ownReactions: ownReactions, + attachments: attachments?.map((it) { + final json = jsonDecode(it); + return Attachment.fromData(json); + })?.toList(), + createdAt: createdAt, + extraData: extraData, + updatedAt: updatedAt, + id: id, + type: type, + status: status, + command: command, + parentId: parentId, + quotedMessageId: quotedMessageId, + quotedMessage: quotedMessage, + reactionCounts: reactionCounts, + reactionScores: reactionScores, + replyCount: replyCount, + showInChannel: showInChannel, + text: messageText, + user: user, + deletedAt: deletedAt, + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedBy: pinnedBy, + ); } /// Useful mapping functions for [Message] extension MessageX on Message { /// Maps a [Message] into [MessageEntity] - MessageEntity toEntity({String cid}) { - return MessageEntity( - id: id, - attachments: attachments?.map((it) { - return jsonEncode(it.toData()); - })?.toList(), - channelCid: cid, - type: type, - parentId: parentId, - quotedMessageId: quotedMessageId, - command: command, - createdAt: createdAt, - shadowed: shadowed, - showInChannel: showInChannel, - replyCount: replyCount, - reactionScores: reactionScores, - reactionCounts: reactionCounts, - status: status, - updatedAt: updatedAt, - extraData: extraData, - userId: user?.id, - deletedAt: deletedAt, - messageText: text, - pinned: pinned, - pinnedAt: pinnedAt, - pinExpires: pinExpires, - pinnedByUserId: pinnedBy?.id, - ); - } + MessageEntity toEntity({String cid}) => MessageEntity( + id: id, + attachments: + attachments?.map((it) => jsonEncode(it.toData()))?.toList(), + channelCid: cid, + type: type, + parentId: parentId, + quotedMessageId: quotedMessageId, + command: command, + createdAt: createdAt, + shadowed: shadowed, + showInChannel: showInChannel, + replyCount: replyCount, + reactionScores: reactionScores, + reactionCounts: reactionCounts, + status: status, + updatedAt: updatedAt, + extraData: extraData, + userId: user?.id, + deletedAt: deletedAt, + messageText: text, + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedByUserId: pinnedBy?.id, + ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart index 1abe896f..ec2a0e91 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart @@ -12,70 +12,66 @@ extension PinnedMessageEntityX on PinnedMessageEntity { List latestReactions, List ownReactions, Message quotedMessage, - }) { - return Message( - shadowed: shadowed, - latestReactions: latestReactions, - ownReactions: ownReactions, - attachments: attachments?.map((it) { - final json = jsonDecode(it); - return Attachment.fromData(json); - })?.toList(), - createdAt: createdAt, - extraData: extraData, - updatedAt: updatedAt, - id: id, - type: type, - status: status, - command: command, - parentId: parentId, - quotedMessageId: quotedMessageId, - quotedMessage: quotedMessage, - reactionCounts: reactionCounts, - reactionScores: reactionScores, - replyCount: replyCount, - showInChannel: showInChannel, - text: messageText, - user: user, - deletedAt: deletedAt, - pinned: pinned, - pinnedAt: pinnedAt, - pinExpires: pinExpires, - pinnedBy: pinnedBy, - ); - } + }) => + Message( + shadowed: shadowed, + latestReactions: latestReactions, + ownReactions: ownReactions, + attachments: attachments?.map((it) { + final json = jsonDecode(it); + return Attachment.fromData(json); + })?.toList(), + createdAt: createdAt, + extraData: extraData, + updatedAt: updatedAt, + id: id, + type: type, + status: status, + command: command, + parentId: parentId, + quotedMessageId: quotedMessageId, + quotedMessage: quotedMessage, + reactionCounts: reactionCounts, + reactionScores: reactionScores, + replyCount: replyCount, + showInChannel: showInChannel, + text: messageText, + user: user, + deletedAt: deletedAt, + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedBy: pinnedBy, + ); } /// Useful mapping functions for [Message] extension PMessageX on Message { /// Maps a [Message] into [PinnedMessageEntity] - PinnedMessageEntity toPinnedEntity({String cid}) { - return PinnedMessageEntity( - id: id, - attachments: attachments?.map((it) { - return jsonEncode(it.toData()); - })?.toList(), - channelCid: cid, - type: type, - parentId: parentId, - quotedMessageId: quotedMessageId, - command: command, - createdAt: createdAt, - shadowed: shadowed, - showInChannel: showInChannel, - replyCount: replyCount, - reactionScores: reactionScores, - reactionCounts: reactionCounts, - status: status, - updatedAt: updatedAt, - extraData: extraData, - userId: user?.id, - deletedAt: deletedAt, - messageText: text, - pinned: pinned, - pinnedAt: pinnedAt, - pinExpires: pinExpires, - pinnedByUserId: pinnedBy?.id, - ); - } + PinnedMessageEntity toPinnedEntity({String cid}) => PinnedMessageEntity( + id: id, + attachments: + attachments?.map((it) => jsonEncode(it.toData()))?.toList(), + channelCid: cid, + type: type, + parentId: parentId, + quotedMessageId: quotedMessageId, + command: command, + createdAt: createdAt, + shadowed: shadowed, + showInChannel: showInChannel, + replyCount: replyCount, + reactionScores: reactionScores, + reactionCounts: reactionCounts, + status: status, + updatedAt: updatedAt, + extraData: extraData, + userId: user?.id, + deletedAt: deletedAt, + messageText: text, + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedByUserId: pinnedBy?.id, + ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart index d265fcb7..20842307 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart @@ -4,30 +4,26 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; /// Useful mapping functions for [ReactionEntity] extension ReactionEntityX on ReactionEntity { /// Maps a [ReactionEntity] into [Reaction] - Reaction toReaction({User user}) { - return Reaction( - extraData: extraData, - type: type, - createdAt: createdAt, - userId: userId, - user: user, - messageId: messageId, - score: score, - ); - } + Reaction toReaction({User user}) => Reaction( + extraData: extraData, + type: type, + createdAt: createdAt, + userId: userId, + user: user, + messageId: messageId, + score: score, + ); } /// Useful mapping functions for [Reaction] extension ReactionX on Reaction { /// Maps a [Reaction] into [ReactionEntity] - ReactionEntity toEntity() { - return ReactionEntity( - extraData: extraData, - type: type, - createdAt: createdAt, - userId: userId, - messageId: messageId, - score: score, - ); - } + ReactionEntity toEntity() => ReactionEntity( + extraData: extraData, + type: type, + createdAt: createdAt, + userId: userId, + messageId: messageId, + score: score, + ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/read_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/read_mapper.dart index c79cf8d5..9664030e 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/read_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/read_mapper.dart @@ -4,24 +4,20 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; /// Useful mapping functions for [ReadEntity] extension ReadEntityX on ReadEntity { /// Maps a [ReadEntity] into [Read] - Read toRead({User user}) { - return Read( - user: user, - lastRead: lastRead, - unreadMessages: unreadMessages, - ); - } + Read toRead({User user}) => Read( + user: user, + lastRead: lastRead, + unreadMessages: unreadMessages, + ); } /// Useful mapping functions for [Read] extension ReadX on Read { /// Maps a [Read] into [ReadEntity] - ReadEntity toEntity({String cid}) { - return ReadEntity( - lastRead: lastRead, - userId: user?.id, - channelCid: cid, - unreadMessages: unreadMessages, - ); - } + ReadEntity toEntity({String cid}) => ReadEntity( + lastRead: lastRead, + userId: user?.id, + channelCid: cid, + unreadMessages: unreadMessages, + ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart index b1e53786..533a45b1 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart @@ -4,33 +4,29 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; /// Useful mapping functions for [UserEntity] extension UserEntityX on UserEntity { /// Maps a [UserEntity] into [User] - User toUser() { - return User( - id: id, - updatedAt: updatedAt, - role: role, - online: online, - lastActive: lastActive, - extraData: extraData, - banned: banned, - createdAt: createdAt, - ); - } + User toUser() => User( + id: id, + updatedAt: updatedAt, + role: role, + online: online, + lastActive: lastActive, + extraData: extraData, + banned: banned, + createdAt: createdAt, + ); } /// Useful mapping functions for [User] extension UserX on User { /// Maps a [User] into [UserEntity] - UserEntity toEntity() { - return UserEntity( - id: id, - role: role, - createdAt: createdAt, - updatedAt: updatedAt, - lastActive: lastActive, - online: online, - banned: banned, - extraData: extraData, - ); - } + UserEntity toEntity() => UserEntity( + id: id, + role: role, + createdAt: createdAt, + updatedAt: updatedAt, + lastActive: lastActive, + online: online, + banned: banned, + extraData: extraData, + ); } diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index e6be01ec..e7ee060b 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -1,7 +1,10 @@ +import 'package:logging/logging.dart' show LogRecord; +import 'package:meta/meta.dart'; +import 'package:mutex/mutex.dart'; import 'package:stream_chat/stream_chat.dart'; -import 'db/moor_chat_database.dart'; -import 'db/shared/shared_db.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/db/shared/shared_db.dart'; /// Various connection modes on which [StreamChatPersistenceClient] can work enum ConnectionMode { @@ -12,6 +15,12 @@ enum ConnectionMode { background, } +final _levelEmojiMapper = { + Level.INFO: 'â„šī¸', + Level.WARNING: 'âš ī¸', + Level.SEVERE: '🚨', +}; + /// A [MoorChatDatabase] based implementation of the [ChatPersistenceClient] class StreamChatPersistenceClient extends ChatPersistenceClient { /// Creates a new instance of the stream chat persistence client @@ -19,18 +28,65 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { /// Connection mode on which the client will work ConnectionMode connectionMode = ConnectionMode.regular, Level logLevel = Level.WARNING, - }) : assert(connectionMode != null), - assert(logLevel != null), + LogHandlerFunction logHandlerFunction, + }) : assert(connectionMode != null, 'ConnectionMode cannot be null'), + assert(logLevel != null, 'LogLevel cannot be null'), _connectionMode = connectionMode, - _logger = Logger.detached('đŸ’Ŋ')..level = logLevel; + _logger = Logger.detached('đŸ’Ŋ')..level = logLevel { + _logger.onRecord.listen(logHandlerFunction ?? _defaultLogHandler); + } + + /// A function that has a parameter of type [LogRecord]. + /// This is called on every new log record. + /// By default the client will use the handler returned by + /// [_getDefaultLogHandler]. + /// Setting it you can handle the log messages directly instead of have them + /// written to stdout, + /// this is very convenient if you use an error tracking tool or if you want + /// to centralize your logs into one facility. + /// + /// ```dart + /// myLogHandlerFunction = (LogRecord record) { + /// // do something with the record (ie. send it to Sentry or Fabric) + /// } + /// + /// final client = StreamChatPersistenceClient( + /// logHandlerFunction: myLogHandlerFunction, + /// ); + ///``` + LogHandlerFunction logHandlerFunction; + + /// [MoorChatDatabase] instance used by this client. + @visibleForTesting + MoorChatDatabase db; - MoorChatDatabase _db; final Logger _logger; final ConnectionMode _connectionMode; + final _mutex = ReadWriteMutex(); + + void _defaultLogHandler(LogRecord record) { + print( + '(${record.time}) ' + '${_levelEmojiMapper[record.level] ?? record.level.name} ' + '${record.loggerName} ${record.message}', + ); + if (record.stackTrace != null) print(record.stackTrace); + } + + Future _readProtected(Future Function() f) async { + T ret; + await _mutex.protectRead(() async { + if (db == null) { + return; + } + ret = await f(); + }); + return ret; + } @override Future connect(String userId) async { - if (_db != null) { + if (db != null) { throw Exception( 'An instance of StreamChatDatabase is already connected.\n' 'disconnect the previous instance before connecting again.', @@ -39,211 +95,266 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { switch (_connectionMode) { case ConnectionMode.regular: _logger.info('Connecting on a regular isolate'); - _db = MoorChatDatabase(userId); + db = MoorChatDatabase(userId); return; case ConnectionMode.background: _logger.info('Connecting on background isolate'); - _db = await SharedDB.constructMoorChatDatabase(userId); + db = SharedDB.constructMoorChatDatabase(userId); return; } } @override - Future getConnectionInfo() { - return _db.connectionEventDao.connectionEvent; - } + Future getConnectionInfo() => _readProtected(() { + _logger.info('getConnectionInfo'); + return db.connectionEventDao.connectionEvent; + }); @override - Future updateConnectionInfo(Event event) { - return _db.connectionEventDao.updateConnectionEvent(event); - } + Future updateConnectionInfo(Event event) => _readProtected(() { + _logger.info('updateConnectionInfo'); + return db.connectionEventDao.updateConnectionEvent(event); + }); @override - Future updateLastSyncAt(DateTime lastSyncAt) { - return _db.connectionEventDao.updateLastSyncAt(lastSyncAt); - } + Future updateLastSyncAt(DateTime lastSyncAt) => _readProtected(() { + _logger.info('updateLastSyncAt'); + return db.connectionEventDao.updateLastSyncAt(lastSyncAt); + }); @override - Future getLastSyncAt() { - return _db.connectionEventDao.lastSyncAt; - } + Future getLastSyncAt() => _readProtected(() { + _logger.info('getLastSyncAt'); + return db.connectionEventDao.lastSyncAt; + }); @override - Future deleteChannels(List cids) { - return _db.channelDao.deleteChannelByCids(cids); - } + Future deleteChannels(List cids) => _readProtected(() { + _logger.info('deleteChannels'); + return db.channelDao.deleteChannelByCids(cids); + }); @override - Future> getChannelCids() => _db.channelDao.cids; + Future> getChannelCids() => _readProtected(() { + _logger.info('getChannelCids'); + return db.channelDao.cids; + }); @override - Future deleteMessageByIds(List messageIds) { - return _db.messageDao.deleteMessageByIds(messageIds); - } + Future deleteMessageByIds(List messageIds) => + _readProtected(() { + _logger.info('deleteMessageByIds'); + return db.messageDao.deleteMessageByIds(messageIds); + }); @override - Future deletePinnedMessageByIds(List messageIds) { - return _db.pinnedMessageDao.deleteMessageByIds(messageIds); - } + Future deletePinnedMessageByIds(List messageIds) => + _readProtected(() { + _logger.info('deletePinnedMessageByIds'); + return db.pinnedMessageDao.deleteMessageByIds(messageIds); + }); @override - Future deleteMessageByCids(List cids) { - return _db.messageDao.deleteMessageByCids(cids); - } + Future deleteMessageByCids(List cids) => _readProtected(() { + _logger.info('deleteMessageByCids'); + return db.messageDao.deleteMessageByCids(cids); + }); @override - Future deletePinnedMessageByCids(List cids) { - return _db.pinnedMessageDao.deleteMessageByCids(cids); - } + Future deletePinnedMessageByCids(List cids) => + _readProtected(() { + _logger.info('deletePinnedMessageByCids'); + return db.pinnedMessageDao.deleteMessageByCids(cids); + }); @override - Future> getMembersByCid(String cid) { - return _db.memberDao.getMembersByCid(cid); - } + Future> getMembersByCid(String cid) => _readProtected(() { + _logger.info('getMembersByCid'); + return db.memberDao.getMembersByCid(cid); + }); @override - Future getChannelByCid(String cid) { - return _db.channelDao.getChannelByCid(cid); - } + Future getChannelByCid(String cid) => _readProtected(() { + _logger.info('getChannelByCid'); + return db.channelDao.getChannelByCid(cid); + }); @override Future> getMessagesByCid( String cid, { PaginationParams messagePagination, - }) { - return _db.messageDao.getMessagesByCid( - cid, - messagePagination: messagePagination, - ); - } + }) => + _readProtected(() { + _logger.info('getMessagesByCid'); + return db.messageDao.getMessagesByCid( + cid, + messagePagination: messagePagination, + ); + }); @override Future> getPinnedMessagesByCid( String cid, { PaginationParams messagePagination, - }) { - return _db.pinnedMessageDao.getMessagesByCid( - cid, - messagePagination: messagePagination, - ); - } + }) => + _readProtected(() { + _logger.info('getPinnedMessagesByCid'); + return db.pinnedMessageDao.getMessagesByCid( + cid, + messagePagination: messagePagination, + ); + }); @override - Future> getReadsByCid(String cid) { - return _db.readDao.getReadsByCid(cid); - } + Future> getReadsByCid(String cid) => _readProtected(() { + _logger.info('getReadsByCid'); + return db.readDao.getReadsByCid(cid); + }); @override - Future>> getChannelThreads(String cid) async { - final messages = await _db.messageDao.getThreadMessages(cid); - final messageByParentIdDictionary = >{}; - for (final message in messages) { - final parentId = message.parentId; - messageByParentIdDictionary[parentId] = [ - ...messageByParentIdDictionary[parentId] ?? [], - message - ]; - } - return messageByParentIdDictionary; - } + Future>> getChannelThreads(String cid) async => + _readProtected(() async { + _logger.info('getChannelThreads'); + final messages = await db.messageDao.getThreadMessages(cid); + final messageByParentIdDictionary = >{}; + for (final message in messages) { + final parentId = message.parentId; + messageByParentIdDictionary[parentId] = [ + ...messageByParentIdDictionary[parentId] ?? [], + message + ]; + } + return messageByParentIdDictionary; + }); @override Future> getReplies( String parentId, { PaginationParams options, - }) { - return _db.messageDao.getThreadMessagesByParentId( - parentId, - options: options, - ); - } + }) => + _readProtected(() async { + _logger.info('getReplies'); + return db.messageDao.getThreadMessagesByParentId( + parentId, + options: options, + ); + }); @override Future> getChannelStates({ Map filter, List> sort = const [], PaginationParams paginationParams, - }) async { - final channels = await _db.channelQueryDao.getChannels( - filter: filter, - sort: sort, - paginationParams: paginationParams, - ); - return Future.wait(channels.map((e) => getChannelStateByCid(e.cid))); - } + }) async => + _readProtected(() async { + _logger.info('getChannelStates'); + final channels = await db.channelQueryDao.getChannels( + filter: filter, + sort: sort, + paginationParams: paginationParams, + ); + return Future.wait(channels.map((e) => getChannelStateByCid(e.cid))); + }); @override Future updateChannelQueries( Map filter, List cids, bool clearQueryCache, - ) { - return _db.channelQueryDao.updateChannelQueries( - filter, - cids, - clearQueryCache, - ); - } + ) => + _readProtected(() async { + _logger.info('updateChannelQueries'); + return db.channelQueryDao.updateChannelQueries( + filter, + cids, + clearQueryCache: clearQueryCache, + ); + }); @override - Future updateChannels(List channels) { - return _db.channelDao.updateChannels(channels); - } + Future updateChannels(List channels) => + _readProtected(() async { + _logger.info('updateChannels'); + return db.channelDao.updateChannels(channels); + }); @override - Future updateMembers(String cid, List members) { - return _db.memberDao.updateMembers(cid, members); - } + Future updateMembers(String cid, List members) => + _readProtected(() async { + _logger.info('updateMembers'); + return db.memberDao.updateMembers(cid, members); + }); @override - Future updateMessages(String cid, List messages) { - return _db.messageDao.updateMessages(cid, messages); - } + Future updateMessages(String cid, List messages) => + _readProtected(() async { + _logger.info('updateMessages'); + return db.messageDao.updateMessages(cid, messages); + }); @override - Future updatePinnedMessages(String cid, List messages) { - return _db.pinnedMessageDao.updateMessages(cid, messages); - } + Future updatePinnedMessages(String cid, List messages) => + _readProtected(() async { + _logger.info('updatePinnedMessages'); + return db.pinnedMessageDao.updateMessages(cid, messages); + }); @override - Future updateReactions(List reactions) { - return _db.reactionDao.updateReactions(reactions); - } + Future updateReactions(List reactions) => + _readProtected(() async { + _logger.info('updateReactions'); + return db.reactionDao.updateReactions(reactions); + }); @override - Future updateReads(String cid, List reads) { - return _db.readDao.updateReads(cid, reads); - } + Future updateReads(String cid, List reads) => + _readProtected(() async { + _logger.info('updateReads'); + return db.readDao.updateReads(cid, reads); + }); @override - Future updateUsers(List users) { - return _db.userDao.updateUsers(users); - } + Future updateUsers(List users) => _readProtected(() async { + _logger.info('updateUsers'); + return db.userDao.updateUsers(users); + }); @override - Future deleteReactionsByMessageId(List messageIds) { - return _db.reactionDao.deleteReactionsByMessageIds(messageIds); - } + Future deleteReactionsByMessageId(List messageIds) => + _readProtected(() async { + _logger.info('deleteReactionsByMessageId'); + return db.reactionDao.deleteReactionsByMessageIds(messageIds); + }); @override - Future deleteMembersByCids(List cids) { - return _db.memberDao.deleteMemberByCids(cids); - } + Future deleteMembersByCids(List cids) => + _readProtected(() async { + _logger.info('deleteMembersByCids'); + return db.memberDao.deleteMemberByCids(cids); + }); @override - Future disconnect({bool flush = false}) async { - if (_db != null) { - _logger.info('Disconnecting'); - if (flush) { - _logger.info('Flushing'); - await _db.batch((batch) { - _db.allTables.forEach((table) { - _db.delete(table).go(); - }); - }); - } - await _db.disconnect(); - _db = null; - } - } + Future updateChannelStates(List channelStates) => + _readProtected(() async => db.transaction(() async { + await super.updateChannelStates(channelStates); + })); + + @override + Future disconnect({bool flush = false}) async => + _mutex.protectWrite(() async { + _logger.info('disconnect'); + if (db != null) { + _logger.info('Disconnecting'); + if (flush) { + _logger.info('Flushing'); + await db.batch((batch) { + db.allTables.forEach((table) { + db.delete(table).go(); + }); + }); + } + await db.disconnect(); + db = null; + } + }); } diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index b82269ca..12d117b1 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_persistence homepage: https://github.com/GetStream/stream-chat-flutter description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. -version: 1.4.0-beta +version: 1.5.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -11,14 +11,17 @@ environment: dependencies: flutter: sdk: flutter + logging: ^0.11.4 + meta: ^1.2.4 moor: ^3.4.0 + mutex: ^2.0.0 path: ^1.7.0 path_provider: ^1.6.27 sqlite3_flutter_libs: ^0.4.0+1 - stream_chat: ^1.4.0-beta + stream_chat: ^1.5.0 dev_dependencies: - test: ^1.15.7 build_runner: ^1.11.0 moor_generator: ^3.4.1 pedantic: ^1.9.2 + test: ^1.15.7 diff --git a/packages/stream_chat_persistence/test/src/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/src/stream_chat_persistence_client_test.dart new file mode 100644 index 00000000..561ca04d --- /dev/null +++ b/packages/stream_chat_persistence/test/src/stream_chat_persistence_client_test.dart @@ -0,0 +1,27 @@ +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; +import 'package:test/test.dart'; + +void main() { + group('connect', () { + test('throws exception because already connected', () { + final streamChatPersistenceClient = StreamChatPersistenceClient( + connectionMode: ConnectionMode.background, + logLevel: Level.INFO, + )..db = MoorChatDatabase( + 'test', + persistOnDisk: false, + ); + + expect( + () => streamChatPersistenceClient.connect('test'), + throwsA(allOf(isException, predicate((e) { + return e.message == + 'An instance of StreamChatDatabase is already connected.\n' + 'disconnect the previous instance before connecting again.'; + }))), + ); + }); + }); +}