Merge pull request #337 from GetStream/release/1.5.0

Release/1.5.0
This commit is contained in:
Salvatore Giordano
2021-03-16 17:06:28 +01:00
committed by GitHub
58 changed files with 1472 additions and 1024 deletions
+1 -1
View File
@@ -95,7 +95,7 @@ jobs:
TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} TOTAL_MAX: ${{ steps.analysis.outputs.total_max }}
run: | run: |
PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX )) PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX ))
if (( $PERCENTAGE < 90 )) if (( $PERCENTAGE < 80 ))
then then
echo Score too low! echo Score too low!
exit 1 exit 1
+2
View File
@@ -5,6 +5,8 @@ on:
- opened - opened
- edited - edited
- synchronize - synchronize
branches:
- develop
jobs: jobs:
main: main:
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash #!/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::$HOME/.pub-cache/bin"
echo "::add-path::$GITHUB_WORKSPACE/_flutter/.pub-cache/bin" echo "::add-path::$GITHUB_WORKSPACE/_flutter/.pub-cache/bin"
echo "::add-path::$GITHUB_WORKSPACE/_flutter/bin/cache/dart-sdk/bin" echo "::add-path::$GITHUB_WORKSPACE/_flutter/bin/cache/dart-sdk/bin"
+28 -33
View File
@@ -13,7 +13,6 @@ on:
jobs: jobs:
analyze: analyze:
if: github.base_ref == 'master'
timeout-minutes: 15 timeout-minutes: 15
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -48,7 +47,6 @@ jobs:
- name: 'Install Tools' - name: 'Install Tools'
run: | run: |
./.github/workflows/scripts/install-tools.sh ./.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' - name: 'Bootstrap Workspace'
run: melos bootstrap run: melos bootstrap
- name: 'Dart' - name: 'Dart'
@@ -56,20 +54,8 @@ jobs:
melos exec -c 1 -- \ melos exec -c 1 -- \
flutter format . flutter format .
./.github/workflows/scripts/validate-formatting.sh ./.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 runs-on: ubuntu-latest
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
@@ -79,26 +65,35 @@ jobs:
- name: 'Install Flutter' - name: 'Install Flutter'
run: ./.github/workflows/scripts/install-flutter.sh stable run: ./.github/workflows/scripts/install-flutter.sh stable
- name: 'Install Tools' - name: 'Install Tools'
run: ./.github/workflows/scripts/install-tools.sh run: |
- name: 'Bootstrap Workspace' ./.github/workflows/scripts/install-tools.sh
run: melos bootstrap flutter pub global activate coverage
- 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
- name: 'Bootstrap Workspace' - name: 'Bootstrap Workspace'
run: melos bootstrap 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' - name: 'Flutter Test'
run: | run: |
melos exec -c 3 --flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \ melos exec -c 3 --flutter --dir-exists=test --ignore="*example*" --ignore="*web*" -- \
flutter test flutter test --coverage
- name: CodeCov
run: bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }}
- uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat/lcov.info
min_coverage: 50
- uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat_persistence/coverage/lcov.info
min_coverage: 0.2
- uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat_flutter_core/coverage/lcov.info
min_coverage: 4.5
- uses: VeryGoodOpenSource/[email protected]
with:
path: packages/stream_chat_flutter/coverage/lcov.info
min_coverage: 16
+2 -1
View File
@@ -2,7 +2,8 @@
.atom/ .atom/
.idea/ .idea/
.vscode/ .vscode/
**/lcov.info
coverage
.packages .packages
.pub/ .pub/
.dart_tool/ .dart_tool/
+4
View File
@@ -1,3 +1,7 @@
## 1.5.0
- Minor fixes and improvements
## 1.4.0-beta ## 1.4.0-beta
- Improved attachment uploading - Improved attachment uploading
@@ -44,7 +44,6 @@ linter:
- avoid_private_typedef_functions - avoid_private_typedef_functions
- avoid_redundant_argument_values - avoid_redundant_argument_values
- avoid_return_types_on_setters - avoid_return_types_on_setters
- avoid_returning_null
- avoid_returning_null_for_void - avoid_returning_null_for_void
- avoid_shadowing_type_parameters - avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements - avoid_single_cascade_in_expression_statements
+13 -6
View File
@@ -7,11 +7,11 @@ import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/api/retry_queue.dart'; import 'package:stream_chat/src/api/retry_queue.dart';
import 'package:stream_chat/src/event_type.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/attachment_file.dart';
import 'package:stream_chat/src/models/channel_state.dart'; import 'package:stream_chat/src/models/channel_state.dart';
import 'package:stream_chat/src/models/user.dart'; import 'package:stream_chat/src/models/user.dart';
import 'package:stream_chat/stream_chat.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. /// This a the class that manages a specific channel.
class Channel { class Channel {
@@ -1209,9 +1209,10 @@ class ChannelClientState {
ChannelClientState( ChannelClientState(
this._channel, this._channel,
ChannelState channelState, ChannelState channelState,
) : _debouncedUpdatePersistenceChannelState = _channel //ignore: unnecessary_parenthesis
?._client?.chatPersistenceClient?.updateChannelState ) : _debouncedUpdatePersistenceChannelState = ((ChannelState state) {
?.debounced(const Duration(seconds: 1)) { _channel?._client?.chatPersistenceClient?.updateChannelState(state);
}).debounced(const Duration(seconds: 1)) {
retryQueue = RetryQueue( retryQueue = RetryQueue(
channel: _channel, channel: _channel,
logger: Logger('RETRY QUEUE ${_channel.cid}'), logger: Logger('RETRY QUEUE ${_channel.cid}'),
@@ -1463,10 +1464,16 @@ class ChannelClientState {
void addMessage(Message message) { void addMessage(Message message) {
if (message.parentId == null || message.showInChannel == true) { if (message.parentId == null || message.showInChannel == true) {
final newMessages = List<Message>.from(_channelState.messages); final newMessages = List<Message>.from(_channelState.messages);
final oldIndex = newMessages.indexWhere((m) => m.id == message.id); final oldIndex = newMessages.indexWhere((m) => m.id == message.id);
if (oldIndex != -1) { 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 { } else {
newMessages.add(message); newMessages.add(message);
} }
+47 -35
View File
@@ -1,6 +1,7 @@
// ignore_for_file: unnecessary_getters_setters
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:stream_chat/src/extensions/map_extension.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:logging/logging.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/db/chat_persistence_client.dart';
import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/exceptions.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/attachment_file.dart';
import 'package:stream_chat/src/models/channel_model.dart'; import 'package:stream_chat/src/models/channel_model.dart';
import 'package:stream_chat/src/models/channel_state.dart'; import 'package:stream_chat/src/models/channel_state.dart';
@@ -106,14 +108,22 @@ class StreamChatClient {
logger.info('instantiating new client'); logger.info('instantiating new client');
} }
set chatPersistenceClient(ChatPersistenceClient value) {
_originalChatPersistenceClient = value;
}
ChatPersistenceClient _originalChatPersistenceClient;
/// Chat persistence client /// Chat persistence client
ChatPersistenceClient chatPersistenceClient; ChatPersistenceClient get chatPersistenceClient => _chatPersistenceClient;
ChatPersistenceClient _chatPersistenceClient;
/// Attachment uploader /// Attachment uploader
AttachmentFileUploader attachmentFileUploader; AttachmentFileUploader attachmentFileUploader;
/// Whether the chat persistence is available or not /// Whether the chat persistence is available or not
bool get persistenceEnabled => chatPersistenceClient != null; bool get persistenceEnabled => _chatPersistenceClient != null;
RetryPolicy _retryPolicy; RetryPolicy _retryPolicy;
@@ -357,7 +367,7 @@ class StreamChatClient {
/// Call this function to dispose the client /// Call this function to dispose the client
void dispose() async { void dispose() async {
await chatPersistenceClient?.disconnect(); await _chatPersistenceClient?.disconnect();
await _disconnect(); await _disconnect();
httpClient.close(); httpClient.close();
await _controller.close(); await _controller.close();
@@ -446,8 +456,8 @@ class StreamChatClient {
if (!event.isLocal) { if (!event.isLocal) {
if (_synced && event.createdAt != null) { if (_synced && event.createdAt != null) {
await chatPersistenceClient?.updateConnectionInfo(event); await _chatPersistenceClient?.updateConnectionInfo(event);
await chatPersistenceClient?.updateLastSyncAt(event.createdAt); await _chatPersistenceClient?.updateLastSyncAt(event.createdAt);
} }
} }
@@ -478,8 +488,9 @@ class StreamChatClient {
_wsConnectionStatus = ConnectionStatus.connecting; _wsConnectionStatus = ConnectionStatus.connecting;
if (persistenceEnabled) { if (_originalChatPersistenceClient != null) {
await chatPersistenceClient.connect(state.user.id); _chatPersistenceClient = _originalChatPersistenceClient;
await _chatPersistenceClient.connect(state.user.id);
} }
_ws = WebSocket( _ws = WebSocket(
@@ -508,34 +519,35 @@ class StreamChatClient {
), ),
); );
if (status == ConnectionStatus.connected && if (status == ConnectionStatus.connected) {
state.channels?.isNotEmpty == true) { handleEvent(Event(
// ignore: unawaited_futures type: EventType.connectionRecovered,
queryChannelsOnline(filter: { online: true,
'cid': { ));
'\$in': state.channels.keys.toList(), if (state.channels?.isNotEmpty == true) {
}, // ignore: unawaited_futures
}).then( queryChannelsOnline(filter: {
(_) async { 'cid': {
await resync(); '\$in': state.channels.keys.toList(),
handleEvent(Event( },
type: EventType.connectionRecovered, }).then(
online: true, (_) async {
)); await resync();
}, },
); );
} else { } else {
_synced = false; _synced = false;
}
} }
}; };
_connectionStatusSubscription = _connectionStatusSubscription =
_ws.connectionStatusStream.listen(_connectionStatusHandler); _ws.connectionStatusStream.listen(_connectionStatusHandler);
var event = await chatPersistenceClient?.getConnectionInfo(); var event = await _chatPersistenceClient?.getConnectionInfo();
await _ws.connect().then((e) async { await _ws.connect().then((e) async {
await chatPersistenceClient?.updateConnectionInfo(e); await _chatPersistenceClient?.updateConnectionInfo(e);
event = e; event = e;
await resync(); await resync();
}).catchError((err, stacktrace) { }).catchError((err, stacktrace) {
@@ -551,14 +563,14 @@ class StreamChatClient {
/// Get the events missed while offline to sync the offline storage /// Get the events missed while offline to sync the offline storage
Future<void> resync([List<String> cids]) async { Future<void> resync([List<String> cids]) async {
final lastSyncAt = await chatPersistenceClient?.getLastSyncAt(); final lastSyncAt = await _chatPersistenceClient?.getLastSyncAt();
if (lastSyncAt == null) { if (lastSyncAt == null) {
_synced = true; _synced = true;
return; return;
} }
cids ??= await chatPersistenceClient?.getChannelCids(); cids ??= await _chatPersistenceClient?.getChannelCids();
if (cids?.isEmpty == true) { if (cids?.isEmpty == true) {
return; return;
@@ -586,7 +598,7 @@ class StreamChatClient {
res.events.forEach(handleEvent); res.events.forEach(handleEvent);
await chatPersistenceClient?.updateLastSyncAt(DateTime.now()); await _chatPersistenceClient?.updateLastSyncAt(DateTime.now());
_synced = true; _synced = true;
} catch (error) { } catch (error) {
logger.severe('Error during resync $error'); logger.severe('Error during resync $error');
@@ -723,7 +735,7 @@ class StreamChatClient {
final updateData = _mapChannelStateToChannel(channels); final updateData = _mapChannelStateToChannel(channels);
await chatPersistenceClient?.updateChannelQueries( await _chatPersistenceClient?.updateChannelQueries(
filter, filter,
channels.map((c) => c.channel.cid).toList(), channels.map((c) => c.channel.cid).toList(),
paginationParams?.offset == null || paginationParams.offset == 0, paginationParams?.offset == null || paginationParams.offset == 0,
@@ -739,7 +751,7 @@ class StreamChatClient {
@required List<SortOption<ChannelModel>> sort, @required List<SortOption<ChannelModel>> sort,
PaginationParams paginationParams = const PaginationParams(), PaginationParams paginationParams = const PaginationParams(),
}) async { }) async {
final offlineChannels = await chatPersistenceClient?.getChannelStates( final offlineChannels = await _chatPersistenceClient?.getChannelStates(
filter: filter, filter: filter,
sort: sort, sort: sort,
paginationParams: paginationParams, paginationParams: paginationParams,
@@ -960,8 +972,8 @@ class StreamChatClient {
logger.info('Disconnecting flushOfflineStorage: $flushChatPersistence; ' logger.info('Disconnecting flushOfflineStorage: $flushChatPersistence; '
'clearUser: $clearUser'); 'clearUser: $clearUser');
await chatPersistenceClient?.disconnect(flush: flushChatPersistence); await _chatPersistenceClient?.disconnect(flush: flushChatPersistence);
chatPersistenceClient = null; _chatPersistenceClient = null;
_connectCompleter = null; _connectCompleter = null;
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client.dart';
/// Current package version /// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header /// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names // ignore: constant_identifier_names
const PACKAGE_VERSION = '1.4.0-beta'; const PACKAGE_VERSION = '1.5.0';
+1 -1
View File
@@ -1,7 +1,7 @@
name: stream_chat name: stream_chat
homepage: https://getstream.io/ homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications. description: The official Dart client for Stream Chat, a service for building chat applications.
version: 1.4.0-beta version: 1.5.0
repository: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
+9 -1
View File
@@ -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 ## 1.4.0-beta
- Unfocus `MessageInput` only when sending commands - Unfocus `MessageInput` only when sending commands
@@ -10,7 +18,7 @@
- Added `MessageListView.onAttachmentTap` callback - Added `MessageListView.onAttachmentTap` callback
- Fixed message newline issue - Fixed message newline issue
- Fixed `MessageListView` scroll keyboard behaviour - Fixed `MessageListView` scroll keyboard behaviour
- Minor fixes and improveqments - Minor fixes and improvements
## 1.3.2-beta ## 1.3.2-beta
@@ -35,6 +35,14 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.0 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: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@@ -13,6 +13,7 @@ import 'attachment_widget.dart';
class FileAttachment extends AttachmentWidget { class FileAttachment extends AttachmentWidget {
final Widget title; final Widget title;
final Widget trailing; final Widget trailing;
final VoidCallback onAttachmentTap;
const FileAttachment({ const FileAttachment({
Key key, Key key,
@@ -21,6 +22,7 @@ class FileAttachment extends AttachmentWidget {
Size size, Size size,
this.title, this.title,
this.trailing, this.trailing,
this.onAttachmentTap,
}) : super(key: key, message: message, attachment: attachment, size: size); }) : super(key: key, message: message, attachment: attachment, size: size);
bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video'; bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video';
@@ -30,45 +32,48 @@ class FileAttachment extends AttachmentWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
child: Container( child: GestureDetector(
width: size?.width ?? 100, onTap: onAttachmentTap,
height: 56.0, child: Container(
decoration: BoxDecoration( width: size?.width ?? 100,
color: StreamChatTheme.of(context).colorTheme.white, height: 56.0,
borderRadius: BorderRadius.circular(12), decoration: BoxDecoration(
border: Border.all( color: StreamChatTheme.of(context).colorTheme.white,
color: StreamChatTheme.of(context).colorTheme.greyWhisper, borderRadius: BorderRadius.circular(12),
border: Border.all(
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
),
), ),
), child: Row(
child: Row( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Container(
Container( height: 40.0,
height: 40.0, width: 33.33,
width: 33.33, margin: EdgeInsets.all(8.0),
margin: EdgeInsets.all(8.0), child: _getFileTypeImage(context),
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),
],
), ),
), SizedBox(width: 8.0),
SizedBox(width: 8.0), Expanded(
_buildTrailing(context), 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),
],
),
), ),
), ),
); );
@@ -12,6 +12,7 @@ class GiphyAttachment extends AttachmentWidget {
final MessageTheme messageTheme; final MessageTheme messageTheme;
final ShowMessageCallback onShowMessage; final ShowMessageCallback onShowMessage;
final ValueChanged<ReturnActionType> onReturnAction; final ValueChanged<ReturnActionType> onReturnAction;
final VoidCallback onAttachmentTap;
const GiphyAttachment({ const GiphyAttachment({
Key key, Key key,
@@ -21,6 +22,7 @@ class GiphyAttachment extends AttachmentWidget {
this.messageTheme, this.messageTheme,
this.onShowMessage, this.onShowMessage,
this.onReturnAction, this.onReturnAction,
this.onAttachmentTap,
}) : super(key: key, message: message, attachment: attachment, size: size); }) : super(key: key, message: message, attachment: attachment, size: size);
@override @override
@@ -62,7 +64,7 @@ class GiphyAttachment extends AttachmentWidget {
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: GestureDetector( child: GestureDetector(
onTap: () => _onImageTap(context), onTap: () => onAttachmentTap ?? _onImageTap(context),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(8), topLeft: Radius.circular(8),
@@ -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/src/attachment/attachment_upload_state_builder.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'attachment_title.dart';
import '../full_screen_media.dart'; import '../full_screen_media.dart';
import '../stream_chat_theme.dart'; import '../stream_chat_theme.dart';
import 'attachment_title.dart';
import 'attachment_widget.dart'; import 'attachment_widget.dart';
class ImageAttachment extends AttachmentWidget { class ImageAttachment extends AttachmentWidget {
@@ -81,6 +81,7 @@ class ImageAttachment extends AttachmentWidget {
return _buildImageAttachment( return _buildImageAttachment(
context, context,
CachedNetworkImage( CachedNetworkImage(
cacheKey: imageUri.path,
height: size?.height, height: size?.height,
width: size?.width, width: size?.width,
placeholder: (_, __) { placeholder: (_, __) {
@@ -5,10 +5,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:stream_chat_flutter/src/message_action.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/reaction_picker.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'extension.dart'; import 'extension.dart';
import 'message_input.dart'; import 'message_input.dart';
@@ -34,6 +34,7 @@ class MessageActionsModal extends StatefulWidget {
final ShapeBorder messageShape; final ShapeBorder messageShape;
final ShapeBorder attachmentShape; final ShapeBorder attachmentShape;
final DisplayWidget showUserAvatar; final DisplayWidget showUserAvatar;
final BorderRadius attachmentBorderRadiusGeometry;
/// List of custom actions /// List of custom actions
final List<MessageAction> customActions; final List<MessageAction> customActions;
@@ -58,6 +59,7 @@ class MessageActionsModal extends StatefulWidget {
this.attachmentShape, this.attachmentShape,
this.reverse = false, this.reverse = false,
this.customActions = const [], this.customActions = const [],
this.attachmentBorderRadiusGeometry,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -153,6 +155,8 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
child: MessageWidget( child: MessageWidget(
key: Key('MessageWidget'), key: Key('MessageWidget'),
reverse: widget.reverse, reverse: widget.reverse,
attachmentBorderRadiusGeometry:
widget.attachmentBorderRadiusGeometry,
message: widget.message.copyWith( message: widget.message.copyWith(
text: widget.message.text.length > 200 text: widget.message.text.length > 200
? '${widget.message.text.substring(0, 200)}...' ? '${widget.message.text.substring(0, 200)}...'
@@ -12,12 +12,12 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:shimmer/shimmer.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/media_list_view.dart';
import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/user_avatar.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:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:substring_highlight/substring_highlight.dart'; import 'package:substring_highlight/substring_highlight.dart';
@@ -35,6 +35,8 @@ typedef AttachmentThumbnailBuilder = Widget Function(
enum ActionsLocation { enum ActionsLocation {
left, left,
right, right,
leftInside,
rightInside,
} }
enum DefaultAttachmentTypes { enum DefaultAttachmentTypes {
@@ -122,6 +124,7 @@ class MessageInput extends StatefulWidget {
this.hideSendAsDm = false, this.hideSendAsDm = false,
this.idleSendButton, this.idleSendButton,
this.activeSendButton, this.activeSendButton,
this.showCommandsButton = true,
}) : super(key: key); }) : super(key: key);
/// Message to edit /// Message to edit
@@ -149,6 +152,9 @@ class MessageInput extends StatefulWidget {
/// If true the attachments button will not be displayed /// If true the attachments button will not be displayed
final bool disableAttachments; final bool disableAttachments;
/// Use this property to hide/show the commands button
final bool showCommandsButton;
/// Hide send as dm checkbox /// Hide send as dm checkbox
final bool hideSendAsDm; final bool hideSendAsDm;
@@ -308,7 +314,7 @@ class MessageInputState extends State<MessageInput> {
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: _buildTextField(context), child: _buildTextField(context),
), ),
if (widget.parentMessage != null && !widget.hideSendAsDm) if (widget.parentMessage != null && !widget.hideSendAsDm)
@@ -336,12 +342,11 @@ class MessageInputState extends State<MessageInput> {
direction: Axis.horizontal, direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
if (!_commandEnabled) _buildExpandActionsButton(), if (!_commandEnabled && widget.actionsLocation == ActionsLocation.left)
if (widget.actionsLocation == ActionsLocation.left) _buildExpandActionsButton(),
...widget.actions ?? [],
_buildTextInput(context), _buildTextInput(context),
if (widget.actionsLocation == ActionsLocation.right) if (!_commandEnabled && widget.actionsLocation == ActionsLocation.right)
...widget.actions ?? [], _buildExpandActionsButton(),
if (widget.sendButtonLocation == SendButtonLocation.outside) if (widget.sendButtonLocation == SendButtonLocation.outside)
_animateSendButton(context), _animateSendButton(context),
], ],
@@ -421,32 +426,36 @@ class MessageInputState extends State<MessageInput> {
child: widget.activeSendButton, child: widget.activeSendButton,
) )
: _buildSendButton(context); : _buildSendButton(context);
return Padding( return AnimatedCrossFade(
padding: const EdgeInsets.all(8.0), crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
child: AnimatedCrossFade( ? CrossFadeState.showFirst
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty) : CrossFadeState.showSecond,
? CrossFadeState.showFirst firstChild: sendButton,
: CrossFadeState.showSecond, secondChild: widget.idleSendButton ?? _buildIdleSendButton(context),
firstChild: sendButton, duration:
secondChild: widget.idleSendButton ?? _buildIdleSendButton(context), StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration,
duration: alignment: Alignment.center,
StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration,
alignment: Alignment.center,
),
); );
} }
Widget _buildExpandActionsButton() { Widget _buildExpandActionsButton() {
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: AnimatedCrossFade( child: AnimatedCrossFade(
crossFadeState: _actionsShrunk crossFadeState: _actionsShrunk
? CrossFadeState.showFirst ? CrossFadeState.showFirst
: CrossFadeState.showSecond, : CrossFadeState.showSecond,
firstChild: IconButton( firstChild: IconButton(
onPressed: () => setState(() => _actionsShrunk = false), onPressed: () => setState(() => _actionsShrunk = false),
icon: StreamSvgIcon.emptyCircleLeft( icon: Transform.rotate(
color: StreamChatTheme.of(context).colorTheme.accentBlue, 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), padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -457,10 +466,12 @@ class MessageInputState extends State<MessageInput> {
), ),
secondChild: FittedBox( secondChild: FittedBox(
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: <Widget>[
if (!widget.disableAttachments) _buildAttachmentButton(), if (!widget.disableAttachments) _buildAttachmentButton(),
if (widget.editMessage == null && if (widget.showCommandsButton &&
widget.editMessage == null &&
StreamChannel.of(context) StreamChannel.of(context)
.channel .channel
?.config ?.config
@@ -468,6 +479,7 @@ class MessageInputState extends State<MessageInput> {
?.isNotEmpty == ?.isNotEmpty ==
true) true)
_buildCommandButton(), _buildCommandButton(),
...widget.actions ?? [],
].insertBetween(const SizedBox(width: 8)), ].insertBetween(const SizedBox(width: 8)),
), ),
), ),
@@ -565,51 +577,75 @@ class MessageInputState extends State<MessageInput> {
), ),
), ),
contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11), contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11),
prefixIconConstraints: BoxConstraints.tight(Size(78, 24)),
suffixIconConstraints: BoxConstraints.tight(Size(40, 40)),
prefixIcon: _commandEnabled prefixIcon: _commandEnabled
? Container( ? Row(
decoration: BoxDecoration( mainAxisSize: MainAxisSize.min,
borderRadius: BorderRadius.circular(12), children: [
color: theme.colorTheme.accentBlue, Padding(
), padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.only(right: 4, left: 8), child: Container(
alignment: Alignment.center, constraints: BoxConstraints.tight(Size(64, 24)),
child: Row( decoration: BoxDecoration(
mainAxisSize: MainAxisSize.min, borderRadius: BorderRadius.circular(12),
children: [ color: theme.colorTheme.accentBlue,
StreamSvgIcon.lightning( ),
color: Colors.white, alignment: Alignment.center,
size: 16.0, child: Row(
), mainAxisSize: MainAxisSize.min,
Text( children: [
_chosenCommand?.name?.toUpperCase() ?? '', StreamSvgIcon.lightning(
style: StreamChatTheme.of(context)
.textTheme
.footnoteBold
.copyWith(
color: Colors.white, 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( suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if (_commandEnabled) if (_commandEnabled)
IconButton( Padding(
icon: StreamSvgIcon.closeSmall(), padding: const EdgeInsets.only(right: 8.0),
splashRadius: 24, child: IconButton(
padding: const EdgeInsets.all(0), icon: StreamSvgIcon.closeSmall(),
constraints: BoxConstraints.tightFor( splashRadius: 24,
height: 24, padding: const EdgeInsets.all(0),
width: 24, 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) if (widget.sendButtonLocation == SendButtonLocation.inside)
_animateSendButton(context), _animateSendButton(context),
], ],
@@ -619,7 +655,13 @@ class MessageInputState extends State<MessageInput> {
Timer _debounce; Timer _debounce;
String _previousValue;
void _onChanged(BuildContext context, String s) { void _onChanged(BuildContext context, String s) {
if (s == _previousValue) {
return;
}
_previousValue = s;
if (_debounce?.isActive == true) _debounce.cancel(); if (_debounce?.isActive == true) _debounce.cancel();
_debounce = Timer( _debounce = Timer(
const Duration(milliseconds: 350), const Duration(milliseconds: 350),
@@ -631,7 +673,11 @@ class MessageInputState extends State<MessageInput> {
setState(() { setState(() {
_messageIsPresent = s.trim().isNotEmpty; _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(); _commandsOverlay?.remove();
@@ -1684,13 +1730,19 @@ class MessageInputState extends State<MessageInput> {
} }
Widget _buildCommandButton() { Widget _buildCommandButton() {
final s = textEditingController.text.trim();
return IconButton( return IconButton(
icon: StreamSvgIcon.lightning( icon: StreamSvgIcon.lightning(
color: _commandsOverlay != null color: s.isNotEmpty
? StreamChatTheme.of(context).messageInputTheme.actionButtonColor ? StreamChatTheme.of(context).colorTheme.greyGainsboro
: StreamChatTheme.of(context) : (_commandsOverlay != null
.messageInputTheme ? StreamChatTheme.of(context)
.actionButtonIdleColor, .messageInputTheme
.actionButtonColor
: StreamChatTheme.of(context)
.messageInputTheme
.actionButtonIdleColor),
), ),
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
@@ -1711,7 +1763,9 @@ class MessageInputState extends State<MessageInput> {
if (_commandsOverlay == null) { if (_commandsOverlay == null) {
setState(() { setState(() {
_commandsOverlay = _buildCommandsOverlayEntry(); _commandsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_commandsOverlay); if (_commandsOverlay != null) {
Overlay.of(context).insert(_commandsOverlay);
}
}); });
} else { } else {
setState(() { setState(() {
@@ -1904,11 +1958,11 @@ class MessageInputState extends State<MessageInput> {
if (file == null) return; if (file == null) return;
final mimeType = file.name?.mimeType; final mimeType = file.name?.mimeType ?? file.path.split('/').last.mimeType;
final extraDataMap = <String, dynamic>{}; final extraDataMap = <String, dynamic>{};
if (mimeType.type == 'video' || mimeType.type == 'image') { if (mimeType?.type == 'video' || mimeType?.type == 'image') {
attachmentType = mimeType.type; attachmentType = mimeType.type;
} else { } else {
attachmentType = 'file'; attachmentType = 'file';
@@ -1965,24 +2019,31 @@ class MessageInputState extends State<MessageInput> {
} }
Widget _buildIdleSendButton(BuildContext context) { Widget _buildIdleSendButton(BuildContext context) {
return StreamSvgIcon( return Padding(
assetName: _getIdleSendIcon(), padding: const EdgeInsets.all(8.0),
color: StreamChatTheme.of(context).messageInputTheme.sendButtonIdleColor, child: StreamSvgIcon(
assetName: _getIdleSendIcon(),
color:
StreamChatTheme.of(context).messageInputTheme.sendButtonIdleColor,
),
); );
} }
Widget _buildSendButton(BuildContext context) { Widget _buildSendButton(BuildContext context) {
return IconButton( return Padding(
onPressed: sendMessage, padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(0), child: IconButton(
splashRadius: 24, onPressed: sendMessage,
constraints: BoxConstraints.tightFor( padding: const EdgeInsets.all(0),
height: 24, splashRadius: 24,
width: 24, constraints: BoxConstraints.tightFor(
), height: 24,
icon: StreamSvgIcon( width: 24,
assetName: _getSendIcon(), ),
color: StreamChatTheme.of(context).messageInputTheme.sendButtonColor, icon: StreamSvgIcon(
assetName: _getSendIcon(),
color: StreamChatTheme.of(context).messageInputTheme.sendButtonColor,
),
), ),
); );
} }
@@ -1021,15 +1021,19 @@ class _MessageListViewState extends State<MessageListView> {
!message.isSystem && !message.isSystem &&
!message.isEphemeral && !message.isEphemeral &&
widget.onMessageSwiped != null) { widget.onMessageSwiped != null) {
child = Swipeable( child = Container(
onSwipeEnd: () { decoration: BoxDecoration(),
FocusScope.of(context).unfocus(); clipBehavior: Clip.hardEdge,
widget.onMessageSwiped(message); child: Swipeable(
}, onSwipeEnd: () {
backgroundIcon: StreamSvgIcon.reply( FocusScope.of(context).unfocus();
color: StreamChatTheme.of(context).colorTheme.accentBlue, widget.onMessageSwiped(message);
},
backgroundIcon: StreamSvgIcon.reply(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
child: child,
), ),
child: child,
); );
} }
@@ -1,16 +1,16 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; 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_bubble.dart';
import 'package:stream_chat_flutter/src/reaction_picker.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/stream_chat.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'extension.dart';
import 'message_widget.dart'; import 'message_widget.dart';
import 'stream_chat_theme.dart'; import 'stream_chat_theme.dart';
import 'extension.dart';
class MessageReactionsModal extends StatelessWidget { class MessageReactionsModal extends StatelessWidget {
final Widget Function(BuildContext, Message) editMessageInputBuilder; final Widget Function(BuildContext, Message) editMessageInputBuilder;
@@ -23,6 +23,7 @@ class MessageReactionsModal extends StatelessWidget {
final ShapeBorder messageShape; final ShapeBorder messageShape;
final ShapeBorder attachmentShape; final ShapeBorder attachmentShape;
final void Function(User) onUserAvatarTap; final void Function(User) onUserAvatarTap;
final BorderRadius attachmentBorderRadiusGeometry;
const MessageReactionsModal({ const MessageReactionsModal({
Key key, Key key,
@@ -36,6 +37,7 @@ class MessageReactionsModal extends StatelessWidget {
this.reverse = false, this.reverse = false,
this.showUserAvatar = DisplayWidget.show, this.showUserAvatar = DisplayWidget.show,
this.onUserAvatarTap, this.onUserAvatarTap,
this.attachmentBorderRadiusGeometry,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -132,6 +134,8 @@ class MessageReactionsModal extends StatelessWidget {
shape: messageShape, shape: messageShape,
attachmentShape: attachmentShape, attachmentShape: attachmentShape,
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
attachmentBorderRadiusGeometry:
attachmentBorderRadiusGeometry,
attachmentPadding: EdgeInsets.all( attachmentPadding: EdgeInsets.all(
hasFileAttachment ? 4 : 2, hasFileAttachment ? 4 : 2,
), ),
@@ -20,7 +20,11 @@ import 'extension.dart';
import 'image_group.dart'; import 'image_group.dart';
import 'message_text.dart'; import 'message_text.dart';
typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment); typedef AttachmentBuilder = Widget Function(
BuildContext,
Message,
List<Attachment>,
);
typedef OnQuotedMessageTap = void Function(String); typedef OnQuotedMessageTap = void Function(String);
/// The display behaviour of a widget /// The display behaviour of a widget
@@ -205,63 +209,152 @@ class MessageWidget extends StatefulWidget {
this.customActions = const [], this.customActions = const [],
this.onAttachmentTap, this.onAttachmentTap,
}) : attachmentBuilders = { }) : attachmentBuilders = {
'image': (context, message, attachment) { 'image': (context, message, attachments) {
return ImageAttachment( var border = RoundedRectangleBorder(
attachment: attachment, side: BorderSide.none,
message: message, borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
messageTheme: messageTheme, );
size: Size(
MediaQuery.of(context).size.width * 0.8, if (attachments.length > 1) {
MediaQuery.of(context).size.height * 0.3, 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, border,
onReturnAction: onReturnAction, reverse,
onAttachmentTap: onAttachmentTap != null attachmentBorderRadiusGeometry ?? BorderRadius.zero,
? () {
onAttachmentTap?.call(message, attachment);
}
: null,
); );
}, },
'video': (context, message, attachment) { 'video': (context, message, attachments) {
return VideoAttachment( var border = RoundedRectangleBorder(
attachment: attachment, side: BorderSide.none,
messageTheme: messageTheme, borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
size: Size( );
MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3, 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, border,
onShowMessage: onShowMessage, reverse,
onReturnAction: onReturnAction, attachmentBorderRadiusGeometry ?? BorderRadius.zero,
onAttachmentTap: onAttachmentTap != null
? () {
onAttachmentTap?.call(message, attachment);
}
: null,
); );
}, },
'giphy': (context, message, attachment) { 'giphy': (context, message, attachments) {
return GiphyAttachment( var border = RoundedRectangleBorder(
attachment: attachment, side: BorderSide.none,
messageTheme: messageTheme, borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
message: message, );
size: Size(
MediaQuery.of(context).size.width * 0.8, return wrapAttachmentWidget(
MediaQuery.of(context).size.height * 0.3, 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, border,
onReturnAction: onReturnAction, reverse,
attachmentBorderRadiusGeometry ?? BorderRadius.zero,
); );
}, },
'file': (context, message, attachment) { 'file': (context, message, attachments) {
return FileAttachment( var border = RoundedRectangleBorder(
message: message, side: attachmentBorderSide ??
attachment: attachment, BorderSide(
size: Size( color: StreamChatTheme.of(context).colorTheme.greyWhisper,
MediaQuery.of(context).size.width * 0.8, ),
MediaQuery.of(context).size.height * 0.3, borderRadius: attachmentBorderRadiusGeometry ?? BorderRadius.zero,
), );
return Column(
children: attachments
.map<Widget>((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 ?? {}), }..addAll(customAttachmentBuilders ?? {}),
@@ -739,6 +832,8 @@ class _MessageWidgetState extends State<MessageWidget>
return StreamChannel( return StreamChannel(
channel: channel, channel: channel,
child: MessageActionsModal( child: MessageActionsModal(
attachmentBorderRadiusGeometry:
widget.attachmentBorderRadiusGeometry,
showUserAvatar: showUserAvatar:
widget.message.user.id == channel.client.state.user.id widget.message.user.id == channel.client.state.user.id
? DisplayWidget.gone ? DisplayWidget.gone
@@ -786,6 +881,8 @@ class _MessageWidgetState extends State<MessageWidget>
return StreamChannel( return StreamChannel(
channel: channel, channel: channel,
child: MessageReactionsModal( child: MessageReactionsModal(
attachmentBorderRadiusGeometry:
widget.attachmentBorderRadiusGeometry,
showUserAvatar: showUserAvatar:
widget.message.user.id == channel.client.state.user.id widget.message.user.id == channel.client.state.user.id
? DisplayWidget.gone ? DisplayWidget.gone
@@ -830,55 +927,37 @@ class _MessageWidgetState extends State<MessageWidget>
} }
Widget _parseAttachments() { Widget _parseAttachments() {
final images = widget.message.attachments final attachmentGroups = <String, List<Attachment>>{};
?.where((element) =>
element.type == 'image' && element.ogScrapeUrl == null)
?.toList() ??
[];
if (images.length > 1) { widget.message.attachments
return Padding( .where((element) => element.ogScrapeUrl == null)
padding: widget.attachmentPadding, .forEach((e) {
child: wrapAttachmentWidget( if (attachmentGroups[e.type] == null) {
context, attachmentGroups[e.type] = [];
Material( }
color: widget.messageTheme.messageBackgroundColor,
child: ImageGroup( attachmentGroups[e.type].add(e);
size: Size( });
MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3, final attachmentList = <Widget>[];
),
images: images, attachmentGroups.forEach((type, attachments) {
message: widget.message, final attachmentBuilder = widget.attachmentBuilders[type];
messageTheme: widget.messageTheme,
onShowMessage: widget.onShowMessage, if (attachmentBuilder == null) return SizedBox();
), final attachmentWidget = attachmentBuilder(
), context,
), widget.message,
attachments,
); );
} attachmentList.add(attachmentWidget);
});
return Padding( return Padding(
padding: widget.attachmentPadding, padding: widget.attachmentPadding,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: widget.message.attachments children: attachmentList?.insertBetween(SizedBox(
?.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(
height: widget.attachmentPadding.vertical / 2, height: widget.attachmentPadding.vertical / 2,
)) ?? )) ??
[], [],
@@ -886,24 +965,6 @@ class _MessageWidgetState extends State<MessageWidget>
); );
} }
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) { void onLongPress(BuildContext context) {
if (widget.message.isEphemeral || if (widget.message.isEphemeral ||
widget.message.status == MessageSendingStatus.sending) { widget.message.status == MessageSendingStatus.sending) {
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -348,3 +350,25 @@ StreamSvgIcon getFileTypeImage(String type) {
break; 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,
),
),
);
}
+2 -2
View File
@@ -1,7 +1,7 @@
name: stream_chat_flutter name: stream_chat_flutter
homepage: https://github.com/GetStream/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. 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 repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -11,7 +11,7 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat_flutter_core: ^1.4.0-beta stream_chat_flutter_core: ^1.5.0
photo_view: ^0.11.0 photo_view: ^0.11.0
rxdart: ^0.25.0 rxdart: ^0.25.0
scrollable_positioned_list: ^0.1.8 scrollable_positioned_list: ^0.1.8
@@ -1,3 +1,7 @@
## 1.5.0
* Minor fixes and improvements
## 1.4.0-beta ## 1.4.0-beta
* Added `MessageListCore.messageFilter` to filter messages locally * Added `MessageListCore.messageFilter` to filter messages locally
@@ -1,7 +1,7 @@
name: stream_chat_flutter_core name: stream_chat_flutter_core
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and 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 repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -10,7 +10,7 @@ environment:
flutter: ">=1.17.0" flutter: ">=1.17.0"
dependencies: dependencies:
stream_chat: ^1.4.0-beta stream_chat: ^1.5.0
flutter: flutter:
sdk: flutter sdk: flutter
rxdart: ^0.25.0 rxdart: ^0.25.0
@@ -1,3 +1,8 @@
## 1.5.0
* Update llc dependency
* Wait for all operations to finish before disconnecting
## 1.4.0-beta ## 1.4.0-beta
* Update llc dependency * Update llc dependency
@@ -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
@@ -6,7 +6,7 @@ import 'package:moor/moor.dart';
/// by the sqlite backend. /// by the sqlite backend.
class ListConverter<T> extends TypeConverter<List<T>, String> { class ListConverter<T> extends TypeConverter<List<T>, String> {
@override @override
List<T> mapToDart(fromDb) { List<T> mapToDart(String fromDb) {
if (fromDb == null) { if (fromDb == null) {
return null; return null;
} }
@@ -14,7 +14,7 @@ class ListConverter<T> extends TypeConverter<List<T>, String> {
} }
@override @override
String mapToSql(value) { String mapToSql(List<T> value) {
if (value == null) { if (value == null) {
return null; return null;
} }
@@ -6,7 +6,7 @@ import 'package:moor/moor.dart';
/// by the sqlite backend. /// by the sqlite backend.
class MapConverter<T> extends TypeConverter<Map<String, T>, String> { class MapConverter<T> extends TypeConverter<Map<String, T>, String> {
@override @override
Map<String, T> mapToDart(fromDb) { Map<String, T> mapToDart(String fromDb) {
if (fromDb == null) { if (fromDb == null) {
return null; return null;
} }
@@ -14,7 +14,7 @@ class MapConverter<T> extends TypeConverter<Map<String, T>, String> {
} }
@override @override
String mapToSql(value) { String mapToSql(Map<String, T> value) {
if (value == null) { if (value == null) {
return null; return null;
} }
@@ -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/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/channels.dart'; import 'package:stream_chat_persistence/src/entity/channels.dart';
import 'package:stream_chat_persistence/src/entity/users.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'; part 'channel_dao.g.dart';
@@ -15,15 +15,14 @@ class ChannelDao extends DatabaseAccessor<MoorChatDatabase>
ChannelDao(MoorChatDatabase db) : super(db); ChannelDao(MoorChatDatabase db) : super(db);
/// Get channel by cid /// Get channel by cid
Future<ChannelModel> getChannelByCid(String cid) async { Future<ChannelModel> getChannelByCid(String cid) async =>
return (select(channels)..where((c) => c.cid.equals(cid))).join([ (select(channels)..where((c) => c.cid.equals(cid))).join([
leftOuterJoin(users, channels.createdById.equalsExp(users.id)), leftOuterJoin(users, channels.createdById.equalsExp(users.id)),
]).map((rows) { ]).map((rows) {
final channel = rows.readTable(channels); final channel = rows.readTable(channels);
final createdBy = rows.readTable(users); final createdBy = rows.readTable(users);
return channel.toChannelModel(createdBy: createdBy?.toUser()); return channel.toChannelModel(createdBy: createdBy?.toUser());
}).getSingle(); }).getSingle();
}
/// Delete all channels by matching cid in [cids] /// Delete all channels by matching cid in [cids]
/// ///
@@ -31,27 +30,22 @@ class ChannelDao extends DatabaseAccessor<MoorChatDatabase>
/// 1. Channel Reads /// 1. Channel Reads
/// 2. Channel Members /// 2. Channel Members
/// 3. Channel Messages -> Messages Reactions /// 3. Channel Messages -> Messages Reactions
Future<void> deleteChannelByCids(List<String> cids) async { Future<void> deleteChannelByCids(List<String> cids) async =>
return (delete(channels)..where((tbl) => tbl.cid.isIn(cids))).go(); (delete(channels)..where((tbl) => tbl.cid.isIn(cids))).go();
}
/// Get the channel cids saved in the storage /// Get the channel cids saved in the storage
Future<List<String>> get cids { Future<List<String>> get cids => (select(channels)
return (select(channels) ..orderBy([(c) => OrderingTerm.desc(c.lastMessageAt)])
..orderBy([(c) => OrderingTerm.desc(c.lastMessageAt)]) ..limit(250))
..limit(250)) .map((c) => c.cid)
.map((c) => c.cid) .get();
.get();
}
/// Updates all the channels using the new [channelList] data /// Updates all the channels using the new [channelList] data
Future<void> updateChannels(List<ChannelModel> channelList) { Future<void> updateChannels(List<ChannelModel> channelList) => batch(
return batch( (it) => it.insertAll(
(it) => it.insertAll( channels,
channels, channelList.map((c) => c.toEntity()).toList(),
channelList.map((c) => c.toEntity()).toList(), mode: InsertMode.insertOrReplace,
mode: InsertMode.insertOrReplace, ),
), );
);
}
} }
@@ -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/channel_queries.dart';
import 'package:stream_chat_persistence/src/entity/channels.dart'; import 'package:stream_chat_persistence/src/entity/channels.dart';
import 'package:stream_chat_persistence/src/entity/users.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'; part 'channel_query_dao.g.dart';
@@ -30,29 +31,31 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
/// the list of matching rows will be deleted /// the list of matching rows will be deleted
Future<void> updateChannelQueries( Future<void> updateChannelQueries(
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<String> cids, List<String> cids, {
bool clearQueryCache, bool clearQueryCache,
) async { }) async =>
final hash = _computeHash(filter); transaction(() async {
if (clearQueryCache) { final hash = _computeHash(filter);
await batch((it) { if (clearQueryCache) {
it.deleteWhere<ChannelQueries, ChannelQueryEntity>( await batch((it) {
channelQueries, it.deleteWhere<ChannelQueries, ChannelQueryEntity>(
(c) => c.queryHash.equals(hash), channelQueries,
); (c) => c.queryHash.equals(hash),
}); );
} });
}
return batch((it) { await batch((it) {
it.insertAll( it.insertAll(
channelQueries, channelQueries,
cids.map((cid) { cids
return ChannelQueryEntity(queryHash: hash, channelCid: cid); .map((cid) =>
}).toList(), ChannelQueryEntity(queryHash: hash, channelCid: cid))
mode: InsertMode.insertOrReplace, .toList(),
); mode: InsertMode.insertOrReplace,
}); );
} });
});
/// Get list of channels by filter, sort and paginationParams /// Get list of channels by filter, sort and paginationParams
Future<List<ChannelModel>> getChannels({ Future<List<ChannelModel>> getChannels({
@@ -67,7 +70,7 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
); );
} }
return true; return true;
}()); }(), '');
final hash = _computeHash(filter); final hash = _computeHash(filter);
final cachedChannelCids = await (select(channelQueries) final cachedChannelCids = await (select(channelQueries)
@@ -86,10 +89,11 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
})).get(); })).get();
final possibleSortingFields = cachedChannels.fold<List<String>>( final possibleSortingFields = cachedChannels.fold<List<String>>(
ChannelModel.topLevelFields, (previousValue, element) { ChannelModel.topLevelFields,
return {...previousValue, ...element.extraData.keys}.toList(); (previousValue, element) =>
}); {...previousValue, ...element.extraData.keys}.toList());
// ignore: parameter_assignments
sort = sort sort = sort
?.where((s) => possibleSortingFields.contains(s.field)) ?.where((s) => possibleSortingFields.contains(s.field))
?.toList(growable: false); ?.toList(growable: false);
@@ -117,7 +121,7 @@ class ChannelQueryDao extends DatabaseAccessor<MoorChatDatabase>
cachedChannels.sort(chainedComparator); cachedChannels.sort(chainedComparator);
if (paginationParams?.offset != null) { if (paginationParams?.offset != null && cachedChannels.isNotEmpty) {
cachedChannels.removeRange(0, paginationParams.offset); cachedChannels.removeRange(0, paginationParams.offset);
} }
@@ -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/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/connection_events.dart'; import 'package:stream_chat_persistence/src/entity/connection_events.dart';
import 'package:stream_chat_persistence/src/entity/users.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'; part 'connection_event_dao.g.dart';
@@ -15,40 +16,38 @@ class ConnectionEventDao extends DatabaseAccessor<MoorChatDatabase>
ConnectionEventDao(MoorChatDatabase db) : super(db); ConnectionEventDao(MoorChatDatabase db) : super(db);
/// Get the latest stored connection event /// Get the latest stored connection event
Future<Event> get connectionEvent { Future<Event> get connectionEvent => select(connectionEvents)
return select(connectionEvents).map((eventEntity) { .map((eventEntity) => eventEntity.toEvent())
return eventEntity.toEvent(); .getSingle();
}).getSingle();
}
/// Get the latest stored lastSyncAt /// Get the latest stored lastSyncAt
Future<DateTime> get lastSyncAt { Future<DateTime> get lastSyncAt =>
return select(connectionEvents).getSingle().then((r) => r?.lastSyncAt); select(connectionEvents).getSingle().then((r) => r?.lastSyncAt);
}
/// Update stored connection event with latest data /// Update stored connection event with latest data
Future<int> updateConnectionEvent(Event event) async { Future<void> updateConnectionEvent(Event event) async =>
final connectionInfo = await select(connectionEvents).getSingle(); transaction(() async {
return into(connectionEvents).insert( final connectionInfo = await select(connectionEvents).getSingle();
ConnectionEventEntity( await into(connectionEvents).insert(
id: 1, ConnectionEventEntity(
lastSyncAt: connectionInfo?.lastSyncAt, id: 1,
lastEventAt: event.createdAt ?? connectionInfo?.lastEventAt, lastSyncAt: connectionInfo?.lastSyncAt,
totalUnreadCount: lastEventAt: event.createdAt ?? connectionInfo?.lastEventAt,
event.totalUnreadCount ?? connectionInfo?.totalUnreadCount, totalUnreadCount:
ownUser: event.me?.toJson() ?? connectionInfo?.ownUser, event.totalUnreadCount ?? connectionInfo?.totalUnreadCount,
unreadChannels: event.unreadChannels ?? connectionInfo?.unreadChannels, ownUser: event.me?.toJson() ?? connectionInfo?.ownUser,
), unreadChannels:
mode: InsertMode.insertOrReplace, event.unreadChannels ?? connectionInfo?.unreadChannels,
); ),
} mode: InsertMode.insertOrReplace,
);
});
/// Update stored lastSyncAt with latest data /// Update stored lastSyncAt with latest data
Future<int> updateLastSyncAt(DateTime lastSyncAt) async { Future<int> updateLastSyncAt(DateTime lastSyncAt) async =>
return (update(connectionEvents)..where((tbl) => tbl.id.equals(1))).write( (update(connectionEvents)..where((tbl) => tbl.id.equals(1))).write(
ConnectionEventsCompanion( ConnectionEventsCompanion(
lastSyncAt: Value(lastSyncAt), lastSyncAt: Value(lastSyncAt),
), ),
); );
}
} }
@@ -1,9 +1,9 @@
export 'user_dao.dart';
export 'channel_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 'message_dao.dart';
export 'pinned_message_dao.dart'; export 'pinned_message_dao.dart';
export 'member_dao.dart';
export 'connection_event_dao.dart';
export 'reaction_dao.dart'; export 'reaction_dao.dart';
export 'read_dao.dart'; export 'read_dao.dart';
export 'channel_query_dao.dart'; export 'user_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/members.dart';
import 'package:stream_chat_persistence/src/entity/users.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'; part 'member_dao.g.dart';
@@ -17,37 +17,33 @@ class MemberDao extends DatabaseAccessor<MoorChatDatabase>
MemberDao(MoorChatDatabase db) : super(db); MemberDao(MoorChatDatabase db) : super(db);
/// Get all members where [Members.channelCid] matches [cid] /// Get all members where [Members.channelCid] matches [cid]
Future<List<Member>> getMembersByCid(String cid) async { Future<List<Member>> getMembersByCid(String cid) async =>
return (select(members).join([ (select(members).join([
leftOuterJoin(users, members.userId.equalsExp(users.id)), leftOuterJoin(users, members.userId.equalsExp(users.id)),
]) ])
..where(members.channelCid.equals(cid)) ..where(members.channelCid.equals(cid))
..orderBy([OrderingTerm.asc(members.createdAt)])) ..orderBy([OrderingTerm.asc(members.createdAt)]))
.map((row) { .map((row) {
final userEntity = row.readTable(users); final userEntity = row.readTable(users);
final memberEntity = row.readTable(members); final memberEntity = row.readTable(members);
return memberEntity.toMember(user: userEntity?.toUser()); return memberEntity.toMember(user: userEntity?.toUser());
}).get(); }).get();
}
/// Updates all the members using the new [memberList] data /// Updates all the members using the new [memberList] data
Future<void> updateMembers(String cid, List<Member> memberList) async { Future<void> updateMembers(String cid, List<Member> memberList) async =>
return batch( batch(
(it) => it.insertAll( (it) => it.insertAll(
members, members,
memberList.map((m) => m.toEntity(cid: cid)).toList(), memberList.map((m) => m.toEntity(cid: cid)).toList(),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrReplace,
), ),
); );
}
/// Deletes all the members whose [Members.channelCid] is present in [cids] /// Deletes all the members whose [Members.channelCid] is present in [cids]
Future<void> deleteMemberByCids(List<String> cids) async { Future<void> deleteMemberByCids(List<String> cids) async => batch((it) {
return batch((it) { it.deleteWhere<Members, MemberEntity>(
it.deleteWhere<Members, MemberEntity>( members,
members, (m) => m.channelCid.isIn(cids),
(m) => m.channelCid.isIn(cids), );
); });
});
}
} }
@@ -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/messages.dart';
import 'package:stream_chat_persistence/src/entity/users.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'; part 'message_dao.g.dart';
@@ -25,17 +25,15 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
/// ///
/// This will automatically delete the following linked records /// This will automatically delete the following linked records
/// 1. Message Reactions /// 1. Message Reactions
Future<void> deleteMessageByIds(List<String> messageIds) { Future<void> deleteMessageByIds(List<String> messageIds) =>
return (delete(messages)..where((tbl) => tbl.id.isIn(messageIds))).go(); (delete(messages)..where((tbl) => tbl.id.isIn(messageIds))).go();
}
/// Removes all the messages by matching [Messages.channelCid] in [cids] /// Removes all the messages by matching [Messages.channelCid] in [cids]
/// ///
/// This will automatically delete the following linked records /// This will automatically delete the following linked records
/// 1. Message Reactions /// 1. Message Reactions
Future<void> deleteMessageByCids(List<String> cids) async { Future<void> deleteMessageByCids(List<String> cids) async =>
return (delete(messages)..where((tbl) => tbl.channelCid.isIn(cids))).go(); (delete(messages)..where((tbl) => tbl.channelCid.isIn(cids))).go();
}
Future<Message> _messageFromJoinRow(TypedResult rows) async { Future<Message> _messageFromJoinRow(TypedResult rows) async {
final userEntity = rows.readTable(_users); final userEntity = rows.readTable(_users);
@@ -60,31 +58,29 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
} }
/// Returns a single message by matching the [Messages.id] with [id] /// Returns a single message by matching the [Messages.id] with [id]
Future<Message> getMessageById(String id) async { Future<Message> getMessageById(String id) async =>
return await (select(messages).join([ await (select(messages).join([
leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), leftOuterJoin(_users, messages.userId.equalsExp(_users.id)),
leftOuterJoin( leftOuterJoin(_pinnedByUsers,
_pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
]) ])
..where(messages.id.equals(id))) ..where(messages.id.equals(id)))
.map(_messageFromJoinRow) .map(_messageFromJoinRow)
.getSingle(); .getSingle();
}
/// Returns all the messages of a particular thread by matching /// Returns all the messages of a particular thread by matching
/// [Messages.channelCid] with [cid] /// [Messages.channelCid] with [cid]
Future<List<Message>> getThreadMessages(String cid) async { Future<List<Message>> getThreadMessages(String cid) async =>
return Future.wait(await (select(messages).join([ Future.wait(await (select(messages).join([
leftOuterJoin(users, messages.userId.equalsExp(_users.id)), leftOuterJoin(users, messages.userId.equalsExp(_users.id)),
leftOuterJoin( leftOuterJoin(_pinnedByUsers,
_pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), messages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
]) ])
..where(messages.channelCid.equals(cid)) ..where(messages.channelCid.equals(cid))
..where(isNotNull(messages.parentId)) ..where(isNotNull(messages.parentId))
..orderBy([OrderingTerm.asc(messages.createdAt)])) ..orderBy([OrderingTerm.asc(messages.createdAt)]))
.map(_messageFromJoinRow) .map(_messageFromJoinRow)
.get()); .get());
}
/// Returns all the messages of a particular thread by matching /// Returns all the messages of a particular thread by matching
/// [Messages.parentId] with [parentId] /// [Messages.parentId] with [parentId]
@@ -102,7 +98,7 @@ class MessageDao extends DatabaseAccessor<MoorChatDatabase>
.map(_messageFromJoinRow) .map(_messageFromJoinRow)
.get()); .get());
if (options?.lessThan != null) { if (options?.lessThan != null && msgList.isNotEmpty) {
final lessThanIndex = msgList.indexWhere((m) => m.id == options.lessThan); final lessThanIndex = msgList.indexWhere((m) => m.id == options.lessThan);
msgList.removeRange(lessThanIndex, msgList.length); msgList.removeRange(lessThanIndex, msgList.length);
} }
@@ -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/pinned_messages.dart';
import 'package:stream_chat_persistence/src/entity/users.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'; part 'pinned_message_dao.g.dart';
@@ -25,19 +25,15 @@ class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
/// ///
/// This will automatically delete the following linked records /// This will automatically delete the following linked records
/// 1. Message Reactions /// 1. Message Reactions
Future<void> deleteMessageByIds(List<String> messageIds) { Future<void> deleteMessageByIds(List<String> messageIds) =>
return (delete(pinnedMessages)..where((tbl) => tbl.id.isIn(messageIds))) (delete(pinnedMessages)..where((tbl) => tbl.id.isIn(messageIds))).go();
.go();
}
/// Removes all the messages by matching [PinnedMessages.channelCid] in [cids] /// Removes all the messages by matching [PinnedMessages.channelCid] in [cids]
/// ///
/// This will automatically delete the following linked records /// This will automatically delete the following linked records
/// 1. Message Reactions /// 1. Message Reactions
Future<void> deleteMessageByCids(List<String> cids) async { Future<void> deleteMessageByCids(List<String> cids) async =>
return (delete(pinnedMessages)..where((tbl) => tbl.channelCid.isIn(cids))) (delete(pinnedMessages)..where((tbl) => tbl.channelCid.isIn(cids))).go();
.go();
}
Future<Message> _messageFromJoinRow(TypedResult rows) async { Future<Message> _messageFromJoinRow(TypedResult rows) async {
final userEntity = rows.readTable(users); final userEntity = rows.readTable(users);
@@ -62,31 +58,29 @@ class PinnedMessageDao extends DatabaseAccessor<MoorChatDatabase>
} }
/// Returns a single message by matching the [PinnedMessages.id] with [id] /// Returns a single message by matching the [PinnedMessages.id] with [id]
Future<Message> getMessageById(String id) async { Future<Message> getMessageById(String id) async =>
return await (select(pinnedMessages).join([ await (select(pinnedMessages).join([
leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers, leftOuterJoin(_pinnedByUsers,
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
]) ])
..where(pinnedMessages.id.equals(id))) ..where(pinnedMessages.id.equals(id)))
.map(_messageFromJoinRow) .map(_messageFromJoinRow)
.getSingle(); .getSingle();
}
/// Returns all the messages of a particular thread by matching /// Returns all the messages of a particular thread by matching
/// [PinnedMessages.channelCid] with [cid] /// [PinnedMessages.channelCid] with [cid]
Future<List<Message>> getThreadMessages(String cid) async { Future<List<Message>> getThreadMessages(String cid) async =>
return Future.wait(await (select(pinnedMessages).join([ Future.wait(await (select(pinnedMessages).join([
leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
leftOuterJoin(_pinnedByUsers, leftOuterJoin(_pinnedByUsers,
pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)), pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id)),
]) ])
..where(pinnedMessages.channelCid.equals(cid)) ..where(pinnedMessages.channelCid.equals(cid))
..where(isNotNull(pinnedMessages.parentId)) ..where(isNotNull(pinnedMessages.parentId))
..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)])) ..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)]))
.map(_messageFromJoinRow) .map(_messageFromJoinRow)
.get()); .get());
}
/// Returns all the messages of a particular thread by matching /// Returns all the messages of a particular thread by matching
/// [PinnedMessages.parentId] with [parentId] /// [PinnedMessages.parentId] with [parentId]
@@ -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/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/reactions.dart'; import 'package:stream_chat_persistence/src/entity/reactions.dart';
import 'package:stream_chat_persistence/src/entity/users.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'; part 'reaction_dao.g.dart';
@@ -16,18 +16,17 @@ class ReactionDao extends DatabaseAccessor<MoorChatDatabase>
/// Returns all the reactions of a particular message by matching /// Returns all the reactions of a particular message by matching
/// [Reactions.messageId] with [messageId] /// [Reactions.messageId] with [messageId]
Future<List<Reaction>> getReactions(String messageId) { Future<List<Reaction>> getReactions(String messageId) =>
return (select(reactions).join([ (select(reactions).join([
leftOuterJoin(users, reactions.userId.equalsExp(users.id)), leftOuterJoin(users, reactions.userId.equalsExp(users.id)),
]) ])
..where(reactions.messageId.equals(messageId)) ..where(reactions.messageId.equals(messageId))
..orderBy([OrderingTerm.asc(reactions.createdAt)])) ..orderBy([OrderingTerm.asc(reactions.createdAt)]))
.map((rows) { .map((rows) {
final userEntity = rows.readTable(users); final userEntity = rows.readTable(users);
final reactionEntity = rows.readTable(reactions); final reactionEntity = rows.readTable(reactions);
return reactionEntity.toReaction(user: userEntity?.toUser()); return reactionEntity.toReaction(user: userEntity?.toUser());
}).get(); }).get();
}
/// Returns all the reactions of a particular message /// Returns all the reactions of a particular message
/// added by a particular user by matching /// added by a particular user by matching
@@ -42,23 +41,21 @@ class ReactionDao extends DatabaseAccessor<MoorChatDatabase>
} }
/// Updates the reactions data with the new [reactionList] data /// Updates the reactions data with the new [reactionList] data
Future<void> updateReactions(List<Reaction> reactionList) { Future<void> updateReactions(List<Reaction> reactionList) => batch((it) {
return batch((it) { it.insertAll(
it.insertAll( reactions,
reactions, reactionList.map((r) => r.toEntity()).toList(),
reactionList.map((r) => r.toEntity()).toList(), mode: InsertMode.insertOrReplace,
mode: InsertMode.insertOrReplace, );
); });
});
}
/// Deletes all the reactions whose [Reactions.messageId] is present in [messageIds] /// Deletes all the reactions whose [Reactions.messageId] is
Future<void> deleteReactionsByMessageIds(List<String> messageIds) { /// present in [messageIds]
return batch((it) { Future<void> deleteReactionsByMessageIds(List<String> messageIds) =>
it.deleteWhere<Reactions, ReactionEntity>( batch((it) {
reactions, it.deleteWhere<Reactions, ReactionEntity>(
(r) => r.messageId.isIn(messageIds), reactions,
); (r) => r.messageId.isIn(messageIds),
}); );
} });
} }
@@ -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/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/reads.dart'; import 'package:stream_chat_persistence/src/entity/reads.dart';
import 'package:stream_chat_persistence/src/entity/users.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'; part 'read_dao.g.dart';
@@ -14,30 +14,26 @@ class ReadDao extends DatabaseAccessor<MoorChatDatabase> with _$ReadDaoMixin {
ReadDao(MoorChatDatabase db) : super(db); ReadDao(MoorChatDatabase db) : super(db);
/// Get all reads where [Reads.channelCid] matches [cid] /// Get all reads where [Reads.channelCid] matches [cid]
Future<List<Read>> getReadsByCid(String cid) async { Future<List<Read>> getReadsByCid(String cid) async => (select(reads).join([
return (select(reads).join([ leftOuterJoin(users, reads.userId.equalsExp(users.id)),
leftOuterJoin(users, reads.userId.equalsExp(users.id)), ])
]) ..where(reads.channelCid.equals(cid))
..where(reads.channelCid.equals(cid)) ..orderBy([
..orderBy([ OrderingTerm.asc(reads.lastRead),
OrderingTerm.asc(reads.lastRead), ]))
])) .map((row) {
.map((row) { final userEntity = row.readTable(users);
final userEntity = row.readTable(users); final readEntity = row.readTable(reads);
final readEntity = row.readTable(reads); return readEntity.toRead(user: userEntity?.toUser());
return readEntity.toRead(user: userEntity?.toUser()); }).get();
}).get();
}
/// Updates the read data of a particular channel with /// Updates the read data of a particular channel with
/// the new [readList] data /// the new [readList] data
Future<void> updateReads(String cid, List<Read> readList) { Future<void> updateReads(String cid, List<Read> readList) => batch(
return batch( (it) => it.insertAll(
(it) => it.insertAll( reads,
reads, readList.map((r) => r.toEntity(cid: cid)).toList(),
readList.map((r) => r.toEntity(cid: cid)).toList(), mode: InsertMode.insertOrReplace,
mode: InsertMode.insertOrReplace, ),
), );
);
}
} }
@@ -2,7 +2,7 @@ import 'package:moor/moor.dart';
import 'package:stream_chat/stream_chat.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/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/entity/users.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'; part 'user_dao.g.dart';
@@ -13,13 +13,11 @@ class UserDao extends DatabaseAccessor<MoorChatDatabase> with _$UserDaoMixin {
UserDao(MoorChatDatabase db) : super(db); UserDao(MoorChatDatabase db) : super(db);
/// Updates the users data with the new [userList] data /// Updates the users data with the new [userList] data
Future<void> updateUsers(List<User> userList) { Future<void> updateUsers(List<User> userList) => batch(
return batch( (it) => it.insertAll(
(it) => it.insertAll( users,
users, userList.map((u) => u.toEntity()).toList(),
userList.map((u) => u.toEntity()).toList(), mode: InsertMode.insertOrReplace,
mode: InsertMode.insertOrReplace, ),
), );
);
}
} }
@@ -1,25 +1,22 @@
import 'package:moor/isolate.dart';
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/converter/converter.dart';
import '../entity/entity.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart';
import '../dao/dao.dart'; import 'package:stream_chat_persistence/src/db/shared/shared_db.dart';
import '../converter/converter.dart'; import 'package:stream_chat_persistence/src/entity/entity.dart';
import 'shared/shared_db.dart';
part 'moor_chat_database.g.dart'; part 'moor_chat_database.g.dart';
LazyDatabase _openConnection( LazyDatabase _openConnection(
String userId, { String userId, {
logStatements = false, bool logStatements = false,
}) { bool persistOnDisk = true,
return LazyDatabase(() async { }) =>
return await SharedDB.constructDatabase( LazyDatabase(() async => SharedDB.constructDatabase(
userId, userId,
logStatements: logStatements, logStatements: logStatements,
); persistOnDisk: persistOnDisk,
}); ));
}
/// A chat database implemented using moor /// A chat database implemented using moor
@UseMoor(tables: [ @UseMoor(tables: [
@@ -48,15 +45,16 @@ class MoorChatDatabase extends _$MoorChatDatabase {
MoorChatDatabase( MoorChatDatabase(
this._userId, { this._userId, {
logStatements = false, logStatements = false,
bool persistOnDisk = true,
}) : super(_openConnection( }) : super(_openConnection(
_userId, _userId,
logStatements: logStatements, logStatements: logStatements,
persistOnDisk: persistOnDisk,
)); ));
/// Instantiate a new database instance /// Instantiate a new database instance
MoorChatDatabase.connect( MoorChatDatabase.connect(
this._userId, this._userId,
this._isolate,
DatabaseConnection connection, DatabaseConnection connection,
) : super.connect(connection); ) : super.connect(connection);
@@ -65,8 +63,6 @@ class MoorChatDatabase extends _$MoorChatDatabase {
/// User id to which the database is connected /// User id to which the database is connected
String get userId => _userId; String get userId => _userId;
MoorIsolate _isolate;
// you should bump this number whenever you change or add a table definition. // you should bump this number whenever you change or add a table definition.
@override @override
int get schemaVersion => 2; int get schemaVersion => 2;
@@ -85,8 +81,5 @@ class MoorChatDatabase extends _$MoorChatDatabase {
); );
/// Closes the database instance /// Closes the database instance
Future<void> disconnect() async { Future<void> disconnect() => close();
await _isolate?.shutdownAll();
await close();
}
} }
@@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'package:moor/ffi.dart'; import 'package:moor/ffi.dart';
import 'package:moor/isolate.dart'; import 'package:moor/isolate.dart';
import 'package:moor/moor.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/src/stream_chat_persistence_client.dart';
import 'package:stream_chat_persistence/stream_chat_persistence.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 /// A Helper class to construct new instances of [MoorChatDatabase] specifically
/// for native platform applications /// for native platform applications
@@ -20,28 +21,29 @@ class SharedDB {
static Future<VmDatabase> constructDatabase( static Future<VmDatabase> constructDatabase(
String userId, { String userId, {
bool logStatements = false, bool logStatements = false,
bool persistOnDisk = true,
}) async { }) async {
final dbName = 'db_$userId'; final dbName = 'db_$userId';
if (Platform.isIOS || Platform.isAndroid) { if (persistOnDisk) {
final dir = await getApplicationDocumentsDirectory(); if (Platform.isIOS || Platform.isAndroid) {
final path = join(dir.path, '$dbName.sqlite'); final dir = await getApplicationDocumentsDirectory();
final file = File(path); final path = join(dir.path, '$dbName.sqlite');
return VmDatabase(file, logStatements: logStatements); final file = File(path);
} return VmDatabase(file, logStatements: logStatements);
if (Platform.isMacOS || Platform.isLinux) { }
final file = File('$dbName.sqlite'); if (Platform.isMacOS || Platform.isLinux) {
return VmDatabase(file, logStatements: logStatements); final file = File('$dbName.sqlite');
return VmDatabase(file, logStatements: logStatements);
}
} }
return VmDatabase.memory(logStatements: logStatements); return VmDatabase.memory(logStatements: logStatements);
} }
static void _startBackground(_IsolateStartRequest request) { static void _startBackground(_IsolateStartRequest request) {
final executor = LazyDatabase(() async { final executor = LazyDatabase(() async => VmDatabase(
return VmDatabase( File(request.targetPath),
File(request.targetPath), logStatements: request.logStatements,
logStatements: request.logStatements, ));
);
});
final moorIsolate = MoorIsolate.inCurrent( final moorIsolate = MoorIsolate.inCurrent(
() => DatabaseConnection.fromExecutor(executor), () => 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 /// Returns a new instance of [MoorChatDatabase] using the factory constructor
/// [MoorChatDatabase.connect] created on a background isolate. /// [MoorChatDatabase.connect] created on a background isolate.
/// ///
/// Generally used with [ConnectionMode.background]. /// Generally used with [ConnectionMode.background].
static Future<MoorChatDatabase> constructMoorChatDatabase( static MoorChatDatabase constructMoorChatDatabase(
String userId, { String userId, {
bool logStatements = false, bool logStatements = false,
}) async { }) {
final dbName = 'db_$userId'; final dbName = 'db_$userId';
final isolate = await _createMoorIsolate( return MoorChatDatabase.connect(
dbName, userId,
logStatements: logStatements, 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 { class _IsolateStartRequest {
final SendPort sendMoorIsolate;
final String targetPath;
final bool logStatements;
const _IsolateStartRequest( const _IsolateStartRequest(
this.sendMoorIsolate, this.sendMoorIsolate,
this.targetPath, { this.targetPath, {
this.logStatements = false, this.logStatements = false,
}); });
final SendPort sendMoorIsolate;
final String targetPath;
final bool logStatements;
} }
@@ -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'; import 'package:stream_chat_persistence/stream_chat_persistence.dart';
/// A Helper class to construct new instances of [MoorChatDatabase] /// A Helper class to construct new instances of [MoorChatDatabase]
@@ -5,20 +7,23 @@ class SharedDB {
/// Returns a new instance of database. /// Returns a new instance of database.
/// ///
/// Generally used with [ConnectionMode.regular]. /// Generally used with [ConnectionMode.regular].
static dynamic constructDatabase( static Future<DelegatedDatabase> constructDatabase(
String userId, { String userId, {
bool logStatements = false, 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. /// Return a new instance of moor chat database.
/// ///
/// Generally used with [ConnectionMode.background]. /// Generally used with [ConnectionMode.background].
static dynamic constructMoorChatDatabase( static MoorChatDatabase constructMoorChatDatabase(
String userId, { String userId, {
bool logStatements = false, bool logStatements = false,
}) { }) {
throw 'Unsupported Platform'; throw UnsupportedError(
'No implementation of the constructMoorChatDatabase api provided');
} }
} }
@@ -1,7 +1,7 @@
import 'package:moor/moor_web.dart'; import 'package:moor/moor_web.dart';
import 'package:stream_chat_persistence/src/stream_chat_persistence_client.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 /// A Helper class to construct new instances of [MoorChatDatabase] specifically
/// for Web applications /// for Web applications
@@ -12,6 +12,7 @@ class SharedDB {
static Future<WebDatabase> constructDatabase( static Future<WebDatabase> constructDatabase(
String userId, { String userId, {
bool logStatements = false, bool logStatements = false,
bool persistOnDisk = true, // ignored on web
}) async { }) async {
final dbName = 'db_$userId'; final dbName = 'db_$userId';
return WebDatabase(dbName, logStatements: logStatements); return WebDatabase(dbName, logStatements: logStatements);
@@ -21,10 +22,10 @@ class SharedDB {
/// default constructor. /// default constructor.
/// ///
/// Generally used with [ConnectionMode.background]. /// Generally used with [ConnectionMode.background].
static Future<MoorChatDatabase> constructMoorChatDatabase( static MoorChatDatabase constructMoorChatDatabase(
String userId, { String userId, {
bool logStatements = false, bool logStatements = false,
}) async { }) {
final dbName = 'db_$userId'; final dbName = 'db_$userId';
return MoorChatDatabase(dbName, logStatements: logStatements); return MoorChatDatabase(dbName, logStatements: logStatements);
} }
@@ -17,7 +17,7 @@ class Channels extends Table {
TextColumn get config => text().map(MapConverter<Object>())(); TextColumn get config => text().map(MapConverter<Object>())();
/// True if this channel entity is frozen /// 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 /// The date of the last message
DateTimeColumn get lastMessageAt => dateTime().nullable()(); DateTimeColumn get lastMessageAt => dateTime().nullable()();
@@ -1,9 +1,9 @@
export 'channel_queries.dart';
export 'channels.dart'; export 'channels.dart';
export 'connection_events.dart';
export 'members.dart';
export 'messages.dart'; export 'messages.dart';
export 'pinned_messages.dart'; export 'pinned_messages.dart';
export 'reactions.dart'; export 'reactions.dart';
export 'users.dart';
export 'members.dart';
export 'reads.dart'; export 'reads.dart';
export 'channel_queries.dart'; export 'users.dart';
export 'connection_events.dart';
@@ -1,6 +1,6 @@
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
import 'messages.dart'; import 'package:stream_chat_persistence/src/entity/messages.dart';
/// Represents a [PinnedMessages] table in [MoorChatDatabase]. /// Represents a [PinnedMessages] table in [MoorChatDatabase].
@DataClassName('PinnedMessageEntity') @DataClassName('PinnedMessageEntity')
@@ -29,34 +29,31 @@ extension ChannelEntityX on ChannelEntity {
List<Read> reads, List<Read> reads,
List<Message> messages, List<Message> messages,
List<Message> pinnedMessages, List<Message> pinnedMessages,
}) { }) =>
return ChannelState( ChannelState(
members: members, members: members,
read: reads, read: reads,
messages: messages, messages: messages,
pinnedMessages: pinnedMessages, pinnedMessages: pinnedMessages,
channel: toChannelModel(createdBy: createdBy), channel: toChannelModel(createdBy: createdBy),
); );
}
} }
/// Useful mapping functions for [ChannelModel] /// Useful mapping functions for [ChannelModel]
extension ChannelModelX on ChannelModel { extension ChannelModelX on ChannelModel {
/// Maps a [ChannelModel] into [ChannelEntity] /// Maps a [ChannelModel] into [ChannelEntity]
ChannelEntity toEntity() { ChannelEntity toEntity() => ChannelEntity(
return ChannelEntity( id: id,
id: id, type: type,
type: type, cid: cid,
cid: cid, config: config.toJson(),
config: config.toJson(), frozen: frozen,
frozen: frozen, lastMessageAt: lastMessageAt,
lastMessageAt: lastMessageAt, createdAt: createdAt,
createdAt: createdAt, updatedAt: updatedAt,
updatedAt: updatedAt, deletedAt: deletedAt,
deletedAt: deletedAt, memberCount: memberCount,
memberCount: memberCount, createdById: createdBy.id,
createdById: createdBy.id, extraData: extraData,
extraData: extraData, );
);
}
} }
@@ -4,11 +4,9 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
/// Useful mapping functions for [ConnectionEventEntity] /// Useful mapping functions for [ConnectionEventEntity]
extension ConnectionEventX on ConnectionEventEntity { extension ConnectionEventX on ConnectionEventEntity {
/// Maps a [ConnectionEventEntity] into [Event] /// Maps a [ConnectionEventEntity] into [Event]
Event toEvent() { Event toEvent() => Event(
return Event( me: ownUser != null ? OwnUser.fromJson(ownUser) : null,
me: ownUser != null ? OwnUser.fromJson(ownUser) : null, totalUnreadCount: totalUnreadCount,
totalUnreadCount: totalUnreadCount, unreadChannels: unreadChannels,
unreadChannels: unreadChannels, );
);
}
} }
@@ -1,8 +1,8 @@
export 'user_mapper.dart';
export 'reaction_mapper.dart';
export 'channel_mapper.dart'; export 'channel_mapper.dart';
export 'event_mapper.dart'; export 'event_mapper.dart';
export 'member_mapper.dart'; export 'member_mapper.dart';
export 'read_mapper.dart';
export 'message_mapper.dart'; export 'message_mapper.dart';
export 'pinned_message_mapper.dart'; export 'pinned_message_mapper.dart';
export 'reaction_mapper.dart';
export 'read_mapper.dart';
export 'user_mapper.dart';
@@ -4,39 +4,35 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
/// Useful mapping functions for [MemberEntity] /// Useful mapping functions for [MemberEntity]
extension MemberEntityX on MemberEntity { extension MemberEntityX on MemberEntity {
/// Maps a [MemberEntity] into [Member] /// Maps a [MemberEntity] into [Member]
Member toMember({User user}) { Member toMember({User user}) => Member(
return Member( user: user,
user: user, userId: userId,
userId: userId, banned: banned,
banned: banned, shadowBanned: shadowBanned,
shadowBanned: shadowBanned, updatedAt: updatedAt,
updatedAt: updatedAt, createdAt: createdAt,
createdAt: createdAt, role: role,
role: role, inviteAcceptedAt: inviteAcceptedAt,
inviteAcceptedAt: inviteAcceptedAt, invited: invited,
invited: invited, inviteRejectedAt: inviteRejectedAt,
inviteRejectedAt: inviteRejectedAt, isModerator: isModerator,
isModerator: isModerator, );
);
}
} }
/// Useful mapping functions for [Member] /// Useful mapping functions for [Member]
extension MemberX on Member { extension MemberX on Member {
/// Maps a [Member] into [MemberEntity] /// Maps a [Member] into [MemberEntity]
MemberEntity toEntity({String cid}) { MemberEntity toEntity({String cid}) => MemberEntity(
return MemberEntity( userId: user?.id,
userId: user?.id, banned: banned,
banned: banned, shadowBanned: shadowBanned,
shadowBanned: shadowBanned, channelCid: cid,
channelCid: cid, createdAt: createdAt,
createdAt: createdAt, isModerator: isModerator,
isModerator: isModerator, inviteRejectedAt: inviteRejectedAt,
inviteRejectedAt: inviteRejectedAt, invited: invited,
invited: invited, inviteAcceptedAt: inviteAcceptedAt,
inviteAcceptedAt: inviteAcceptedAt, role: role,
role: role, updatedAt: updatedAt,
updatedAt: updatedAt, );
);
}
} }
@@ -12,70 +12,66 @@ extension MessageEntityX on MessageEntity {
List<Reaction> latestReactions, List<Reaction> latestReactions,
List<Reaction> ownReactions, List<Reaction> ownReactions,
Message quotedMessage, Message quotedMessage,
}) { }) =>
return Message( Message(
shadowed: shadowed, shadowed: shadowed,
latestReactions: latestReactions, latestReactions: latestReactions,
ownReactions: ownReactions, ownReactions: ownReactions,
attachments: attachments?.map((it) { attachments: attachments?.map((it) {
final json = jsonDecode(it); final json = jsonDecode(it);
return Attachment.fromData(json); return Attachment.fromData(json);
})?.toList(), })?.toList(),
createdAt: createdAt, createdAt: createdAt,
extraData: extraData, extraData: extraData,
updatedAt: updatedAt, updatedAt: updatedAt,
id: id, id: id,
type: type, type: type,
status: status, status: status,
command: command, command: command,
parentId: parentId, parentId: parentId,
quotedMessageId: quotedMessageId, quotedMessageId: quotedMessageId,
quotedMessage: quotedMessage, quotedMessage: quotedMessage,
reactionCounts: reactionCounts, reactionCounts: reactionCounts,
reactionScores: reactionScores, reactionScores: reactionScores,
replyCount: replyCount, replyCount: replyCount,
showInChannel: showInChannel, showInChannel: showInChannel,
text: messageText, text: messageText,
user: user, user: user,
deletedAt: deletedAt, deletedAt: deletedAt,
pinned: pinned, pinned: pinned,
pinnedAt: pinnedAt, pinnedAt: pinnedAt,
pinExpires: pinExpires, pinExpires: pinExpires,
pinnedBy: pinnedBy, pinnedBy: pinnedBy,
); );
}
} }
/// Useful mapping functions for [Message] /// Useful mapping functions for [Message]
extension MessageX on Message { extension MessageX on Message {
/// Maps a [Message] into [MessageEntity] /// Maps a [Message] into [MessageEntity]
MessageEntity toEntity({String cid}) { MessageEntity toEntity({String cid}) => MessageEntity(
return MessageEntity( id: id,
id: id, attachments:
attachments: attachments?.map((it) { attachments?.map((it) => jsonEncode(it.toData()))?.toList(),
return jsonEncode(it.toData()); channelCid: cid,
})?.toList(), type: type,
channelCid: cid, parentId: parentId,
type: type, quotedMessageId: quotedMessageId,
parentId: parentId, command: command,
quotedMessageId: quotedMessageId, createdAt: createdAt,
command: command, shadowed: shadowed,
createdAt: createdAt, showInChannel: showInChannel,
shadowed: shadowed, replyCount: replyCount,
showInChannel: showInChannel, reactionScores: reactionScores,
replyCount: replyCount, reactionCounts: reactionCounts,
reactionScores: reactionScores, status: status,
reactionCounts: reactionCounts, updatedAt: updatedAt,
status: status, extraData: extraData,
updatedAt: updatedAt, userId: user?.id,
extraData: extraData, deletedAt: deletedAt,
userId: user?.id, messageText: text,
deletedAt: deletedAt, pinned: pinned,
messageText: text, pinnedAt: pinnedAt,
pinned: pinned, pinExpires: pinExpires,
pinnedAt: pinnedAt, pinnedByUserId: pinnedBy?.id,
pinExpires: pinExpires, );
pinnedByUserId: pinnedBy?.id,
);
}
} }
@@ -12,70 +12,66 @@ extension PinnedMessageEntityX on PinnedMessageEntity {
List<Reaction> latestReactions, List<Reaction> latestReactions,
List<Reaction> ownReactions, List<Reaction> ownReactions,
Message quotedMessage, Message quotedMessage,
}) { }) =>
return Message( Message(
shadowed: shadowed, shadowed: shadowed,
latestReactions: latestReactions, latestReactions: latestReactions,
ownReactions: ownReactions, ownReactions: ownReactions,
attachments: attachments?.map((it) { attachments: attachments?.map((it) {
final json = jsonDecode(it); final json = jsonDecode(it);
return Attachment.fromData(json); return Attachment.fromData(json);
})?.toList(), })?.toList(),
createdAt: createdAt, createdAt: createdAt,
extraData: extraData, extraData: extraData,
updatedAt: updatedAt, updatedAt: updatedAt,
id: id, id: id,
type: type, type: type,
status: status, status: status,
command: command, command: command,
parentId: parentId, parentId: parentId,
quotedMessageId: quotedMessageId, quotedMessageId: quotedMessageId,
quotedMessage: quotedMessage, quotedMessage: quotedMessage,
reactionCounts: reactionCounts, reactionCounts: reactionCounts,
reactionScores: reactionScores, reactionScores: reactionScores,
replyCount: replyCount, replyCount: replyCount,
showInChannel: showInChannel, showInChannel: showInChannel,
text: messageText, text: messageText,
user: user, user: user,
deletedAt: deletedAt, deletedAt: deletedAt,
pinned: pinned, pinned: pinned,
pinnedAt: pinnedAt, pinnedAt: pinnedAt,
pinExpires: pinExpires, pinExpires: pinExpires,
pinnedBy: pinnedBy, pinnedBy: pinnedBy,
); );
}
} }
/// Useful mapping functions for [Message] /// Useful mapping functions for [Message]
extension PMessageX on Message { extension PMessageX on Message {
/// Maps a [Message] into [PinnedMessageEntity] /// Maps a [Message] into [PinnedMessageEntity]
PinnedMessageEntity toPinnedEntity({String cid}) { PinnedMessageEntity toPinnedEntity({String cid}) => PinnedMessageEntity(
return PinnedMessageEntity( id: id,
id: id, attachments:
attachments: attachments?.map((it) { attachments?.map((it) => jsonEncode(it.toData()))?.toList(),
return jsonEncode(it.toData()); channelCid: cid,
})?.toList(), type: type,
channelCid: cid, parentId: parentId,
type: type, quotedMessageId: quotedMessageId,
parentId: parentId, command: command,
quotedMessageId: quotedMessageId, createdAt: createdAt,
command: command, shadowed: shadowed,
createdAt: createdAt, showInChannel: showInChannel,
shadowed: shadowed, replyCount: replyCount,
showInChannel: showInChannel, reactionScores: reactionScores,
replyCount: replyCount, reactionCounts: reactionCounts,
reactionScores: reactionScores, status: status,
reactionCounts: reactionCounts, updatedAt: updatedAt,
status: status, extraData: extraData,
updatedAt: updatedAt, userId: user?.id,
extraData: extraData, deletedAt: deletedAt,
userId: user?.id, messageText: text,
deletedAt: deletedAt, pinned: pinned,
messageText: text, pinnedAt: pinnedAt,
pinned: pinned, pinExpires: pinExpires,
pinnedAt: pinnedAt, pinnedByUserId: pinnedBy?.id,
pinExpires: pinExpires, );
pinnedByUserId: pinnedBy?.id,
);
}
} }
@@ -4,30 +4,26 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
/// Useful mapping functions for [ReactionEntity] /// Useful mapping functions for [ReactionEntity]
extension ReactionEntityX on ReactionEntity { extension ReactionEntityX on ReactionEntity {
/// Maps a [ReactionEntity] into [Reaction] /// Maps a [ReactionEntity] into [Reaction]
Reaction toReaction({User user}) { Reaction toReaction({User user}) => Reaction(
return Reaction( extraData: extraData,
extraData: extraData, type: type,
type: type, createdAt: createdAt,
createdAt: createdAt, userId: userId,
userId: userId, user: user,
user: user, messageId: messageId,
messageId: messageId, score: score,
score: score, );
);
}
} }
/// Useful mapping functions for [Reaction] /// Useful mapping functions for [Reaction]
extension ReactionX on Reaction { extension ReactionX on Reaction {
/// Maps a [Reaction] into [ReactionEntity] /// Maps a [Reaction] into [ReactionEntity]
ReactionEntity toEntity() { ReactionEntity toEntity() => ReactionEntity(
return ReactionEntity( extraData: extraData,
extraData: extraData, type: type,
type: type, createdAt: createdAt,
createdAt: createdAt, userId: userId,
userId: userId, messageId: messageId,
messageId: messageId, score: score,
score: score, );
);
}
} }
@@ -4,24 +4,20 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
/// Useful mapping functions for [ReadEntity] /// Useful mapping functions for [ReadEntity]
extension ReadEntityX on ReadEntity { extension ReadEntityX on ReadEntity {
/// Maps a [ReadEntity] into [Read] /// Maps a [ReadEntity] into [Read]
Read toRead({User user}) { Read toRead({User user}) => Read(
return Read( user: user,
user: user, lastRead: lastRead,
lastRead: lastRead, unreadMessages: unreadMessages,
unreadMessages: unreadMessages, );
);
}
} }
/// Useful mapping functions for [Read] /// Useful mapping functions for [Read]
extension ReadX on Read { extension ReadX on Read {
/// Maps a [Read] into [ReadEntity] /// Maps a [Read] into [ReadEntity]
ReadEntity toEntity({String cid}) { ReadEntity toEntity({String cid}) => ReadEntity(
return ReadEntity( lastRead: lastRead,
lastRead: lastRead, userId: user?.id,
userId: user?.id, channelCid: cid,
channelCid: cid, unreadMessages: unreadMessages,
unreadMessages: unreadMessages, );
);
}
} }
@@ -4,33 +4,29 @@ import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
/// Useful mapping functions for [UserEntity] /// Useful mapping functions for [UserEntity]
extension UserEntityX on UserEntity { extension UserEntityX on UserEntity {
/// Maps a [UserEntity] into [User] /// Maps a [UserEntity] into [User]
User toUser() { User toUser() => User(
return User( id: id,
id: id, updatedAt: updatedAt,
updatedAt: updatedAt, role: role,
role: role, online: online,
online: online, lastActive: lastActive,
lastActive: lastActive, extraData: extraData,
extraData: extraData, banned: banned,
banned: banned, createdAt: createdAt,
createdAt: createdAt, );
);
}
} }
/// Useful mapping functions for [User] /// Useful mapping functions for [User]
extension UserX on User { extension UserX on User {
/// Maps a [User] into [UserEntity] /// Maps a [User] into [UserEntity]
UserEntity toEntity() { UserEntity toEntity() => UserEntity(
return UserEntity( id: id,
id: id, role: role,
role: role, createdAt: createdAt,
createdAt: createdAt, updatedAt: updatedAt,
updatedAt: updatedAt, lastActive: lastActive,
lastActive: lastActive, online: online,
online: online, banned: banned,
banned: banned, extraData: extraData,
extraData: extraData, );
);
}
} }
@@ -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 'package:stream_chat/stream_chat.dart';
import 'db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'db/shared/shared_db.dart'; import 'package:stream_chat_persistence/src/db/shared/shared_db.dart';
/// Various connection modes on which [StreamChatPersistenceClient] can work /// Various connection modes on which [StreamChatPersistenceClient] can work
enum ConnectionMode { enum ConnectionMode {
@@ -12,6 +15,12 @@ enum ConnectionMode {
background, background,
} }
final _levelEmojiMapper = {
Level.INFO: '',
Level.WARNING: '⚠️',
Level.SEVERE: '🚨',
};
/// A [MoorChatDatabase] based implementation of the [ChatPersistenceClient] /// A [MoorChatDatabase] based implementation of the [ChatPersistenceClient]
class StreamChatPersistenceClient extends ChatPersistenceClient { class StreamChatPersistenceClient extends ChatPersistenceClient {
/// Creates a new instance of the stream chat persistence client /// 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 /// Connection mode on which the client will work
ConnectionMode connectionMode = ConnectionMode.regular, ConnectionMode connectionMode = ConnectionMode.regular,
Level logLevel = Level.WARNING, Level logLevel = Level.WARNING,
}) : assert(connectionMode != null), LogHandlerFunction logHandlerFunction,
assert(logLevel != null), }) : assert(connectionMode != null, 'ConnectionMode cannot be null'),
assert(logLevel != null, 'LogLevel cannot be null'),
_connectionMode = connectionMode, _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 Logger _logger;
final ConnectionMode _connectionMode; 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<T> _readProtected<T>(Future<T> Function() f) async {
T ret;
await _mutex.protectRead(() async {
if (db == null) {
return;
}
ret = await f();
});
return ret;
}
@override @override
Future<void> connect(String userId) async { Future<void> connect(String userId) async {
if (_db != null) { if (db != null) {
throw Exception( throw Exception(
'An instance of StreamChatDatabase is already connected.\n' 'An instance of StreamChatDatabase is already connected.\n'
'disconnect the previous instance before connecting again.', 'disconnect the previous instance before connecting again.',
@@ -39,211 +95,266 @@ class StreamChatPersistenceClient extends ChatPersistenceClient {
switch (_connectionMode) { switch (_connectionMode) {
case ConnectionMode.regular: case ConnectionMode.regular:
_logger.info('Connecting on a regular isolate'); _logger.info('Connecting on a regular isolate');
_db = MoorChatDatabase(userId); db = MoorChatDatabase(userId);
return; return;
case ConnectionMode.background: case ConnectionMode.background:
_logger.info('Connecting on background isolate'); _logger.info('Connecting on background isolate');
_db = await SharedDB.constructMoorChatDatabase(userId); db = SharedDB.constructMoorChatDatabase(userId);
return; return;
} }
} }
@override @override
Future<Event> getConnectionInfo() { Future<Event> getConnectionInfo() => _readProtected(() {
return _db.connectionEventDao.connectionEvent; _logger.info('getConnectionInfo');
} return db.connectionEventDao.connectionEvent;
});
@override @override
Future<void> updateConnectionInfo(Event event) { Future<void> updateConnectionInfo(Event event) => _readProtected(() {
return _db.connectionEventDao.updateConnectionEvent(event); _logger.info('updateConnectionInfo');
} return db.connectionEventDao.updateConnectionEvent(event);
});
@override @override
Future<void> updateLastSyncAt(DateTime lastSyncAt) { Future<void> updateLastSyncAt(DateTime lastSyncAt) => _readProtected(() {
return _db.connectionEventDao.updateLastSyncAt(lastSyncAt); _logger.info('updateLastSyncAt');
} return db.connectionEventDao.updateLastSyncAt(lastSyncAt);
});
@override @override
Future<DateTime> getLastSyncAt() { Future<DateTime> getLastSyncAt() => _readProtected(() {
return _db.connectionEventDao.lastSyncAt; _logger.info('getLastSyncAt');
} return db.connectionEventDao.lastSyncAt;
});
@override @override
Future<void> deleteChannels(List<String> cids) { Future<void> deleteChannels(List<String> cids) => _readProtected(() {
return _db.channelDao.deleteChannelByCids(cids); _logger.info('deleteChannels');
} return db.channelDao.deleteChannelByCids(cids);
});
@override @override
Future<List<String>> getChannelCids() => _db.channelDao.cids; Future<List<String>> getChannelCids() => _readProtected(() {
_logger.info('getChannelCids');
return db.channelDao.cids;
});
@override @override
Future<void> deleteMessageByIds(List<String> messageIds) { Future<void> deleteMessageByIds(List<String> messageIds) =>
return _db.messageDao.deleteMessageByIds(messageIds); _readProtected(() {
} _logger.info('deleteMessageByIds');
return db.messageDao.deleteMessageByIds(messageIds);
});
@override @override
Future<void> deletePinnedMessageByIds(List<String> messageIds) { Future<void> deletePinnedMessageByIds(List<String> messageIds) =>
return _db.pinnedMessageDao.deleteMessageByIds(messageIds); _readProtected(() {
} _logger.info('deletePinnedMessageByIds');
return db.pinnedMessageDao.deleteMessageByIds(messageIds);
});
@override @override
Future<void> deleteMessageByCids(List<String> cids) { Future<void> deleteMessageByCids(List<String> cids) => _readProtected(() {
return _db.messageDao.deleteMessageByCids(cids); _logger.info('deleteMessageByCids');
} return db.messageDao.deleteMessageByCids(cids);
});
@override @override
Future<void> deletePinnedMessageByCids(List<String> cids) { Future<void> deletePinnedMessageByCids(List<String> cids) =>
return _db.pinnedMessageDao.deleteMessageByCids(cids); _readProtected(() {
} _logger.info('deletePinnedMessageByCids');
return db.pinnedMessageDao.deleteMessageByCids(cids);
});
@override @override
Future<List<Member>> getMembersByCid(String cid) { Future<List<Member>> getMembersByCid(String cid) => _readProtected(() {
return _db.memberDao.getMembersByCid(cid); _logger.info('getMembersByCid');
} return db.memberDao.getMembersByCid(cid);
});
@override @override
Future<ChannelModel> getChannelByCid(String cid) { Future<ChannelModel> getChannelByCid(String cid) => _readProtected(() {
return _db.channelDao.getChannelByCid(cid); _logger.info('getChannelByCid');
} return db.channelDao.getChannelByCid(cid);
});
@override @override
Future<List<Message>> getMessagesByCid( Future<List<Message>> getMessagesByCid(
String cid, { String cid, {
PaginationParams messagePagination, PaginationParams messagePagination,
}) { }) =>
return _db.messageDao.getMessagesByCid( _readProtected(() {
cid, _logger.info('getMessagesByCid');
messagePagination: messagePagination, return db.messageDao.getMessagesByCid(
); cid,
} messagePagination: messagePagination,
);
});
@override @override
Future<List<Message>> getPinnedMessagesByCid( Future<List<Message>> getPinnedMessagesByCid(
String cid, { String cid, {
PaginationParams messagePagination, PaginationParams messagePagination,
}) { }) =>
return _db.pinnedMessageDao.getMessagesByCid( _readProtected(() {
cid, _logger.info('getPinnedMessagesByCid');
messagePagination: messagePagination, return db.pinnedMessageDao.getMessagesByCid(
); cid,
} messagePagination: messagePagination,
);
});
@override @override
Future<List<Read>> getReadsByCid(String cid) { Future<List<Read>> getReadsByCid(String cid) => _readProtected(() {
return _db.readDao.getReadsByCid(cid); _logger.info('getReadsByCid');
} return db.readDao.getReadsByCid(cid);
});
@override @override
Future<Map<String, List<Message>>> getChannelThreads(String cid) async { Future<Map<String, List<Message>>> getChannelThreads(String cid) async =>
final messages = await _db.messageDao.getThreadMessages(cid); _readProtected(() async {
final messageByParentIdDictionary = <String, List<Message>>{}; _logger.info('getChannelThreads');
for (final message in messages) { final messages = await db.messageDao.getThreadMessages(cid);
final parentId = message.parentId; final messageByParentIdDictionary = <String, List<Message>>{};
messageByParentIdDictionary[parentId] = [ for (final message in messages) {
...messageByParentIdDictionary[parentId] ?? [], final parentId = message.parentId;
message messageByParentIdDictionary[parentId] = [
]; ...messageByParentIdDictionary[parentId] ?? [],
} message
return messageByParentIdDictionary; ];
} }
return messageByParentIdDictionary;
});
@override @override
Future<List<Message>> getReplies( Future<List<Message>> getReplies(
String parentId, { String parentId, {
PaginationParams options, PaginationParams options,
}) { }) =>
return _db.messageDao.getThreadMessagesByParentId( _readProtected(() async {
parentId, _logger.info('getReplies');
options: options, return db.messageDao.getThreadMessagesByParentId(
); parentId,
} options: options,
);
});
@override @override
Future<List<ChannelState>> getChannelStates({ Future<List<ChannelState>> getChannelStates({
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<SortOption<ChannelModel>> sort = const [], List<SortOption<ChannelModel>> sort = const [],
PaginationParams paginationParams, PaginationParams paginationParams,
}) async { }) async =>
final channels = await _db.channelQueryDao.getChannels( _readProtected(() async {
filter: filter, _logger.info('getChannelStates');
sort: sort, final channels = await db.channelQueryDao.getChannels(
paginationParams: paginationParams, filter: filter,
); sort: sort,
return Future.wait(channels.map((e) => getChannelStateByCid(e.cid))); paginationParams: paginationParams,
} );
return Future.wait(channels.map((e) => getChannelStateByCid(e.cid)));
});
@override @override
Future<void> updateChannelQueries( Future<void> updateChannelQueries(
Map<String, dynamic> filter, Map<String, dynamic> filter,
List<String> cids, List<String> cids,
bool clearQueryCache, bool clearQueryCache,
) { ) =>
return _db.channelQueryDao.updateChannelQueries( _readProtected(() async {
filter, _logger.info('updateChannelQueries');
cids, return db.channelQueryDao.updateChannelQueries(
clearQueryCache, filter,
); cids,
} clearQueryCache: clearQueryCache,
);
});
@override @override
Future<void> updateChannels(List<ChannelModel> channels) { Future<void> updateChannels(List<ChannelModel> channels) =>
return _db.channelDao.updateChannels(channels); _readProtected(() async {
} _logger.info('updateChannels');
return db.channelDao.updateChannels(channels);
});
@override @override
Future<void> updateMembers(String cid, List<Member> members) { Future<void> updateMembers(String cid, List<Member> members) =>
return _db.memberDao.updateMembers(cid, members); _readProtected(() async {
} _logger.info('updateMembers');
return db.memberDao.updateMembers(cid, members);
});
@override @override
Future<void> updateMessages(String cid, List<Message> messages) { Future<void> updateMessages(String cid, List<Message> messages) =>
return _db.messageDao.updateMessages(cid, messages); _readProtected(() async {
} _logger.info('updateMessages');
return db.messageDao.updateMessages(cid, messages);
});
@override @override
Future<void> updatePinnedMessages(String cid, List<Message> messages) { Future<void> updatePinnedMessages(String cid, List<Message> messages) =>
return _db.pinnedMessageDao.updateMessages(cid, messages); _readProtected(() async {
} _logger.info('updatePinnedMessages');
return db.pinnedMessageDao.updateMessages(cid, messages);
});
@override @override
Future<void> updateReactions(List<Reaction> reactions) { Future<void> updateReactions(List<Reaction> reactions) =>
return _db.reactionDao.updateReactions(reactions); _readProtected(() async {
} _logger.info('updateReactions');
return db.reactionDao.updateReactions(reactions);
});
@override @override
Future<void> updateReads(String cid, List<Read> reads) { Future<void> updateReads(String cid, List<Read> reads) =>
return _db.readDao.updateReads(cid, reads); _readProtected(() async {
} _logger.info('updateReads');
return db.readDao.updateReads(cid, reads);
});
@override @override
Future<void> updateUsers(List<User> users) { Future<void> updateUsers(List<User> users) => _readProtected(() async {
return _db.userDao.updateUsers(users); _logger.info('updateUsers');
} return db.userDao.updateUsers(users);
});
@override @override
Future<void> deleteReactionsByMessageId(List<String> messageIds) { Future<void> deleteReactionsByMessageId(List<String> messageIds) =>
return _db.reactionDao.deleteReactionsByMessageIds(messageIds); _readProtected(() async {
} _logger.info('deleteReactionsByMessageId');
return db.reactionDao.deleteReactionsByMessageIds(messageIds);
});
@override @override
Future<void> deleteMembersByCids(List<String> cids) { Future<void> deleteMembersByCids(List<String> cids) =>
return _db.memberDao.deleteMemberByCids(cids); _readProtected(() async {
} _logger.info('deleteMembersByCids');
return db.memberDao.deleteMemberByCids(cids);
});
@override @override
Future<void> disconnect({bool flush = false}) async { Future<void> updateChannelStates(List<ChannelState> channelStates) =>
if (_db != null) { _readProtected(() async => db.transaction(() async {
_logger.info('Disconnecting'); await super.updateChannelStates(channelStates);
if (flush) { }));
_logger.info('Flushing');
await _db.batch((batch) { @override
_db.allTables.forEach((table) { Future<void> disconnect({bool flush = false}) async =>
_db.delete(table).go(); _mutex.protectWrite(() async {
}); _logger.info('disconnect');
}); if (db != null) {
} _logger.info('Disconnecting');
await _db.disconnect(); if (flush) {
_db = null; _logger.info('Flushing');
} await db.batch((batch) {
} db.allTables.forEach((table) {
db.delete(table).go();
});
});
}
await db.disconnect();
db = null;
}
});
} }
@@ -1,7 +1,7 @@
name: stream_chat_persistence name: stream_chat_persistence
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Official Stream Chat Persistence library. Build your own chat experience using Dart and 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 repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -11,14 +11,17 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
logging: ^0.11.4
meta: ^1.2.4
moor: ^3.4.0 moor: ^3.4.0
mutex: ^2.0.0
path: ^1.7.0 path: ^1.7.0
path_provider: ^1.6.27 path_provider: ^1.6.27
sqlite3_flutter_libs: ^0.4.0+1 sqlite3_flutter_libs: ^0.4.0+1
stream_chat: ^1.4.0-beta stream_chat: ^1.5.0
dev_dependencies: dev_dependencies:
test: ^1.15.7
build_runner: ^1.11.0 build_runner: ^1.11.0
moor_generator: ^3.4.1 moor_generator: ^3.4.1
pedantic: ^1.9.2 pedantic: ^1.9.2
test: ^1.15.7
@@ -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.';
}))),
);
});
});
}