diff --git a/melos.yaml b/melos.yaml index 0118b49ec1..3eec77ef35 100644 --- a/melos.yaml +++ b/melos.yaml @@ -25,6 +25,7 @@ command: # List of all the dependencies used in the project. dependencies: async: ^2.11.0 + avatar_glow: ^3.0.0 cached_network_image: ^3.3.1 chewie: ^1.8.1 collection: ^1.17.2 @@ -43,6 +44,8 @@ command: file_selector: ^1.0.3 flutter_app_badger: ^1.5.0 flutter_local_notifications: ^18.0.1 + flutter_map: ^8.1.1 + flutter_map_animations: ^0.9.0 flutter_markdown: ^0.7.2+1 flutter_portal: ^1.1.4 flutter_secure_storage: ^9.2.2 @@ -50,6 +53,7 @@ command: flutter_svg: ^2.0.10+1 freezed_annotation: ">=2.4.1 <4.0.0" gal: ^2.3.1 + geolocator: ^13.0.0 get_thumbnail_video: ^0.7.3 go_router: ^14.6.2 http_parser: ^4.0.2 @@ -59,6 +63,7 @@ command: jose: ^0.3.4 json_annotation: ^4.9.0 just_audio: ">=0.9.38 <0.11.0" + latlong2: ^0.9.1 logging: ^1.2.0 lottie: ^3.1.2 media_kit: ^1.1.10+1 @@ -79,11 +84,11 @@ command: share_plus: ^11.0.0 shimmer: ^3.0.0 sqlite3_flutter_libs: ^0.5.26 - stream_chat: ^9.16.0 - stream_chat_flutter: ^9.16.0 - stream_chat_flutter_core: ^9.16.0 - stream_chat_localizations: ^9.16.0 - stream_chat_persistence: ^9.16.0 + stream_chat: ^10.0.0-beta.5 + stream_chat_flutter: ^10.0.0-beta.5 + stream_chat_flutter_core: ^10.0.0-beta.5 + stream_chat_localizations: ^10.0.0-beta.5 + stream_chat_persistence: ^10.0.0-beta.5 streaming_shared_preferences: ^2.0.0 svg_icon_widget: ^0.0.1 synchronized: ^3.1.0+1 diff --git a/migrations/v10-migration.md b/migrations/v10-migration.md new file mode 100644 index 0000000000..951fd6c344 --- /dev/null +++ b/migrations/v10-migration.md @@ -0,0 +1,441 @@ +# ๐Ÿš€ Flutter SDK v10 Migration Guide + +**Important!** This guide outlines all breaking changes introduced in the Flutter SDK **v10.0.0** release, including pre-release stages like `v10.0.0-beta.1`. Follow each section based on the version you're migrating from. + +--- + +## ๐Ÿ“‹ Breaking Changes Overview + +This guide includes breaking changes grouped by release phase: + +### ๐Ÿšง v10.0.0-beta.4 + +- [SendReaction](#-sendreaction) + +### ๐Ÿšง v10.0.0-beta.3 + +- [AttachmentPickerType](#-attachmentpickertype) +- [StreamAttachmentPickerOption](#-streamattachmentpickeroption) +- [showStreamAttachmentPickerModalBottomSheet](#-showstreamattachmentpickermodalbottomsheet) +- [AttachmentPickerBottomSheet](#-attachmentpickerbottomsheet) + +### ๐Ÿšง v10.0.0-beta.1 + +- [StreamReactionPicker](#-streamreactionpicker) +- [StreamMessageAction](#-streammessageaction) +- [StreamMessageReactionsModal](#-streammessagereactionsmodal) +- [StreamMessageWidget](#-streammessagewidget) + +--- + +## ๐Ÿงช Migration for v10.0.0-beta.4 + +### ๐Ÿ›  SendReaction + +#### Key Changes: + +- `sendReaction` method now accepts a full `Reaction` object instead of individual parameters. + +#### Migration Steps: + +**Before:** +```dart +// Using individual parameters +channel.sendReaction( + message, + 'like', + score: 1, + extraData: {'custom_field': 'value'}, +); + +client.sendReaction( + messageId, + 'love', + enforceUnique: true, + extraData: {'custom_field': 'value'}, +); +``` + +**After:** +```dart +// Using Reaction object +channel.sendReaction( + message, + Reaction( + type: 'like', + score: 1, + emojiCode: '๐Ÿ‘', + extraData: {'custom_field': 'value'}, + ), +); + +client.sendReaction( + messageId, + Reaction( + type: 'love', + emojiCode: 'โค๏ธ', + extraData: {'custom_field': 'value'}, + ), + enforceUnique: true, +); +``` + +> โš ๏ธ **Important:** +> - The `sendReaction` method now requires a `Reaction` object +> - Optional parameters like `enforceUnique` and `skipPush` remain as method parameters +> - You can now specify custom emoji codes for reactions using the `emojiCode` field + +--- + +## ๐Ÿงช Migration for v10.0.0-beta.3 + +### ๐Ÿ›  AttachmentPickerType + +#### Key Changes: + +- `AttachmentPickerType` enum replaced with sealed class hierarchy +- Now supports extensible custom types like contact and location pickers +- Use built-in types like `AttachmentPickerType.images` or define your own via `CustomAttachmentPickerType` + +#### Migration Steps: + +**Before:** +```dart +// Using enum-based attachment types +final attachmentType = AttachmentPickerType.images; +``` + +**After:** +```dart +// Using sealed class attachment types +final attachmentType = AttachmentPickerType.images; + +// For custom types +class LocationAttachmentPickerType extends CustomAttachmentPickerType { + const LocationAttachmentPickerType(); +} +``` + +> โš ๏ธ **Important:** +> The enum is now a sealed class, but the basic usage remains the same for built-in types. + +--- + +### ๐Ÿ›  StreamAttachmentPickerOption + +#### Key Changes: + +- `StreamAttachmentPickerOption` replaced with two sealed classes: + - `SystemAttachmentPickerOption` for system pickers (camera, files) + - `TabbedAttachmentPickerOption` for tabbed pickers (gallery, polls, location) + +#### Migration Steps: + +**Before:** +```dart +final option = AttachmentPickerOption( + title: 'Gallery', + icon: Icon(Icons.photo_library), + supportedTypes: [AttachmentPickerType.images, AttachmentPickerType.videos], + optionViewBuilder: (context, controller) { + return GalleryPickerView(controller: controller); + }, +); + +final webOrDesktopOption = WebOrDesktopAttachmentPickerOption( + title: 'File Upload', + icon: Icon(Icons.upload_file), + type: AttachmentPickerType.files, +); +``` + +**After:** +```dart +// For custom UI pickers (gallery, polls) +final tabbedOption = TabbedAttachmentPickerOption( + title: 'Gallery', + icon: Icon(Icons.photo_library), + supportedTypes: [AttachmentPickerType.images, AttachmentPickerType.videos], + optionViewBuilder: (context, controller) { + return GalleryPickerView(controller: controller); + }, +); + +// For system pickers (camera, file dialogs) +final systemOption = SystemAttachmentPickerOption( + title: 'Camera', + icon: Icon(Icons.camera_alt), + supportedTypes: [AttachmentPickerType.images], + onTap: (context, controller) => pickFromCamera(), +); +``` + +> โš ๏ธ **Important:** +> - Use `SystemAttachmentPickerOption` for system pickers (camera, file dialogs) +> - Use `TabbedAttachmentPickerOption` for custom UI pickers (gallery, polls) + +--- + +### ๐Ÿ›  showStreamAttachmentPickerModalBottomSheet + +#### Key Changes: + +- Now returns `StreamAttachmentPickerResult` instead of `AttachmentPickerValue` +- Improved type safety and clearer intent handling + +#### Migration Steps: + +**Before:** +```dart +final result = await showStreamAttachmentPickerModalBottomSheet( + context: context, + controller: controller, +); + +// result is AttachmentPickerValue +``` + +**After:** +```dart +final result = await showStreamAttachmentPickerModalBottomSheet( + context: context, + controller: controller, +); + +// result is StreamAttachmentPickerResult +switch (result) { + case AttachmentsPicked(): + // Handle picked attachments + case PollCreated(): + // Handle created poll + case AttachmentPickerError(): + // Handle error + case CustomAttachmentPickerResult(): + // Handle custom result +} +``` + +> โš ๏ธ **Important:** +> Always handle the new `StreamAttachmentPickerResult` return type with proper switch cases. + +--- + +### ๐Ÿ›  AttachmentPickerBottomSheet + +#### Key Changes: + +- `StreamMobileAttachmentPickerBottomSheet` โ†’ `StreamTabbedAttachmentPickerBottomSheet` +- `StreamWebOrDesktopAttachmentPickerBottomSheet` โ†’ `StreamSystemAttachmentPickerBottomSheet` + +#### Migration Steps: + +**Before:** +```dart +StreamMobileAttachmentPickerBottomSheet( + context: context, + controller: controller, + customOptions: [option], +); + +StreamWebOrDesktopAttachmentPickerBottomSheet( + context: context, + controller: controller, + customOptions: [option], +); +``` + +**After:** +```dart +StreamTabbedAttachmentPickerBottomSheet( + context: context, + controller: controller, + customOptions: [tabbedOption], +); + +StreamSystemAttachmentPickerBottomSheet( + context: context, + controller: controller, + customOptions: [systemOption], +); +``` + +> โš ๏ธ **Important:** +> The new names better reflect their respective layouts and functionality. + +--- + +## ๐Ÿงช Migration for v10.0.0-beta.1 + +### ๐Ÿ›  StreamReactionPicker + +#### Key Changes: + +- New `StreamReactionPicker.builder` constructor +- Added properties: `padding`, `scrollable`, `borderRadius` +- Automatic reaction handling removed โ€” must now use `onReactionPicked` + +#### Migration Steps: + +**Before:** +```dart +StreamReactionPicker( + message: message, +); +``` + +**After (Recommended โ€“ Builder):** +```dart +StreamReactionPicker.builder( + context, + message, + (Reaction reaction) { + // Explicitly handle reaction + }, +); +``` + +**After (Alternative โ€“ Direct Configuration):** +```dart +StreamReactionPicker( + message: message, + reactionIcons: StreamChatConfiguration.of(context).reactionIcons, + onReactionPicked: (Reaction reaction) { + // Handle reaction here + }, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + scrollable: true, + borderRadius: BorderRadius.circular(24), +); +``` + +> โš ๏ธ **Important:** +> Automatic reaction handling has been removed. You must explicitly handle reactions using `onReactionPicked`. + +--- + +### ๐Ÿ›  StreamMessageAction + +#### Key Changes: + +- Now generic: `StreamMessageAction` +- Individual `onTap` handlers removed โ€” use `onCustomActionTap` instead +- Added new styling props for better customization + +#### Migration Steps: + +**Before:** +```dart +final customAction = StreamMessageAction( + title: Text('Custom Action'), + leading: Icon(Icons.settings), + onTap: (message) { + // Handle action + }, +); +``` + +**After (Type-safe):** +```dart +final customAction = StreamMessageAction( + action: CustomMessageAction( + message: message, + extraData: {'type': 'custom_action'}, + ), + title: Text('Custom Action'), + leading: Icon(Icons.settings), + isDestructive: false, + iconColor: Colors.blue, +); + +StreamMessageWidget( + message: message, + customActions: [customAction], + onCustomActionTap: (CustomMessageAction action) { + // Handle action here + }, +); +``` + +> โš ๏ธ **Important:** +> Individual `onTap` callbacks have been removed. Always handle actions using the centralized `onCustomActionTap`. + +--- + +### ๐Ÿ›  StreamMessageReactionsModal + +#### Key Changes: + +- Based on `StreamMessageModal` for consistency +- `messageTheme` removed โ€” inferred automatically +- Reaction handling must now be handled via `onReactionPicked` + +#### Migration Steps: + +**Before:** +```dart +StreamMessageReactionsModal( + message: message, + messageWidget: myMessageWidget, + messageTheme: myCustomMessageTheme, + reverse: true, +); +``` + +**After:** +```dart +StreamMessageReactionsModal( + message: message, + messageWidget: myMessageWidget, + reverse: true, + onReactionPicked: (SelectReaction reactionAction) { + // Handle reaction explicitly + }, +); +``` + +> โš ๏ธ **Important:** +> `messageTheme` has been removed. Reaction handling must now be explicit using `onReactionPicked`. + +--- + +### ๐Ÿ›  StreamMessageWidget + +#### Key Changes: + +- `showReactionTail` parameter has been removed +- Tail now automatically shows when the picker is visible + +#### Migration Steps: + +**Before:** +```dart +StreamMessageWidget( + message: message, + showReactionTail: true, +); +``` + +**After:** +```dart +StreamMessageWidget( + message: message, +); +``` + +> โš ๏ธ **Important:** +> The `showReactionTail` parameter is no longer supported. Tail is now always shown when the picker is visible. + +--- + +## ๐ŸŽ‰ You're Ready to Migrate! + +### For v10.0.0-beta.3: +- โœ… Update attachment picker options to use `SystemAttachmentPickerOption` or `TabbedAttachmentPickerOption` +- โœ… Handle new `StreamAttachmentPickerResult` return type from attachment picker +- โœ… Use renamed bottom sheet classes (`StreamTabbedAttachmentPickerBottomSheet`, `StreamSystemAttachmentPickerBottomSheet`) + +### For v10.0.0-beta.1: +- โœ… Use `StreamReactionPicker.builder` or supply `onReactionPicked` +- โœ… Convert all `StreamMessageAction` instances to type-safe generic usage +- โœ… Centralize handling with `onCustomActionTap` +- โœ… Remove deprecated props like `showReactionTail` and `messageTheme` diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index fb5e53a747..3349d4521d 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,7 +1,12 @@ +## 10.0.0-beta.5 + +- Included the changes from version [`9.16.0`](https://pub.dev/packages/stream_chat/changelog). + ## 9.16.0 ๐Ÿž Fixed +- Fixed `skipPush` and `skipEnrichUrl` not preserving during message send or update retry - Fixed `Channel` methods to throw proper `StateError` exceptions instead of relying on assertions for state validation. - Fixed `OwnUser` specific fields getting lost when creating a new `OwnUser` instance from @@ -13,6 +18,25 @@ - Added support for `Client.setPushPreferences` which allows setting PushPreferences for the current user or for a specific channel. +## 10.0.0-beta.4 + +๐Ÿ›‘๏ธ Breaking + +- **Changed `sendReaction` method signature**: The `sendReaction` method on both `Client` and + `Channel` now accepts a full `Reaction` object instead of individual parameters (`type`, `score`, + `extraData`). This change provides more flexibility and better type safety. + +โœ… Added + +- Added comprehensive location sharing support with static and live location features: + - `Channel.sendStaticLocation()` - Send a static location message to the channel + - `Channel.startLiveLocationSharing()` - Start sharing live location with automatic updates + - `Channel.activeLiveLocations` - Track members active live location shares in the channel + - `Client.activeLiveLocations` - Access current user active live location shares across channels + - Location event listeners for `locationShared`, `locationUpdated`, and `locationExpired` events + +- Included the changes from version [`9.15.0`](https://pub.dev/packages/stream_chat/changelog). + ## 9.15.0 โœ… Added @@ -27,6 +51,18 @@ - Fixed draft message persistence issues where removed drafts were not properly deleted from the database. +## 10.0.0-beta.3 + +๐Ÿ›‘๏ธ Breaking + +- **Deprecated API Cleanup**: Removed all deprecated classes, methods, and properties for the v10 major release: + - **Removed Classes**: `PermissionType` (use string constants like `'delete-channel'`, `'update-channel'`), `CallApi`, `CallPayload`, `CallTokenPayload`, `CreateCallPayload` + - **Removed Methods**: `cooldownStartedAt` getter from `Channel`, `getCallToken` and `createCall` from `StreamChatClient` + - **Removed Properties**: `reactionCounts` and `reactionScores` getters from `Message` (use `reactionGroups` instead), `call` property from `StreamChatApi` + - **Removed Files**: `permission_type.dart`, `call_api.dart`, `call_payload.dart` and their associated tests + +- Included the changes from version [`9.14.0`](https://pub.dev/packages/stream_chat/changelog). + ## 9.14.0 ๐Ÿž Fixed @@ -45,10 +81,18 @@ - Deprecated `SortOption.new` constructor in favor of `SortOption.desc` and `SortOption.asc`. +## 10.0.0-beta.2 + +- Included the changes from version [`9.13.0`](https://pub.dev/packages/stream_chat/changelog). + ## 9.13.0 - Bug fixes and improvements +## 10.0.0-beta.1 + +- Bug fixes and improvements + ## 9.12.0 โœ… Added diff --git a/packages/stream_chat/example/pubspec.yaml b/packages/stream_chat/example/pubspec.yaml index 39b90aae34..96f41d89a7 100644 --- a/packages/stream_chat/example/pubspec.yaml +++ b/packages/stream_chat/example/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat: ^9.16.0 + stream_chat: ^10.0.0-beta.5 flutter: uses-material-design: true diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 1849233552..1bbca836f5 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -306,15 +306,6 @@ class Channel { return math.max(0, cooldownDuration - elapsedTime); } - /// Stores time at which cooldown was started - @Deprecated( - "Use a combination of 'remainingCooldown' and 'currentUserLastMessageAt'", - ) - DateTime? get cooldownStartedAt { - if (getRemainingCooldown() <= 0) return null; - return currentUserLastMessageAt; - } - /// Channel creation date. DateTime? get createdAt { _checkInitialized(); @@ -674,7 +665,7 @@ class Channel { _checkInitialized(); // Clean up stale error messages before sending a new message. - state!.cleanUpStaleErrorMessages(); + state?.cleanUpStaleErrorMessages(); // Cancelling previous completer in case it's called again in the process // Eg. Updating the message while the previous call is in progress. @@ -699,7 +690,7 @@ class Channel { ).toList(), ); - state!.updateMessage(message); + state?.updateMessage(message); try { if (message.attachments.any((it) => !it.uploadState.isSuccess)) { @@ -733,17 +724,22 @@ class Channel { state: MessageState.sent, ); - state!.updateMessage(sentMessage); + state?.updateMessage(sentMessage); return response; } catch (e) { + final failedMessage = message.copyWith( + // Update the message state to failed. + state: MessageState.sendingFailed( + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), + ); + + state?.updateMessage(failedMessage); + // If the error is retriable, add it to the retry queue. if (e is StreamChatNetworkError && e.isRetriable) { - state!._retryQueue.add([ - message.copyWith( - // Update the message state to failed. - state: MessageState.sendingFailed, - ), - ]); + state?._retryQueue.add([failedMessage]); } rethrow; @@ -762,7 +758,6 @@ class Channel { bool skipEnrichUrl = false, }) async { _checkInitialized(); - final originalMessage = message; // Cancelling previous completer in case it's called again in the process // Eg. Updating the message while the previous call is in progress. @@ -819,22 +814,20 @@ class Channel { return response; } catch (e) { - if (e is StreamChatNetworkError) { - if (e.isRetriable) { - state!._retryQueue.add([ - message.copyWith( - // Update the message state to failed. - state: MessageState.updatingFailed, - ), - ]); - } else { - // Reset the message to original state if the update fails and is not - // retriable. - state?.updateMessage(originalMessage.copyWith( - state: MessageState.updatingFailed, - )); - } + final failedMessage = message.copyWith( + // Update the message state to failed. + state: MessageState.updatingFailed( + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), + ); + + state?.updateMessage(failedMessage); + // If the error is retriable, add it to the retry queue. + if (e is StreamChatNetworkError && e.isRetriable) { + state?._retryQueue.add([failedMessage]); } + rethrow; } } @@ -851,7 +844,6 @@ class Channel { bool skipEnrichUrl = false, }) async { _checkInitialized(); - final originalMessage = message; // Cancelling previous completer in case it's called again in the process // Eg. Updating the message while the previous call is in progress. @@ -889,21 +881,19 @@ class Channel { return response; } catch (e) { - if (e is StreamChatNetworkError) { - if (e.isRetriable) { - state!._retryQueue.add([ - message.copyWith( - // Update the message state to failed. - state: MessageState.updatingFailed, - ), - ]); - } else { - // Reset the message to original state if the update fails and is not - // retriable. - state?.updateMessage(originalMessage.copyWith( - state: MessageState.updatingFailed, - )); - } + final failedMessage = message.copyWith( + // Update the message state to failed. + state: MessageState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: skipEnrichUrl, + ), + ); + + state?.updateMessage(failedMessage); + // If the error is retriable, add it to the retry queue. + if (e is StreamChatNetworkError && e.isRetriable) { + state?._retryQueue.add([failedMessage]); } rethrow; @@ -922,7 +912,7 @@ class Channel { // Directly deleting the local messages and bounced error messages as they // are not available on the server. if (message.remoteCreatedAt == null || message.isBouncedWithError) { - state!.deleteMessage( + state?.deleteMessage( message.copyWith( type: MessageType.deleted, localDeletedAt: DateTime.now(), @@ -977,32 +967,69 @@ class Channel { return response; } catch (e) { + final failedMessage = message.copyWith( + // Update the message state to failed. + state: MessageState.deletingFailed(hard: hard), + ); + + state?.deleteMessage(failedMessage, hardDelete: hard); + // If the error is retriable, add it to the retry queue. if (e is StreamChatNetworkError && e.isRetriable) { - state!._retryQueue.add([ - message.copyWith( - // Update the message state to failed. - state: MessageState.deletingFailed(hard: hard), - ), - ]); + state?._retryQueue.add([failedMessage]); } + rethrow; } } - /// Retry the operation on the message based on the failed state. + /// Retries operations on a message based on its failed state. /// - /// For example, if the message failed to send, it will retry sending the - /// message and vice-versa. + /// This method examines the message's state and performs the appropriate + /// retry action: + /// - For [MessageState.sendingFailed], it attempts to send the message. + /// - For [MessageState.updatingFailed], it attempts to update the message. + /// - For [MessageState.partialUpdatingFailed], it attempts to partially + /// update the message with the same 'set' and 'unset' parameters that were + /// used in the original request. + /// - For [MessageState.deletingFailed], it attempts to delete the message. + /// with the same 'hard' parameter that was used in the original request + /// - For messages with [isBouncedWithError], it attempts to send the message. Future retryMessage(Message message) async { - assert(message.state.isFailed, 'Message state is not failed'); + assert( + message.state.isFailed || message.isBouncedWithError, + 'Only failed or bounced messages can be retried', + ); return message.state.maybeWhen( failed: (state, _) => state.when( - sendingFailed: () => sendMessage(message), - updatingFailed: () => updateMessage(message), + sendingFailed: (skipPush, skipEnrichUrl) => sendMessage( + message, + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), + updatingFailed: (skipPush, skipEnrichUrl) => updateMessage( + message, + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), + partialUpdatingFailed: (set, unset, skipEnrichUrl) { + return partialUpdateMessage( + message, + set: set, + unset: unset, + skipEnrichUrl: skipEnrichUrl, + ); + }, deletingFailed: (hard) => deleteMessage(message, hard: hard), ), - orElse: () => throw StateError('Message state is not failed'), + orElse: () { + // Check if the message is bounced with error. + if (message.isBouncedWithError) return sendMessage(message); + + throw StateError( + 'Only failed or bounced messages can be retried', + ); + }, ); } @@ -1075,6 +1102,72 @@ class Channel { return _client.deleteDraft(id!, type, parentId: parentId); } + /// Sends a static location to this channel. + /// + /// Optionally, provide a [messageText] and [extraData] to send along with + /// the location. + Future sendStaticLocation({ + String? id, + String? messageText, + String? createdByDeviceId, + required LocationCoordinates location, + Map extraData = const {}, + }) { + final message = Message( + id: id, + text: messageText, + extraData: extraData, + ); + + final currentUserId = _client.state.currentUser?.id; + final locationMessage = message.copyWith( + sharedLocation: Location( + channelCid: cid, + userId: currentUserId, + messageId: message.id, + latitude: location.latitude, + longitude: location.longitude, + createdByDeviceId: createdByDeviceId, + ), + ); + + return sendMessage(locationMessage); + } + + /// Sends a live location sharing message to this channel. + /// + /// Optionally, provide a [messageText] and [extraData] to send along with + /// the location. + Future startLiveLocationSharing({ + String? id, + String? messageText, + String? createdByDeviceId, + required DateTime endSharingAt, + required LocationCoordinates location, + Map extraData = const {}, + }) { + final message = Message( + id: id, + text: messageText, + extraData: extraData, + ); + + final currentUserId = _client.state.currentUser?.id; + final locationMessage = message.copyWith( + sharedLocation: Location( + channelCid: cid, + userId: currentUserId, + messageId: message.id, + endAt: endSharingAt, + latitude: location.latitude, + longitude: location.longitude, + createdByDeviceId: createdByDeviceId, + ), + ); + + return sendMessage(locationMessage); + } + /// Send a file to this channel. Future sendFile( AttachmentFile file, { @@ -1173,7 +1266,7 @@ class Channel { /// Optionally provide a [messageText] to send a message along with the poll. Future sendPoll( Poll poll, { - String messageText = '', + String? messageText, }) async { _checkInitialized(); final res = await _pollLock.synchronized(() => _client.createPoll(poll)); @@ -1337,28 +1430,17 @@ class Channel { /// Set [enforceUnique] to true to remove the existing user reaction. Future sendReaction( Message message, - String type, { - int score = 1, - Map extraData = const {}, + Reaction reaction, { + bool skipPush = false, bool enforceUnique = false, }) async { _checkInitialized(); - final currentUser = _client.state.currentUser; - if (currentUser == null) { - throw StateError( - 'Cannot send reaction: current user is not available. ' - 'Ensure the client is connected and a user is set.', - ); - } final messageId = message.id; - final reaction = Reaction( - type: type, + // ignore: parameter_assignments + reaction = reaction.copyWith( messageId: messageId, - user: currentUser, - score: score, - createdAt: DateTime.timestamp(), - extraData: extraData, + user: _client.state.currentUser, ); final updatedMessage = message.addMyReaction( @@ -1371,9 +1453,8 @@ class Channel { try { final reactionResp = await _client.sendReaction( messageId, - reaction.type, - score: reaction.score, - extraData: reaction.extraData, + reaction, + skipPush: skipPush, enforceUnique: enforceUnique, ); return reactionResp; @@ -1389,6 +1470,8 @@ class Channel { Message message, Reaction reaction, ) async { + _checkInitialized(); + final updatedMessage = message.deleteMyReaction( reactionType: reaction.type, ); @@ -2107,78 +2190,78 @@ class ChannelClientState { _channelStateController = BehaviorSubject.seeded(channelState); + // region TYPING EVENTS _listenTypingEvents(); + // endregion + // region MESSAGE EVENTS _listenMessageNew(); - _listenMessageDeleted(); - _listenMessageUpdated(); + // endregion - /* Start of draft events */ - + // region DRAFT EVENTS _listenDraftUpdated(); - _listenDraftDeleted(); + // endregion - /* End of draft events */ - - _listenReactions(); - + // region REACTION EVENTS + _listenReactionNew(); + _listenReactionUpdated(); _listenReactionDeleted(); + // endregion - /* Start of poll events */ - + // region POLL EVENTS + _listenPollCreated(); _listenPollUpdated(); - _listenPollClosed(); - _listenPollAnswerCasted(); - _listenPollVoteCasted(); - _listenPollVoteChanged(); - _listenPollAnswerRemoved(); - _listenPollVoteRemoved(); + // endregion - /* End of poll events */ - + // region READ EVENTS _listenReadEvents(); + // endregion + // region CHANNEL EVENTS _listenChannelTruncated(); - _listenChannelUpdated(); + // endregion + // region MEMBER EVENTS _listenMemberAdded(); - _listenMemberRemoved(); - _listenMemberUpdated(); - _listenMemberBanned(); - _listenMemberUnbanned(); + // endregion + // region USER WATCHING EVENTS _listenUserStartWatching(); - _listenUserStopWatching(); + // endregion - /* Start of reminder events */ - + // region REMINDER EVENTS _listenReminderCreated(); - _listenReminderUpdated(); - _listenReminderDeleted(); + // endregion - /* End of reminder events */ + // region LOCATION EVENTS + _listenLocationShared(); + _listenLocationUpdated(); + _listenLocationExpired(); + // endregion _startCleaningStaleTypingEvents(); _startCleaningStalePinnedMessages(); + _startCleaningExpiredLocations(); + _channel._client.chatPersistenceClient ?.getChannelThreads(_channel.cid!) .then((threads) { @@ -2441,8 +2524,10 @@ class ChannelClientState { /// Retry failed message. Future retryFailedMessages() async { - final failedMessages = [...messages, ...threads.values.expand((v) => v)] - .where((it) => it.state.isFailed); + final allMessages = [...messages, ...threads.values.flattened]; + final failedMessages = allMessages.where((it) => it.state.isFailed); + + if (failedMessages.isEmpty) return; _retryQueue.add(failedMessages); } @@ -2457,6 +2542,17 @@ class ChannelClientState { return threadMessage; } + void _listenPollCreated() { + _subscriptions.add( + _channel.on(EventType.pollCreated).listen((event) { + final message = event.message; + if (message == null || message.poll == null) return; + + return addNewMessage(message); + }), + ); + } + void _listenPollUpdated() { _subscriptions.add(_channel.on(EventType.pollUpdated).listen((event) { final eventPoll = event.poll; @@ -2719,58 +2815,162 @@ class ChannelClientState { } } + Message? _findLocationMessage(String id) { + final message = messages.firstWhereOrNull((it) { + return it.sharedLocation?.messageId == id; + }); + + if (message != null) return message; + + final threadMessage = threads.values.flattened.firstWhereOrNull((it) { + return it.sharedLocation?.messageId == id; + }); + + return threadMessage; + } + + void _listenLocationShared() { + _subscriptions.add( + _channel.on(EventType.locationShared).listen((event) { + final message = event.message; + if (message == null || message.sharedLocation == null) return; + + return addNewMessage(message); + }), + ); + } + + void _listenLocationUpdated() { + _subscriptions.add( + _channel.on(EventType.locationUpdated).listen((event) { + final location = event.message?.sharedLocation; + if (location == null) return; + + final messageId = location.messageId; + if (messageId == null) return; + + final oldMessage = _findLocationMessage(messageId); + if (oldMessage == null) return; + + final updatedMessage = oldMessage.copyWith(sharedLocation: location); + return updateMessage(updatedMessage); + }), + ); + } + + void _listenLocationExpired() { + _subscriptions.add( + _channel.on(EventType.locationExpired).listen((event) { + final location = event.message?.sharedLocation; + if (location == null) return; + + final messageId = location.messageId; + if (messageId == null) return; + + final oldMessage = _findLocationMessage(messageId); + if (oldMessage == null) return; + + final updatedMessage = oldMessage.copyWith(sharedLocation: location); + return updateMessage(updatedMessage); + }), + ); + } + void _listenReactionDeleted() { - _subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) { - final oldMessage = - messages.firstWhereOrNull((it) => it.id == event.message?.id) ?? - threads[event.message?.parentId] - ?.firstWhereOrNull((e) => e.id == event.message?.id); - final reaction = event.reaction; - final ownReactions = oldMessage?.ownReactions - ?.whereNot((it) => - it.type == reaction?.type && - it.score == reaction?.score && - it.messageId == reaction?.messageId && - it.userId == reaction?.userId && - it.extraData == reaction?.extraData) - .toList(growable: false); - final message = event.message!.copyWith( - ownReactions: ownReactions, - ); - updateMessage(message); - })); + _subscriptions.add( + _channel.on(EventType.reactionDeleted).listen((event) { + final (eventReaction, eventMessage) = (event.reaction, event.message); + if (eventReaction == null || eventMessage == null) return; + + final messageId = eventMessage.id; + final parentId = eventMessage.parentId; + + for (final message in [...messages, ...?threads[parentId]]) { + if (message.id == messageId) { + final currentUserId = _channel.client.state.currentUser?.id; + + final currentMessage = switch (currentUserId) { + final userId? when userId == eventReaction.userId => + message.deleteMyReaction(reactionType: eventReaction.type), + _ => message, + }; + + return updateMessage( + eventMessage.copyWith( + ownReactions: currentMessage.ownReactions, + ), + ); + } + } + }), + ); } - void _listenReactions() { + void _listenReactionNew() { _subscriptions.add(_channel.on(EventType.reactionNew).listen((event) { - final oldMessage = - messages.firstWhereOrNull((it) => it.id == event.message?.id) ?? - threads[event.message?.parentId] - ?.firstWhereOrNull((e) => e.id == event.message?.id); - final message = event.message!.copyWith( - ownReactions: oldMessage?.ownReactions, - ); - updateMessage(message); + final (eventReaction, eventMessage) = (event.reaction, event.message); + if (eventReaction == null || eventMessage == null) return; + + final messageId = eventMessage.id; + final parentId = eventMessage.parentId; + + for (final message in [...messages, ...?threads[parentId]]) { + if (message.id == messageId) { + final currentUserId = _channel.client.state.currentUser?.id; + + final currentMessage = switch (currentUserId) { + final userId? when userId == eventReaction.userId => + message.addMyReaction(eventReaction), + _ => message, + }; + + return updateMessage( + eventMessage.copyWith( + ownReactions: currentMessage.ownReactions, + ), + ); + } + } })); } + void _listenReactionUpdated() { + _subscriptions.add( + _channel.on(EventType.reactionUpdated).listen((event) { + final (eventReaction, eventMessage) = (event.reaction, event.message); + if (eventReaction == null || eventMessage == null) return; + + final messageId = eventMessage.id; + final parentId = eventMessage.parentId; + + for (final message in [...messages, ...?threads[parentId]]) { + if (message.id == messageId) { + final currentUserId = _channel.client.state.currentUser?.id; + + final currentMessage = switch (currentUserId) { + final userId? when userId == eventReaction.userId => + // reaction.updated is only called if enforce_unique is true + message.addMyReaction(eventReaction, enforceUnique: true), + _ => message, + }; + + return updateMessage( + eventMessage.copyWith( + ownReactions: currentMessage.ownReactions, + ), + ); + } + } + }), + ); + } + void _listenMessageUpdated() { - _subscriptions.add(_channel - .on( - EventType.messageUpdated, - EventType.reactionUpdated, - ) - .listen((event) { - final oldMessage = - messages.firstWhereOrNull((it) => it.id == event.message?.id) ?? - threads[event.message?.parentId] - ?.firstWhereOrNull((e) => e.id == event.message?.id); - final message = event.message!.copyWith( - poll: oldMessage?.poll, - pollId: oldMessage?.pollId, - ownReactions: oldMessage?.ownReactions, - ); - updateMessage(message); + _subscriptions.add(_channel.on(EventType.messageUpdated).listen((event) { + final message = event.message; + if (message == null) return; + + return updateMessage(message); })); } @@ -2779,7 +2979,7 @@ class ChannelClientState { final message = event.message!; final hardDelete = event.hardDelete ?? false; - deleteMessage(message, hardDelete: hardDelete); + return deleteMessage(message, hardDelete: hardDelete); })); } @@ -2793,19 +2993,22 @@ class ChannelClientState { final message = event.message; if (message == null) return; - final isThreadMessage = message.parentId != null; - final isShownInChannel = message.showInChannel == true; - final isThreadOnlyMessage = isThreadMessage && !isShownInChannel; + return addNewMessage(message); + })); + } + + /// Adds a new message to the channel state and updates the unread count. + void addNewMessage(Message message) { + final isThreadMessage = message.parentId != null; + final isShownInChannel = message.showInChannel == true; + final isThreadOnlyMessage = isThreadMessage && !isShownInChannel; - // Only add the message if the channel is upToDate or if the message is - // a thread-only message. - if (isUpToDate || isThreadOnlyMessage) { - updateMessage(message); - } + // Only add the message if the channel is upToDate or if the message is + // a thread-only message. + if (isUpToDate || isThreadOnlyMessage) updateMessage(message); - // Otherwise, check if we can count the message as unread. - if (_countMessageAsUnread(message)) unreadCount += 1; - })); + // Otherwise, check if we can count the message as unread. + if (_countMessageAsUnread(message)) unreadCount += 1; } // Logic taken from the backend SDK @@ -2937,9 +3140,6 @@ class ChannelClientState { newMessages.add(message); } - // Handle updates to pinned messages. - final newPinnedMessages = _updatePinnedMessages(message); - // Calculate the new last message at time. var lastMessageAt = _channelState.channel?.lastMessageAt; lastMessageAt ??= message.createdAt; @@ -2950,17 +3150,45 @@ class ChannelClientState { // Apply the updated lists to the channel state. _channelState = _channelState.copyWith( messages: newMessages.sorted(_sortByCreatedAt), - pinnedMessages: newPinnedMessages, channel: _channelState.channel?.copyWith( lastMessageAt: lastMessageAt, ), ); } - // If the message is part of a thread, update thread information. + // If the message is part of a thread, update thread message. if (message.parentId case final parentId?) { - updateThreadInfo(parentId, [message]); + _updateThreadMessage(parentId, message); } + + // Handle updates to pinned messages. + final newPinnedMessages = _updatePinnedMessages(message); + + // Handle updates to the active live locations. + final newActiveLiveLocations = _updateActiveLiveLocations(message); + + // Update the channel state with the new pinned messages and + // active live locations. + _channelState = _channelState.copyWith( + pinnedMessages: newPinnedMessages, + activeLiveLocations: newActiveLiveLocations, + ); + } + + void _updateThreadMessage(String parentId, Message message) { + final existingThreadMessages = threads[parentId] ?? []; + final updatedThreadMessages = [ + ...existingThreadMessages.merge( + [message], + key: (it) => it.id, + update: (original, updated) => updated.syncWith(original), + ), + ]; + + _threads = { + ...threads, + parentId: updatedThreadMessages.sorted(_sortByCreatedAt) + }; } /// Cleans up all the stale error messages which requires no action. @@ -2976,70 +3204,122 @@ class ChannelClientState { /// Updates the list of pinned messages based on the current message's /// pinned status. List _updatePinnedMessages(Message message) { - final newPinnedMessages = [...pinnedMessages]; - final oldPinnedIndex = - newPinnedMessages.indexWhere((m) => m.id == message.id); - - if (message.pinned) { - // If the message is pinned, add or update it in the list of pinned - // messages. - if (oldPinnedIndex != -1) { - newPinnedMessages[oldPinnedIndex] = message; - } else { - newPinnedMessages.add(message); - } - } else { - // If the message is not pinned, remove it from the list of pinned - // messages. - newPinnedMessages.removeWhere((m) => m.id == message.id); + final existingPinnedMessages = [...pinnedMessages]; + + // If the message is pinned and not yet deleted, + // merge it into the existing pinned messages. + if (message.pinned && !message.isDeleted) { + final newPinnedMessages = [ + ...existingPinnedMessages.merge( + [message], + key: (it) => it.id, + update: (old, updated) => updated, + ), + ]; + + return newPinnedMessages; } + // Otherwise, remove the message from the pinned messages. + final newPinnedMessages = [ + ...existingPinnedMessages.where((m) => m.id != message.id), + ]; + return newPinnedMessages; } + List _updateActiveLiveLocations(Message message) { + final existingLocations = [...activeLiveLocations]; + + final location = message.sharedLocation; + if (location == null) return existingLocations; + + // If the location is live and active and not yet deleted, + // merge it into the existing active live locations. + if (location.isLive && location.isActive && !message.isDeleted) { + final newActiveLiveLocations = [ + ...existingLocations.merge( + [location], + key: (it) => (it.userId, it.channelCid, it.createdByDeviceId), + update: (old, updated) => updated, + ), + ]; + + return [...newActiveLiveLocations.where((it) => it.isActive)]; + } + + // Otherwise, remove the location from the active live locations. + final newActiveLiveLocations = [ + ...existingLocations.where((it) => it.messageId != location.messageId), + ]; + + return [...newActiveLiveLocations.where((it) => it.isActive)]; + } + /// Remove a [message] from this [channelState]. void removeMessage(Message message) async { await _channel._client.chatPersistenceClient?.deleteMessageById(message.id); - final parentId = message.parentId; - // i.e. it's a thread message, Remove it - if (parentId != null) { - final newThreads = {...threads}; - // Early return in case the thread is not available - if (!newThreads.containsKey(parentId)) return; - - // Remove thread message shown in thread page. - newThreads.update( - parentId, - (messages) => [...messages.where((e) => e.id != message.id)], - ); + if (message.parentId == null || message.showInChannel == true) { + // Remove regular message, thread message shown in channel + var updatedMessages = [...messages.where((e) => e.id != message.id)]; - _threads = newThreads; + // Remove quoted message reference from every message if available. + updatedMessages = [ + ...updatedMessages.map((it) { + // Early return if the message doesn't have a quoted message. + if (it.quotedMessageId != message.id) return it; + + // Setting it to null will remove the quoted message from the message. + return it.copyWith(quotedMessage: null, quotedMessageId: null); + }), + ]; - // Early return if the thread message is not shown in channel. - if (message.showInChannel == false) return; + _channelState = _channelState.copyWith( + messages: updatedMessages.sorted(_sortByCreatedAt), + ); } - // Remove regular message, thread message shown in channel - var updatedMessages = [...messages]..removeWhere((e) => e.id == message.id); + if (message.parentId case final parentId?) { + // If the message is a thread message, remove it from the threads. + _removeThreadMessage(message, parentId); + } - // Remove quoted message reference from every message if available. - updatedMessages = [...updatedMessages].map((it) { - // Early return if the message doesn't have a quoted message. - if (it.quotedMessageId != message.id) return it; + // If the message is pinned, remove it from the pinned messages. + final updatedPinnedMessages = [ + ...pinnedMessages.where((it) => it.id != message.id), + ]; - // Setting it to null will remove the quoted message from the message. - return it.copyWith( - quotedMessage: null, - quotedMessageId: null, - ); - }).toList(); + // If the message is a live location, update the active live locations. + final updatedActiveLiveLocations = [ + ...activeLiveLocations.where((it) => it.messageId != message.id), + ]; _channelState = _channelState.copyWith( - messages: updatedMessages, + pinnedMessages: updatedPinnedMessages, + activeLiveLocations: updatedActiveLiveLocations, ); } + void _removeThreadMessage(Message message, String parentId) { + final existingThreadMessages = threads[parentId] ?? []; + final updatedThreadMessages = [ + ...existingThreadMessages.where((it) => it.id != message.id), + ]; + + // If there are no more messages in the thread, remove the thread entry. + if (updatedThreadMessages.isEmpty) { + _threads = {...threads}..remove(parentId); + return; + } + + // Otherwise, update the thread messages. + _threads = { + ...threads, + parentId: updatedThreadMessages.sorted(_sortByCreatedAt), + }; + } + /// Removes/Updates the [message] based on the [hardDelete] value. void deleteMessage(Message message, {bool hardDelete = false}) { if (hardDelete) return removeMessage(message); @@ -3152,6 +3432,16 @@ class ChannelClientState { (watchers, users) => [...?watchers?.map((e) => users[e.id] ?? e)], ).distinct(const ListEquality().equals); + /// Channel active live locations. + List get activeLiveLocations { + return _channelState.activeLiveLocations ?? []; + } + + /// Channel active live locations as a stream. + Stream> get activeLiveLocationsStream => channelStateStream + .map((cs) => cs.activeLiveLocations ?? []) + .distinct(const ListEquality().equals); + /// Channel draft. Draft? get draft => _channelState.draft; @@ -3321,6 +3611,7 @@ class ChannelClientState { draft: updatedState.draft, pinnedMessages: updatedState.pinnedMessages, pushPreferences: updatedState.pushPreferences, + activeLiveLocations: updatedState.activeLiveLocations, ); } @@ -3370,19 +3661,19 @@ class ChannelClientState { /// Update threads with updated information about messages. void updateThreadInfo(String parentId, List messages) { - final newThreads = {...threads}..update( - parentId, - (original) => [ - ...original.merge( - messages, - key: (message) => message.id, - update: (original, updated) => updated.syncWith(original), - ), - ].sorted(_sortByCreatedAt), - ifAbsent: () => messages.sorted(_sortByCreatedAt), - ); + final existingThreadMessages = threads[parentId] ?? []; + final updatedThreadMessages = [ + ...existingThreadMessages.merge( + messages, + key: (it) => it.id, + update: (original, updated) => updated.syncWith(original), + ), + ]; - _threads = newThreads; + _threads = { + ...threads, + parentId: updatedThreadMessages.sorted(_sortByCreatedAt) + }; } Draft? _getThreadDraft(String parentId, List? messages) { @@ -3497,6 +3788,42 @@ class ChannelClientState { ); } + Timer? _staleLiveLocationsCleanerTimer; + void _startCleaningExpiredLocations() { + _staleLiveLocationsCleanerTimer?.cancel(); + _staleLiveLocationsCleanerTimer = Timer.periodic( + const Duration(seconds: 1), + (_) { + final currentUserId = _channel._client.state.currentUser?.id; + if (currentUserId == null) return; + + final expired = activeLiveLocations.where((it) => it.isExpired); + if (expired.isEmpty) return; + + for (final sharedLocation in expired) { + // Skip if the location is shared by the current user, + // as we are already handling them in the client. + if (sharedLocation.userId == currentUserId) continue; + + final lastUpdatedAt = DateTime.timestamp(); + final locationExpiredEvent = Event( + type: EventType.locationExpired, + cid: sharedLocation.channelCid, + message: Message( + id: sharedLocation.messageId, + updatedAt: lastUpdatedAt, + sharedLocation: sharedLocation.copyWith( + updatedAt: lastUpdatedAt, + ), + ), + ); + + _channel._client.handleEvent(locationExpiredEvent); + } + }, + ); + } + /// Call this method to dispose this object. void dispose() { _debouncedUpdatePersistenceChannelState.cancel(); @@ -3507,6 +3834,7 @@ class ChannelClientState { _threadsController.close(); _staleTypingEventsCleanerTimer?.cancel(); _stalePinnedMessagesCleanerTimer?.cancel(); + _staleLiveLocationsCleanerTimer?.cancel(); _typingEventsController.close(); } } @@ -3698,4 +4026,9 @@ extension ChannelCapabilityCheck on Channel { bool get canQueryPollVotes { return ownCapabilities.contains(ChannelCapability.queryPollVotes); } + + /// True, if the current user can share location in the channel. + bool get canShareLocation { + return ownCapabilities.contains(ChannelCapability.shareLocation); + } } diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 7897b83ec5..9ce7b13c03 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -5,6 +5,7 @@ import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/client/channel.dart'; +import 'package:stream_chat/src/client/event_resolvers.dart' as event_resolvers; import 'package:stream_chat/src/client/retry_policy.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; import 'package:stream_chat/src/core/api/requests.dart'; @@ -24,6 +25,8 @@ import 'package:stream_chat/src/core/models/draft.dart'; import 'package:stream_chat/src/core/models/draft_message.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/location.dart'; +import 'package:stream_chat/src/core/models/location_coordinates.dart'; import 'package:stream_chat/src/core/models/member.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/message_reminder.dart'; @@ -32,8 +35,11 @@ import 'package:stream_chat/src/core/models/poll.dart'; import 'package:stream_chat/src/core/models/poll_option.dart'; import 'package:stream_chat/src/core/models/poll_vote.dart'; import 'package:stream_chat/src/core/models/push_preference.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; import 'package:stream_chat/src/core/models/thread.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/core/util/event_controller.dart'; +import 'package:stream_chat/src/core/util/extension.dart'; import 'package:stream_chat/src/core/util/utils.dart'; import 'package:stream_chat/src/db/chat_persistence_client.dart'; import 'package:stream_chat/src/event_type.dart'; @@ -230,21 +236,19 @@ class StreamChatClient { StreamSubscription>? _connectionStatusSubscription; - final _eventController = PublishSubject(); - /// Stream of [Event] coming from [_ws] connection /// Listen to this or use the [on] method to filter specific event types - Stream get eventStream => _eventController.stream.map( - // If the poll vote is an answer, we should emit a different event - // to make it easier to handle in the state. - (event) => switch ((event.type, event.pollVote?.isAnswer == true)) { - (EventType.pollVoteCasted || EventType.pollVoteChanged, true) => - event.copyWith(type: EventType.pollAnswerCasted), - (EventType.pollVoteRemoved, true) => - event.copyWith(type: EventType.pollAnswerRemoved), - _ => event, - }, - ); + Stream get eventStream => _eventController.stream; + late final _eventController = EventController( + resolvers: [ + event_resolvers.pollCreatedResolver, + event_resolvers.pollAnswerCastedResolver, + event_resolvers.pollAnswerRemovedResolver, + event_resolvers.locationSharedResolver, + event_resolvers.locationUpdatedResolver, + event_resolvers.locationExpiredResolver, + ], + ); /// The current status value of the [_ws] connection ConnectionStatus get wsConnectionStatus => _ws.connectionStatus; @@ -691,27 +695,6 @@ class StreamChatClient { } } - /// Returns a token associated with the [callId]. - @Deprecated('Will be removed in the next major version') - Future getCallToken(String callId) async => - _chatApi.call.getCallToken(callId); - - /// Creates a new call. - @Deprecated('Will be removed in the next major version') - Future createCall({ - required String callId, - required String callType, - required String channelType, - required String channelId, - }) { - return _chatApi.call.createCall( - callId: callId, - callType: callType, - channelType: channelType, - channelId: channelId, - ); - } - /// Requests channels with a given query from the API. Future> queryChannelsOnline({ Filter? filter, @@ -1645,23 +1628,16 @@ class StreamChatClient { /// Set [enforceUnique] to true to remove the existing user reaction Future sendReaction( String messageId, - String reactionType, { - int score = 1, - Map extraData = const {}, + Reaction reaction, { + bool skipPush = false, bool enforceUnique = false, - }) { - final _extraData = { - 'score': score, - ...extraData, - }; - - return _chatApi.message.sendReaction( - messageId, - reactionType, - extraData: _extraData, - enforceUnique: enforceUnique, - ); - } + }) => + _chatApi.message.sendReaction( + messageId, + reaction, + skipPush: skipPush, + enforceUnique: enforceUnique, + ); /// Delete a [reactionType] from this [messageId] Future deleteReaction( @@ -1832,6 +1808,51 @@ class StreamChatClient { pagination: pagination, ); + /// Retrieves all the active live locations of the current user. + Future getActiveLiveLocations() async { + try { + final response = await _chatApi.user.getActiveLiveLocations(); + + // Update the active live locations in the state. + final activeLiveLocations = response.activeLiveLocations; + state.activeLiveLocations = activeLiveLocations; + + return response; + } catch (e, stk) { + logger.severe('Error getting active live locations', e, stk); + rethrow; + } + } + + /// Updates an existing live location created by the current user. + Future updateLiveLocation({ + required String messageId, + String? createdByDeviceId, + LocationCoordinates? location, + DateTime? endAt, + }) { + return _chatApi.user.updateLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + location: location, + endAt: endAt, + ); + } + + /// Expire an existing live location created by the current user. + Future stopLiveLocation({ + required String messageId, + String? createdByDeviceId, + }) { + return updateLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + // Passing the current time as endAt will mark the location as expired + // and make it inactive. + endAt: DateTime.timestamp(), + ); + } + /// Enables slow mode Future enableSlowdown( String channelId, @@ -2132,15 +2153,27 @@ class ClientState { }), ); + // region CHANNEL EVENTS _listenChannelLeft(); - _listenChannelDeleted(); - _listenChannelHidden(); + // endregion + // region USER EVENTS _listenUserUpdated(); + // endregion + // region READ EVENTS _listenAllChannelsRead(); + // endregion + + // region LOCATION EVENTS + _listenLocationShared(); + _listenLocationUpdated(); + _listenLocationExpired(); + // endregion + + _startCleaningExpiredLocations(); } /// Stops listening to the client events. @@ -2243,6 +2276,103 @@ class ClientState { ); } + void _listenLocationShared() { + _eventsSubscription?.add( + _client.on(EventType.locationShared).listen((event) { + final location = event.message?.sharedLocation; + if (location == null || location.isStatic) return; + + final currentUserId = currentUser?.id; + if (currentUserId == null) return; + if (location.userId != currentUserId) return; + + final newActiveLiveLocations = [ + ...activeLiveLocations.merge( + [location], + key: (it) => (it.userId, it.channelCid, it.createdByDeviceId), + update: (original, updated) => updated, + ), + ]; + + activeLiveLocations = newActiveLiveLocations; + }), + ); + } + + void _listenLocationUpdated() { + _eventsSubscription?.add( + _client.on(EventType.locationUpdated).listen((event) { + final location = event.message?.sharedLocation; + if (location == null || location.isStatic) return; + + final currentUserId = currentUser?.id; + if (currentUserId == null) return; + if (location.userId != currentUserId) return; + + final newActiveLiveLocations = [ + ...activeLiveLocations.merge( + [location], + key: (it) => (it.userId, it.channelCid, it.createdByDeviceId), + update: (original, updated) => updated, + ), + ]; + + activeLiveLocations = newActiveLiveLocations; + }), + ); + } + + void _listenLocationExpired() { + _eventsSubscription?.add( + _client.on(EventType.locationExpired).listen((event) { + final location = event.message?.sharedLocation; + if (location == null || location.isStatic) return; + + final currentUserId = currentUser?.id; + if (currentUserId == null) return; + if (location.userId != currentUserId) return; + + final newActiveLiveLocations = [ + ...activeLiveLocations.where( + (it) => it.messageId != location.messageId, + ) + ]; + + activeLiveLocations = newActiveLiveLocations; + }), + ); + } + + Timer? _staleLiveLocationsCleanerTimer; + void _startCleaningExpiredLocations() { + _staleLiveLocationsCleanerTimer?.cancel(); + _staleLiveLocationsCleanerTimer = Timer.periodic( + const Duration(seconds: 1), + (_) { + final expired = activeLiveLocations.where((it) => it.isExpired); + if (expired.isEmpty) return; + + for (final sharedLocation in expired) { + final lastUpdatedAt = DateTime.timestamp(); + + final locationExpiredEvent = Event( + type: EventType.locationExpired, + cid: sharedLocation.channelCid, + message: Message( + id: sharedLocation.messageId, + updatedAt: lastUpdatedAt, + sharedLocation: sharedLocation.copyWith( + updatedAt: lastUpdatedAt, + ), + ), + ); + + _client.handleEvent(locationExpiredEvent); + } + }, + ); + } + final StreamChatClient _client; /// Sets the user currently interacting with the client @@ -2277,6 +2407,23 @@ class ClientState { /// The current user as a stream Stream> get usersStream => _usersController.stream; + /// The current active live locations shared by the user. + List get activeLiveLocations { + return _activeLiveLocationsController.value; + } + + /// The current active live locations shared by the user as a stream. + Stream> get activeLiveLocationsStream { + return _activeLiveLocationsController.stream; + } + + /// Sets the active live locations. + set activeLiveLocations(List locations) { + // For safe-keeping, we filter out any inactive locations before update. + final activeLocations = [...locations.where((it) => it.isActive)]; + _activeLiveLocationsController.add(activeLocations); + } + /// The current unread channels count int get unreadChannels => _unreadChannelsController.value; @@ -2349,14 +2496,18 @@ class ClientState { final _unreadChannelsController = BehaviorSubject.seeded(0); final _unreadThreadsController = BehaviorSubject.seeded(0); final _totalUnreadCountController = BehaviorSubject.seeded(0); + final _activeLiveLocationsController = BehaviorSubject.seeded([]); /// Call this method to dispose this object void dispose() { cancelEventSubscription(); _currentUserController.close(); + _usersController.close(); _unreadChannelsController.close(); _unreadThreadsController.close(); _totalUnreadCountController.close(); + _activeLiveLocationsController.close(); + _staleLiveLocationsCleanerTimer?.cancel(); final channels = [...this.channels.keys]; for (final channel in channels) { diff --git a/packages/stream_chat/lib/src/client/event_resolvers.dart b/packages/stream_chat/lib/src/client/event_resolvers.dart new file mode 100644 index 0000000000..6f462f6260 --- /dev/null +++ b/packages/stream_chat/lib/src/client/event_resolvers.dart @@ -0,0 +1,129 @@ +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/event_type.dart'; + +/// Resolves message new events into more specific `pollCreated` events +/// for easier downstream state handling. +/// +/// Applies when: +/// - `event.type` is `messageNew` or `notification.message_new, and +/// - `event.poll` is not null +/// +/// Returns a modified event with type `pollCreated`, +/// or `null` if not applicable. +Event? pollCreatedResolver(Event event) { + final validTypes = {EventType.messageNew, EventType.notificationMessageNew}; + if (!validTypes.contains(event.type)) return null; + + final poll = event.poll; + if (poll == null) return null; + + // If the event is a message new or notification message new and + // it contains a poll, we can resolve it to a poll created event. + return event.copyWith(type: EventType.pollCreated); +} + +/// Resolves casted or changed poll vote events into more specific +/// `pollAnswerCasted` events for easier downstream state handling. +/// +/// Applies when: +/// - `event.type` is `pollVoteCasted` or `pollVoteChanged`, and +/// - `event.pollVote?.isAnswer == true` +/// +/// Returns a modified event with type `pollAnswerCasted`, +/// or `null` if not applicable. +Event? pollAnswerCastedResolver(Event event) { + final validTypes = {EventType.pollVoteCasted, EventType.pollVoteChanged}; + if (!validTypes.contains(event.type)) return null; + + final pollVote = event.pollVote; + if (pollVote?.isAnswer == false) return null; + + // If the event is a poll vote casted or changed and it's an answer + // we can resolve it to a poll answer casted event. + return event.copyWith(type: EventType.pollAnswerCasted); +} + +/// Resolves removed poll vote events into more specific +/// `pollAnswerRemoved` events for easier downstream state handling. +/// +/// Applies when: +/// - `event.type` is `pollVoteRemoved`, and +/// - `event.pollVote?.isAnswer == true` +/// +/// Returns a modified event with type `pollAnswerRemoved`, +/// or `null` if not applicable. +Event? pollAnswerRemovedResolver(Event event) { + if (event.type != EventType.pollVoteRemoved) return null; + + final pollVote = event.pollVote; + if (pollVote?.isAnswer == false) return null; + + // If the event is a poll vote removed and it's an answer + // we can resolve it to a poll answer removed event. + return event.copyWith(type: EventType.pollAnswerRemoved); +} + +/// Resolves message new events into more specific `locationShared` events +/// for easier downstream state handling. +/// +/// Applies when: +/// - `event.type` is `messageNew` or `notification.message_new, and +/// - `event.message.sharedLocation` is not null +/// +/// Returns a modified event with type `locationShared`, +/// or `null` if not applicable. +Event? locationSharedResolver(Event event) { + final validTypes = {EventType.messageNew, EventType.notificationMessageNew}; + if (!validTypes.contains(event.type)) return null; + + final sharedLocation = event.message?.sharedLocation; + if (sharedLocation == null) return null; + + // If the event is a message new or notification message new and it + // contains a shared location, we can resolve it to a location shared event. + return event.copyWith(type: EventType.locationShared); +} + +/// Resolves message updated events into more specific `locationUpdated` +/// events for easier downstream state handling. +/// +/// Applies when: +/// - `event.type` is `messageUpdated`, and +/// - `event.message.sharedLocation` is not null and not expired +/// +/// Returns a modified event with type `locationUpdated`, +/// or `null` if not applicable. +Event? locationUpdatedResolver(Event event) { + if (event.type != EventType.messageUpdated) return null; + + final sharedLocation = event.message?.sharedLocation; + if (sharedLocation == null) return null; + + if (sharedLocation.isLive && sharedLocation.isExpired) return null; + + // If the location is static or still active, we can resolve it + // to a location updated event. + return event.copyWith(type: EventType.locationUpdated); +} + +/// Resolves message updated events into more specific `locationExpired` +/// events for easier downstream state handling. +/// +/// Applies when: +/// - `event.type` is `messageUpdated`, and +/// - `event.message.sharedLocation` is not null and expired +/// +/// Returns a modified event with type `locationExpired`, +/// or `null` if not applicable. +Event? locationExpiredResolver(Event event) { + if (event.type != EventType.messageUpdated) return null; + + final sharedLocation = event.message?.sharedLocation; + if (sharedLocation == null) return null; + + if (sharedLocation.isStatic || sharedLocation.isActive) return null; + + // If the location is live and expired, we can resolve it to a + // location expired event. + return event.copyWith(type: EventType.locationExpired); +} diff --git a/packages/stream_chat/lib/src/client/retry_queue.dart b/packages/stream_chat/lib/src/client/retry_queue.dart index 8675ee68b7..c1f8841406 100644 --- a/packages/stream_chat/lib/src/client/retry_queue.dart +++ b/packages/stream_chat/lib/src/client/retry_queue.dart @@ -119,10 +119,11 @@ class RetryQueue { } static DateTime? _getMessageDate(Message message) { - return message.state.maybeWhen( - failed: (state, _) => state.when( - sendingFailed: () => message.createdAt, - updatingFailed: () => message.updatedAt, + return message.state.maybeMap( + failed: (it) => it.state.map( + sendingFailed: (_) => message.createdAt, + updatingFailed: (_) => message.updatedAt, + partialUpdatingFailed: (_) => message.updatedAt, deletingFailed: (_) => message.deletedAt, ), orElse: () => null, diff --git a/packages/stream_chat/lib/src/core/api/call_api.dart b/packages/stream_chat/lib/src/core/api/call_api.dart deleted file mode 100644 index 4981cb0a93..0000000000 --- a/packages/stream_chat/lib/src/core/api/call_api.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:stream_chat/src/core/api/responses.dart'; -import 'package:stream_chat/src/core/http/stream_http_client.dart'; - -/// Defines the api dedicated to call operations. -@Deprecated('Will be removed in the next major version') -class CallApi { - /// Initialize a new call api - CallApi(this._client); - - final StreamHttpClient _client; - - /// Returns a token dedicated to the [callId] - Future getCallToken(String callId) async { - final response = await _client.post( - '/calls/$callId', - data: {}, - ); - // return response.data; - return CallTokenPayload.fromJson(response.data); - } - - /// Creates a new call - Future createCall({ - required String callId, - required String callType, - required String channelType, - required String channelId, - }) async { - final response = await _client.post( - _getChannelUrl(channelId, channelType), - data: { - 'id': callId, - 'type': callType, - }, - ); - // return response.data; - return CreateCallPayload.fromJson(response.data); - } - - String _getChannelUrl(String channelId, String channelType) => - '/channels/$channelType/$channelId/call'; -} diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart index 442e39761f..19a363bb3b 100644 --- a/packages/stream_chat/lib/src/core/api/message_api.dart +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -8,6 +8,7 @@ import 'package:stream_chat/src/core/models/draft.dart'; import 'package:stream_chat/src/core/models/draft_message.dart'; import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/reaction.dart'; /// Defines the api dedicated to messages operations class MessageApi { @@ -210,19 +211,17 @@ class MessageApi { /// Set [enforceUnique] to true to remove the existing user reaction Future sendReaction( String messageId, - String reactionType, { - Map extraData = const {}, + Reaction reaction, { + bool skipPush = false, bool enforceUnique = false, }) async { - final reaction = Map.from(extraData) - ..addAll({'type': reactionType}); - final response = await _client.post( '/messages/$messageId/reaction', - data: { - 'reaction': reaction, + data: json.encode({ + 'reaction': reaction.toJson(), + 'skip_push': skipPush, 'enforce_unique': enforceUnique, - }, + }), ); return SendReactionResponse.fromJson(response.data); } diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index cbd4505678..c1f6703634 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -1,14 +1,13 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/client/client.dart'; -import 'package:stream_chat/src/core/api/call_api.dart'; import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/core/models/banned_user.dart'; -import 'package:stream_chat/src/core/models/call_payload.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/device.dart'; import 'package:stream_chat/src/core/models/draft.dart'; import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/location.dart'; import 'package:stream_chat/src/core/models/member.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/message_reminder.dart'; @@ -515,36 +514,6 @@ class OGAttachmentResponse extends _BaseResponse { _$OGAttachmentResponseFromJson(json); } -/// The response to [CallApi.getCallToken] -@Deprecated('Will be removed in the next major version') -@JsonSerializable(createToJson: false) -class CallTokenPayload extends _BaseResponse { - /// Create a new instance from a [json]. - static CallTokenPayload fromJson(Map json) => - _$CallTokenPayloadFromJson(json); - - /// The token to use for the call. - String? token; - - /// The user id specific to Agora. - int? agoraUid; - - /// The appId specific to Agora. - String? agoraAppId; -} - -/// The response to [CallApi.createCall] -@Deprecated('Will be removed in the next major version') -@JsonSerializable(createToJson: false) -class CreateCallPayload extends _BaseResponse { - /// Create a new instance from a [json]. - static CreateCallPayload fromJson(Map json) => - _$CreateCallPayloadFromJson(json); - - /// The call object. - CallPayload? call; -} - /// Contains information about a [User] that was banned from a [Channel] or App. @JsonSerializable(createToJson: false) class UserBlockResponse extends _BaseResponse { @@ -846,3 +815,14 @@ class UpsertPushPreferencesResponse extends _BaseResponse { static UpsertPushPreferencesResponse fromJson(Map json) => _$UpsertPushPreferencesResponseFromJson(json); } + +/// Model response for [StreamChatClient.updateDraft] api call +@JsonSerializable(createToJson: false) +class GetActiveLiveLocationsResponse extends _BaseResponse { + /// List of active live locations returned by the api call + late List activeLiveLocations; + + /// Create a new instance from a json + static GetActiveLiveLocationsResponse fromJson(Map json) => + _$GetActiveLiveLocationsResponseFromJson(json); +} diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index 30dd445960..adfa731942 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -307,20 +307,6 @@ OGAttachmentResponse _$OGAttachmentResponseFromJson( ..titleLink = json['title_link'] as String? ..type = json['type'] as String?; -CallTokenPayload _$CallTokenPayloadFromJson(Map json) => - CallTokenPayload() - ..duration = json['duration'] as String? - ..token = json['token'] as String? - ..agoraUid = (json['agora_uid'] as num?)?.toInt() - ..agoraAppId = json['agora_app_id'] as String?; - -CreateCallPayload _$CreateCallPayloadFromJson(Map json) => - CreateCallPayload() - ..duration = json['duration'] as String? - ..call = json['call'] == null - ? null - : CallPayload.fromJson(json['call'] as Map); - UserBlockResponse _$UserBlockResponseFromJson(Map json) => UserBlockResponse() ..duration = json['duration'] as String? @@ -512,3 +498,11 @@ UpsertPushPreferencesResponse _$UpsertPushPreferencesResponseFromJson( )), ) ?? {}; + +GetActiveLiveLocationsResponse _$GetActiveLiveLocationsResponseFromJson( + Map json) => + GetActiveLiveLocationsResponse() + ..duration = json['duration'] as String? + ..activeLiveLocations = (json['active_live_locations'] as List) + .map((e) => Location.fromJson(e as Map)) + .toList(); diff --git a/packages/stream_chat/lib/src/core/api/sort_order.dart b/packages/stream_chat/lib/src/core/api/sort_order.dart index e37fd445e3..e04ca10264 100644 --- a/packages/stream_chat/lib/src/core/api/sort_order.dart +++ b/packages/stream_chat/lib/src/core/api/sort_order.dart @@ -34,21 +34,8 @@ enum NullOrdering { /// // Sort channels by last message date in descending order /// final sort = SortOption("last_message_at"); /// ``` -@JsonSerializable(includeIfNull: false) +@JsonSerializable(createFactory: false, includeIfNull: false) class SortOption { - /// Creates a new SortOption instance with the specified field and direction. - /// - /// ```dart - /// final sorting = SortOption("last_message_at") // Default: descending order - /// ``` - @Deprecated('Use SortOption.desc or SortOption.asc instead') - const SortOption( - this.field, { - this.direction = SortOption.DESC, - this.nullOrdering = NullOrdering.nullsFirst, - Comparator? comparator, - }) : _comparator = comparator; - /// Creates a SortOption for descending order sorting by the specified field. /// /// Example: @@ -77,10 +64,6 @@ class SortOption { }) : direction = SortOption.ASC, _comparator = comparator; - /// Create a new instance from JSON. - factory SortOption.fromJson(Map json) => - _$SortOptionFromJson(json); - /// Ascending order (1) static const ASC = 1; diff --git a/packages/stream_chat/lib/src/core/api/sort_order.g.dart b/packages/stream_chat/lib/src/core/api/sort_order.g.dart index 1a4e70f1fe..31f18e61c9 100644 --- a/packages/stream_chat/lib/src/core/api/sort_order.g.dart +++ b/packages/stream_chat/lib/src/core/api/sort_order.g.dart @@ -6,13 +6,6 @@ part of 'sort_order.dart'; // JsonSerializableGenerator // ************************************************************************** -SortOption _$SortOptionFromJson( - Map json) => - SortOption( - json['field'] as String, - direction: (json['direction'] as num?)?.toInt() ?? SortOption.DESC, - ); - Map _$SortOptionToJson( SortOption instance) => { diff --git a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart index 1f94ffe77a..85cbf4aa7e 100644 --- a/packages/stream_chat/lib/src/core/api/stream_chat_api.dart +++ b/packages/stream_chat/lib/src/core/api/stream_chat_api.dart @@ -1,7 +1,6 @@ import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; -import 'package:stream_chat/src/core/api/call_api.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; import 'package:stream_chat/src/core/api/device_api.dart'; import 'package:stream_chat/src/core/api/general_api.dart'; @@ -70,12 +69,6 @@ class StreamChatApi { ThreadsApi get threads => _threads ??= ThreadsApi(_client); ThreadsApi? _threads; - /// Api dedicated to call operations - @Deprecated('Will be removed in the next major version') - CallApi get call => _call ??= CallApi(_client); - @Deprecated('Will be removed in the next major version') - CallApi? _call; - /// Api dedicated to channel operations ChannelApi get channel => _channel ??= ChannelApi(_client); ChannelApi? _channel; diff --git a/packages/stream_chat/lib/src/core/api/user_api.dart b/packages/stream_chat/lib/src/core/api/user_api.dart index 064792bfe4..668e7ac70a 100644 --- a/packages/stream_chat/lib/src/core/api/user_api.dart +++ b/packages/stream_chat/lib/src/core/api/user_api.dart @@ -5,6 +5,8 @@ import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/api/sort_order.dart'; import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/location.dart'; +import 'package:stream_chat/src/core/models/location_coordinates.dart'; import 'package:stream_chat/src/core/models/user.dart'; /// Defines the api dedicated to users operations @@ -95,4 +97,35 @@ class UserApi { return GetUnreadCountResponse.fromJson(response.data); } + + /// Retrieves all the active live locations of the current user. + Future getActiveLiveLocations() async { + final response = await _client.get( + '/users/live_locations', + ); + + return GetActiveLiveLocationsResponse.fromJson(response.data); + } + + /// Updates an existing live location created by the current user. + Future updateLiveLocation({ + required String messageId, + String? createdByDeviceId, + LocationCoordinates? location, + DateTime? endAt, + }) async { + final response = await _client.put( + '/users/live_locations', + data: json.encode({ + 'message_id': messageId, + if (createdByDeviceId != null) + 'created_by_device_id': createdByDeviceId, + if (location?.latitude case final latitude) 'latitude': latitude, + if (location?.longitude case final longitude) 'longitude': longitude, + if (endAt != null) 'end_at': endAt.toIso8601String(), + }), + ); + + return Location.fromJson(response.data); + } } diff --git a/packages/stream_chat/lib/src/core/models/call_payload.dart b/packages/stream_chat/lib/src/core/models/call_payload.dart deleted file mode 100644 index 2f1fdf206b..0000000000 --- a/packages/stream_chat/lib/src/core/models/call_payload.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'call_payload.g.dart'; - -/// Model containing the information about a call. -@JsonSerializable(createToJson: false) -@Deprecated('Will be removed in the next major version') -class CallPayload extends Equatable { - /// Create a new instance. - const CallPayload({ - required this.id, - required this.provider, - this.agora, - this.hms, - }); - - /// Create a new instance from a [json]. - factory CallPayload.fromJson(Map json) => - _$CallPayloadFromJson(json); - - /// The call id. - final String id; - - /// The call provider. - final String provider; - - /// The payload specific to Agora. - final AgoraPayload? agora; - - /// The payload specific to 100ms. - final HMSPayload? hms; - - @override - List get props => [id, provider, agora, hms]; -} - -/// Payload for Agora call. -@JsonSerializable(createToJson: false) -class AgoraPayload extends Equatable { - /// Create a new instance. - const AgoraPayload({required this.channel}); - - /// Create a new instance from a [json]. - factory AgoraPayload.fromJson(Map json) => - _$AgoraPayloadFromJson(json); - - /// The Agora channel. - final String channel; - - @override - List get props => [channel]; -} - -/// Payload for 100ms call. -@JsonSerializable(createToJson: false) -class HMSPayload extends Equatable { - /// Create a new instance. - const HMSPayload({required this.roomId, required this.roomName}); - - /// Create a new instance from a [json]. - factory HMSPayload.fromJson(Map json) => - _$HMSPayloadFromJson(json); - - /// The id of the 100ms room. - final String roomId; - - /// The name of the 100ms room. - final String roomName; - - @override - List get props => [roomId, roomName]; -} diff --git a/packages/stream_chat/lib/src/core/models/call_payload.g.dart b/packages/stream_chat/lib/src/core/models/call_payload.g.dart deleted file mode 100644 index bc786cc5db..0000000000 --- a/packages/stream_chat/lib/src/core/models/call_payload.g.dart +++ /dev/null @@ -1,27 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'call_payload.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -CallPayload _$CallPayloadFromJson(Map json) => CallPayload( - id: json['id'] as String, - provider: json['provider'] as String, - agora: json['agora'] == null - ? null - : AgoraPayload.fromJson(json['agora'] as Map), - hms: json['hms'] == null - ? null - : HMSPayload.fromJson(json['hms'] as Map), - ); - -AgoraPayload _$AgoraPayloadFromJson(Map json) => AgoraPayload( - channel: json['channel'] as String, - ); - -HMSPayload _$HMSPayloadFromJson(Map json) => HMSPayload( - roomId: json['room_id'] as String, - roomName: json['room_name'] as String, - ); diff --git a/packages/stream_chat/lib/src/core/models/channel_config.dart b/packages/stream_chat/lib/src/core/models/channel_config.dart index 2bcf0a1ed5..489b71e334 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.dart @@ -26,6 +26,7 @@ class ChannelConfig { this.urlEnrichment = false, this.skipLastMsgUpdateForSystemMsgs = false, this.userMessageReminders = false, + this.sharedLocations = false, }) : createdAt = createdAt ?? DateTime.now(), updatedAt = updatedAt ?? DateTime.now(); @@ -91,6 +92,9 @@ class ChannelConfig { /// True if the user can set reminders for messages in this channel. final bool userMessageReminders; + /// True if shared locations are enabled for this channel. + final bool sharedLocations; + /// Serialize to json Map toJson() => _$ChannelConfigToJson(this); } diff --git a/packages/stream_chat/lib/src/core/models/channel_config.g.dart b/packages/stream_chat/lib/src/core/models/channel_config.g.dart index 8cb758b875..1a57d3e743 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.g.dart @@ -34,6 +34,7 @@ ChannelConfig _$ChannelConfigFromJson(Map json) => skipLastMsgUpdateForSystemMsgs: json['skip_last_msg_update_for_system_msgs'] as bool? ?? false, userMessageReminders: json['user_message_reminders'] as bool? ?? false, + sharedLocations: json['shared_locations'] as bool? ?? false, ); Map _$ChannelConfigToJson(ChannelConfig instance) => @@ -57,4 +58,5 @@ Map _$ChannelConfigToJson(ChannelConfig instance) => 'skip_last_msg_update_for_system_msgs': instance.skipLastMsgUpdateForSystemMsgs, 'user_message_reminders': instance.userMessageReminders, + 'shared_locations': instance.sharedLocations, }; diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index d55f0e42b5..49a3c99bf2 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -366,4 +366,7 @@ extension type const ChannelCapability(String capability) implements String { /// Ability to query poll votes. static const queryPollVotes = ChannelCapability('query-poll-votes'); + + /// Ability to share location. + static const shareLocation = ChannelCapability('share-location'); } diff --git a/packages/stream_chat/lib/src/core/models/channel_state.dart b/packages/stream_chat/lib/src/core/models/channel_state.dart index 601ef679a2..67096781ce 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.dart @@ -2,6 +2,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_model.dart'; import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/src/core/models/draft.dart'; +import 'package:stream_chat/src/core/models/location.dart'; import 'package:stream_chat/src/core/models/member.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/push_preference.dart'; @@ -31,6 +32,7 @@ class ChannelState implements ComparableFieldProvider { this.membership, this.draft, this.pushPreferences, + this.activeLiveLocations, }); /// The channel to which this state belongs @@ -63,6 +65,9 @@ class ChannelState implements ComparableFieldProvider { /// The push preferences for this channel if it exists. final ChannelPushPreference? pushPreferences; + /// The list of active live locations in the channel. + final List? activeLiveLocations; + /// Create a new instance from a json static ChannelState fromJson(Map json) => _$ChannelStateFromJson(json); @@ -82,6 +87,7 @@ class ChannelState implements ComparableFieldProvider { Member? membership, Object? draft = _nullConst, ChannelPushPreference? pushPreferences, + List? activeLiveLocations, }) => ChannelState( channel: channel ?? this.channel, @@ -94,6 +100,7 @@ class ChannelState implements ComparableFieldProvider { membership: membership ?? this.membership, draft: draft == _nullConst ? this.draft : draft as Draft?, pushPreferences: pushPreferences ?? this.pushPreferences, + activeLiveLocations: activeLiveLocations ?? this.activeLiveLocations, ); @override diff --git a/packages/stream_chat/lib/src/core/models/channel_state.g.dart b/packages/stream_chat/lib/src/core/models/channel_state.g.dart index e154f1235c..3f1efb4d1b 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.g.dart @@ -36,6 +36,9 @@ ChannelState _$ChannelStateFromJson(Map json) => ChannelState( ? null : ChannelPushPreference.fromJson( json['push_preferences'] as Map), + activeLiveLocations: (json['active_live_locations'] as List?) + ?.map((e) => Location.fromJson(e as Map)) + .toList(), ); Map _$ChannelStateToJson(ChannelState instance) => @@ -51,4 +54,6 @@ Map _$ChannelStateToJson(ChannelState instance) => 'membership': instance.membership?.toJson(), 'draft': instance.draft?.toJson(), 'push_preferences': instance.pushPreferences?.toJson(), + 'active_live_locations': + instance.activeLiveLocations?.map((e) => e.toJson()).toList(), }; diff --git a/packages/stream_chat/lib/src/core/models/location.dart b/packages/stream_chat/lib/src/core/models/location.dart new file mode 100644 index 0000000000..008ab6390d --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/location.dart @@ -0,0 +1,157 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/location_coordinates.dart'; +import 'package:stream_chat/src/core/models/message.dart'; + +part 'location.g.dart'; + +/// {@template location} +/// A model class representing a shared location. +/// +/// The [Location] represents a location shared in a channel message. +/// +/// It can be of two types: +/// 1. **Static Location**: A location that does not change over time and has +/// no end time. +/// 2. **Live Location**: A location that updates in real-time and has an +/// end time. +/// {@endtemplate} +@JsonSerializable() +class Location extends Equatable { + /// {@macro location} + Location({ + this.channelCid, + this.channel, + this.messageId, + this.message, + this.userId, + required this.latitude, + required this.longitude, + this.createdByDeviceId, + this.endAt, + DateTime? createdAt, + DateTime? updatedAt, + }) : createdAt = createdAt ?? DateTime.timestamp(), + updatedAt = updatedAt ?? DateTime.timestamp(); + + /// Create a new instance from a json + factory Location.fromJson(Map json) => + _$LocationFromJson(json); + + /// The channel CID where the message exists. + /// + /// This is only available if the location is coming from server response. + @JsonKey(includeToJson: false) + final String? channelCid; + + /// The channel where the message exists. + @JsonKey(includeToJson: false) + final ChannelModel? channel; + + /// The ID of the message that contains the shared location. + @JsonKey(includeToJson: false) + final String? messageId; + + /// The message that contains the shared location. + @JsonKey(includeToJson: false) + final Message? message; + + /// The ID of the user who shared the location. + @JsonKey(includeToJson: false) + final String? userId; + + /// The latitude of the shared location. + final double latitude; + + /// The longitude of the shared location. + final double longitude; + + /// The ID of the device that created the reminder. + @JsonKey(includeIfNull: false) + final String? createdByDeviceId; + + /// The date at which the shared location will end. + @JsonKey(includeIfNull: false) + final DateTime? endAt; + + /// The date at which the reminder was created. + @JsonKey(includeToJson: false) + final DateTime createdAt; + + /// The date at which the reminder was last updated. + @JsonKey(includeToJson: false) + final DateTime updatedAt; + + /// Returns true if the live location is still active (end_at > now) + bool get isActive { + final endAt = this.endAt; + if (endAt == null) return false; + + return endAt.isAfter(DateTime.now()); + } + + /// Returns true if the live location is expired (end_at <= now) + bool get isExpired => !isActive; + + /// Returns true if this is a live location (has end_at) + bool get isLive => endAt != null; + + /// Returns true if this is a static location (no end_at) + bool get isStatic => endAt == null; + + /// Returns the coordinates of the shared location. + LocationCoordinates get coordinates { + return LocationCoordinates( + latitude: latitude, + longitude: longitude, + ); + } + + /// Serialize to json + Map toJson() => _$LocationToJson(this); + + /// Creates a copy of [Location] with specified attributes overridden. + Location copyWith({ + String? channelCid, + ChannelModel? channel, + String? messageId, + Message? message, + String? userId, + double? latitude, + double? longitude, + String? createdByDeviceId, + DateTime? endAt, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return Location( + channelCid: channelCid ?? this.channelCid, + channel: channel ?? this.channel, + messageId: messageId ?? this.messageId, + message: message ?? this.message, + userId: userId ?? this.userId, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + createdByDeviceId: createdByDeviceId ?? this.createdByDeviceId, + endAt: endAt ?? this.endAt, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + @override + List get props => [ + channelCid, + channel, + messageId, + message, + userId, + latitude, + longitude, + createdByDeviceId, + endAt, + createdAt, + updatedAt, + ]; +} diff --git a/packages/stream_chat/lib/src/core/models/location.g.dart b/packages/stream_chat/lib/src/core/models/location.g.dart new file mode 100644 index 0000000000..e1fac9ac92 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/location.g.dart @@ -0,0 +1,39 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'location.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Location _$LocationFromJson(Map json) => Location( + channelCid: json['channel_cid'] as String?, + channel: json['channel'] == null + ? null + : ChannelModel.fromJson(json['channel'] as Map), + messageId: json['message_id'] as String?, + message: json['message'] == null + ? null + : Message.fromJson(json['message'] as Map), + userId: json['user_id'] as String?, + latitude: (json['latitude'] as num).toDouble(), + longitude: (json['longitude'] as num).toDouble(), + createdByDeviceId: json['created_by_device_id'] as String?, + endAt: json['end_at'] == null + ? null + : DateTime.parse(json['end_at'] as String), + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + ); + +Map _$LocationToJson(Location instance) => { + 'latitude': instance.latitude, + 'longitude': instance.longitude, + if (instance.createdByDeviceId case final value?) + 'created_by_device_id': value, + if (instance.endAt?.toIso8601String() case final value?) 'end_at': value, + }; diff --git a/packages/stream_chat/lib/src/core/models/location_coordinates.dart b/packages/stream_chat/lib/src/core/models/location_coordinates.dart new file mode 100644 index 0000000000..f23389d7e6 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/location_coordinates.dart @@ -0,0 +1,33 @@ +import 'package:equatable/equatable.dart'; + +/// {@template locationInfo} +/// A model class representing a location with latitude and longitude. +/// {@endtemplate} +class LocationCoordinates extends Equatable { + /// {@macro locationInfo} + const LocationCoordinates({ + required this.latitude, + required this.longitude, + }); + + /// The latitude of the location. + final double latitude; + + /// The longitude of the location. + final double longitude; + + /// Creates a copy of [LocationCoordinates] with specified attributes + /// overridden. + LocationCoordinates copyWith({ + double? latitude, + double? longitude, + }) { + return LocationCoordinates( + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + ); + } + + @override + List get props => [latitude, longitude]; +} diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 93847c11af..150224031c 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -4,6 +4,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/attachment.dart'; import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/src/core/models/draft.dart'; +import 'package:stream_chat/src/core/models/location.dart'; import 'package:stream_chat/src/core/models/message_reminder.dart'; import 'package:stream_chat/src/core/models/message_state.dart'; import 'package:stream_chat/src/core/models/moderation.dart'; @@ -34,11 +35,7 @@ class Message extends Equatable implements ComparableFieldProvider { this.mentionedUsers = const [], this.silent = false, this.shadowed = false, - @Deprecated("Use 'reactionGroups' instead") - Map? reactionCounts, - @Deprecated("Use 'reactionGroups' instead") - Map? reactionScores, - Map? reactionGroups, + this.reactionGroups, this.latestReactions, this.ownReactions, this.parentId, @@ -69,17 +66,13 @@ class Message extends Equatable implements ComparableFieldProvider { this.moderation, this.draft, this.reminder, + this.sharedLocation, }) : id = id ?? const Uuid().v4(), type = MessageType(type), pinExpires = pinExpires?.toUtc(), remoteCreatedAt = createdAt, remoteUpdatedAt = updatedAt, remoteDeletedAt = deletedAt, - reactionGroups = _maybeGetReactionGroups( - reactionGroups: reactionGroups, - reactionCounts: reactionCounts, - reactionScores: reactionScores, - ), _quotedMessageId = quotedMessageId, _pollId = pollId; @@ -127,45 +120,40 @@ class Message extends Equatable implements ComparableFieldProvider { @JsonKey(toJson: User.toIds) final List mentionedUsers; - /// A map describing the count of number of every reaction. - @JsonKey(includeToJson: false) - @Deprecated("Use 'reactionGroups' instead") - Map? get reactionCounts { - return reactionGroups?.map((type, it) => MapEntry(type, it.count)); - } - - /// A map describing the count of score of every reaction. - @JsonKey(includeToJson: false) - @Deprecated("Use 'reactionGroups' instead") - Map? get reactionScores { - return reactionGroups?.map((type, it) => MapEntry(type, it.sumScores)); - } - - static Map? _maybeGetReactionGroups({ - Map? reactionGroups, - Map? reactionCounts, - Map? reactionScores, - }) { + static Object? _reactionGroupsReadValue( + Map json, + String key, + ) { + final reactionGroups = json[key] as Map?; if (reactionGroups != null) return reactionGroups; + + final reactionCounts = json['reaction_counts'] as Map?; + final reactionScores = json['reaction_scores'] as Map?; if (reactionCounts == null && reactionScores == null) return null; final reactionTypes = {...?reactionCounts?.keys, ...?reactionScores?.keys}; if (reactionTypes.isEmpty) return null; - final groups = {}; + final groups = {}; for (final type in reactionTypes) { final count = reactionCounts?[type] ?? 0; final sumScores = reactionScores?[type] ?? 0; if (count == 0 || sumScores == 0) continue; - groups[type] = ReactionGroup(count: count, sumScores: sumScores); + final now = DateTime.timestamp(); + groups[type] = { + 'count': count, + 'sum_scores': sumScores, + 'first_reaction_at': now.toIso8601String(), + 'last_reaction_at': now.toIso8601String(), + }; } return groups; } /// A map of reaction types and their corresponding reaction groups. - @JsonKey(includeToJson: false) + @JsonKey(includeToJson: false, readValue: _reactionGroupsReadValue) final Map? reactionGroups; /// The latest reactions to the message created by any user. @@ -307,13 +295,22 @@ class Message extends Equatable implements ComparableFieldProvider { /// Optional draft message linked to this message. /// /// This is present when the message is a thread i.e. contains replies. + @JsonKey(includeToJson: false) final Draft? draft; /// Optional reminder for this message. /// /// This is present when a user has set a reminder for this message. + @JsonKey(includeToJson: false) final MessageReminder? reminder; + /// Optional shared location associated with this message. + /// + /// This is used to share a location in a message, allowing users to view the + /// location on a map. + @JsonKey(includeIfNull: false) + final Location? sharedLocation; + /// Message custom extraData. final Map extraData; @@ -362,6 +359,7 @@ class Message extends Equatable implements ComparableFieldProvider { 'moderation_details', 'draft', 'reminder', + 'shared_location', ]; /// Serialize to json. @@ -389,10 +387,6 @@ class Message extends Equatable implements ComparableFieldProvider { List? mentionedUsers, bool? silent, bool? shadowed, - @Deprecated("Use 'reactionGroups' instead") - Map? reactionCounts, - @Deprecated("Use 'reactionGroups' instead") - Map? reactionScores, Map? reactionGroups, List? latestReactions, List? ownReactions, @@ -424,6 +418,7 @@ class Message extends Equatable implements ComparableFieldProvider { Moderation? moderation, Object? draft = _nullConst, Object? reminder = _nullConst, + Location? sharedLocation, }) { assert(() { if (pinExpires is! DateTime && @@ -464,12 +459,7 @@ class Message extends Equatable implements ComparableFieldProvider { mentionedUsers: mentionedUsers ?? this.mentionedUsers, silent: silent ?? this.silent, shadowed: shadowed ?? this.shadowed, - reactionGroups: _maybeGetReactionGroups( - reactionGroups: reactionGroups, - reactionCounts: reactionCounts, - reactionScores: reactionScores, - ) ?? - this.reactionGroups, + reactionGroups: reactionGroups ?? this.reactionGroups, latestReactions: latestReactions ?? this.latestReactions, ownReactions: ownReactions ?? this.ownReactions, parentId: parentId ?? this.parentId, @@ -506,6 +496,7 @@ class Message extends Equatable implements ComparableFieldProvider { draft: draft == _nullConst ? this.draft : draft as Draft?, reminder: reminder == _nullConst ? this.reminder : reminder as MessageReminder?, + sharedLocation: sharedLocation ?? this.sharedLocation, ); } @@ -551,6 +542,7 @@ class Message extends Equatable implements ComparableFieldProvider { moderation: other.moderation, draft: other.draft, reminder: other.reminder, + sharedLocation: other.sharedLocation, ); } @@ -616,6 +608,7 @@ class Message extends Equatable implements ComparableFieldProvider { moderation, draft, reminder, + sharedLocation, ]; @override diff --git a/packages/stream_chat/lib/src/core/models/message.g.dart b/packages/stream_chat/lib/src/core/models/message.g.dart index cc90809bd9..3add1df41b 100644 --- a/packages/stream_chat/lib/src/core/models/message.g.dart +++ b/packages/stream_chat/lib/src/core/models/message.g.dart @@ -22,13 +22,9 @@ Message _$MessageFromJson(Map json) => Message( const [], silent: json['silent'] as bool? ?? false, shadowed: json['shadowed'] as bool? ?? false, - reactionCounts: (json['reaction_counts'] as Map?)?.map( - (k, e) => MapEntry(k, (e as num).toInt()), - ), - reactionScores: (json['reaction_scores'] as Map?)?.map( - (k, e) => MapEntry(k, (e as num).toInt()), - ), - reactionGroups: (json['reaction_groups'] as Map?)?.map( + reactionGroups: (Message._reactionGroupsReadValue(json, 'reaction_groups') + as Map?) + ?.map( (k, e) => MapEntry(k, ReactionGroup.fromJson(e as Map)), ), @@ -95,6 +91,9 @@ Message _$MessageFromJson(Map json) => Message( reminder: json['reminder'] == null ? null : MessageReminder.fromJson(json['reminder'] as Map), + sharedLocation: json['shared_location'] == null + ? null + : Location.fromJson(json['shared_location'] as Map), ); Map _$MessageToJson(Message instance) => { @@ -112,7 +111,7 @@ Map _$MessageToJson(Message instance) => { 'poll_id': instance.pollId, if (instance.restrictedVisibility case final value?) 'restricted_visibility': value, - 'draft': instance.draft?.toJson(), - 'reminder': instance.reminder?.toJson(), + if (instance.sharedLocation?.toJson() case final value?) + 'shared_location': value, 'extra_data': instance.extraData, }; diff --git a/packages/stream_chat/lib/src/core/models/message_state.dart b/packages/stream_chat/lib/src/core/models/message_state.dart index 0cbefec15b..cf98096843 100644 --- a/packages/stream_chat/lib/src/core/models/message_state.dart +++ b/packages/stream_chat/lib/src/core/models/message_state.dart @@ -103,9 +103,11 @@ extension MessageStateX on MessageState { /// Returns true if the message is in failed updating state. bool get isUpdatingFailed { - final messageState = this; - if (messageState is! MessageFailed) return false; - return messageState.state is UpdatingFailed; + return switch (this) { + MessageFailed(state: UpdatingFailed()) => true, + MessageFailed(state: PartialUpdatingFailed()) => true, + _ => false, + }; } /// Returns true if the message is in failed deleting state. @@ -182,6 +184,46 @@ sealed class MessageState with _$MessageState { ); } + /// Sending failed state when the message fails to be sent. + factory MessageState.sendingFailed({ + required bool skipPush, + required bool skipEnrichUrl, + }) { + return MessageState.failed( + state: FailedState.sendingFailed( + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), + ); + } + + /// Updating failed state when the message fails to be updated. + factory MessageState.updatingFailed({ + required bool skipPush, + required bool skipEnrichUrl, + }) { + return MessageState.failed( + state: FailedState.updatingFailed( + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), + ); + } + + factory MessageState.partialUpdatingFailed({ + Map? set, + List? unset, + required bool skipEnrichUrl, + }) { + return MessageState.failed( + state: FailedState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: skipEnrichUrl, + ), + ); + } + /// Sending state when the message is being sent. static const sending = MessageState.outgoing( state: OutgoingState.sending(), @@ -222,16 +264,6 @@ sealed class MessageState with _$MessageState { state: CompletedState.deleted(hard: true), ); - /// Sending failed state when the message fails to be sent. - static const sendingFailed = MessageState.failed( - state: FailedState.sendingFailed(), - ); - - /// Updating failed state when the message fails to be updated. - static const updatingFailed = MessageState.failed( - state: FailedState.updatingFailed(), - ); - /// Deleting failed state when the message fails to be soft deleted. static const softDeletingFailed = MessageState.failed( state: FailedState.deletingFailed(), @@ -285,10 +317,22 @@ sealed class CompletedState with _$CompletedState { @freezed sealed class FailedState with _$FailedState { /// Sending failed state when the message fails to be sent. - const factory FailedState.sendingFailed() = SendingFailed; + const factory FailedState.sendingFailed({ + @Default(false) bool skipPush, + @Default(false) bool skipEnrichUrl, + }) = SendingFailed; /// Updating failed state when the message fails to be updated. - const factory FailedState.updatingFailed() = UpdatingFailed; + const factory FailedState.updatingFailed({ + @Default(false) bool skipPush, + @Default(false) bool skipEnrichUrl, + }) = UpdatingFailed; + + const factory FailedState.partialUpdatingFailed({ + Map? set, + List? unset, + @Default(false) bool skipEnrichUrl, + }) = PartialUpdatingFailed; /// Deleting failed state when the message fails to be deleted. const factory FailedState.deletingFailed({ @@ -616,14 +660,21 @@ extension FailedStatePatternMatching on FailedState { /// @nodoc @optionalTypeArgs TResult when({ - required TResult Function() sendingFailed, - required TResult Function() updatingFailed, + required TResult Function(bool skipPush, bool skipEnrichUrl) sendingFailed, + required TResult Function(bool skipPush, bool skipEnrichUrl) updatingFailed, + required TResult Function( + Map? set, List? unset, bool skipEnrichUrl) + partialUpdatingFailed, required TResult Function(bool hard) deletingFailed, }) { final failedState = this; return switch (failedState) { - SendingFailed() => sendingFailed(), - UpdatingFailed() => updatingFailed(), + SendingFailed() => + sendingFailed(failedState.skipPush, failedState.skipEnrichUrl), + UpdatingFailed() => + updatingFailed(failedState.skipPush, failedState.skipEnrichUrl), + PartialUpdatingFailed() => partialUpdatingFailed( + failedState.set, failedState.unset, failedState.skipEnrichUrl), DeletingFailed() => deletingFailed(failedState.hard), }; } @@ -631,14 +682,21 @@ extension FailedStatePatternMatching on FailedState { /// @nodoc @optionalTypeArgs TResult? whenOrNull({ - TResult? Function()? sendingFailed, - TResult? Function()? updatingFailed, + TResult? Function(bool skipPush, bool skipEnrichUrl)? sendingFailed, + TResult? Function(bool skipPush, bool skipEnrichUrl)? updatingFailed, + required TResult Function( + Map? set, List? unset, bool skipEnrichUrl) + partialUpdatingFailed, TResult? Function(bool hard)? deletingFailed, }) { final failedState = this; return switch (failedState) { - SendingFailed() => sendingFailed?.call(), - UpdatingFailed() => updatingFailed?.call(), + SendingFailed() => + sendingFailed?.call(failedState.skipPush, failedState.skipEnrichUrl), + UpdatingFailed() => + updatingFailed?.call(failedState.skipPush, failedState.skipEnrichUrl), + PartialUpdatingFailed() => partialUpdatingFailed( + failedState.set, failedState.unset, failedState.skipEnrichUrl), DeletingFailed() => deletingFailed?.call(failedState.hard), }; } @@ -646,15 +704,22 @@ extension FailedStatePatternMatching on FailedState { /// @nodoc @optionalTypeArgs TResult maybeWhen({ - TResult Function()? sendingFailed, - TResult Function()? updatingFailed, + TResult Function(bool skipPush, bool skipEnrichUrl)? sendingFailed, + TResult Function(bool skipPush, bool skipEnrichUrl)? updatingFailed, + required TResult Function( + Map? set, List? unset, bool skipEnrichUrl) + partialUpdatingFailed, TResult Function(bool hard)? deletingFailed, required TResult orElse(), }) { final failedState = this; final result = switch (failedState) { - SendingFailed() => sendingFailed?.call(), - UpdatingFailed() => updatingFailed?.call(), + SendingFailed() => + sendingFailed?.call(failedState.skipPush, failedState.skipEnrichUrl), + UpdatingFailed() => + updatingFailed?.call(failedState.skipPush, failedState.skipEnrichUrl), + PartialUpdatingFailed() => partialUpdatingFailed( + failedState.set, failedState.unset, failedState.skipEnrichUrl), DeletingFailed() => deletingFailed?.call(failedState.hard), }; @@ -666,12 +731,15 @@ extension FailedStatePatternMatching on FailedState { TResult map({ required TResult Function(SendingFailed value) sendingFailed, required TResult Function(UpdatingFailed value) updatingFailed, + required TResult Function(PartialUpdatingFailed value) + partialUpdatingFailed, required TResult Function(DeletingFailed value) deletingFailed, }) { final failedState = this; return switch (failedState) { SendingFailed() => sendingFailed(failedState), UpdatingFailed() => updatingFailed(failedState), + PartialUpdatingFailed() => partialUpdatingFailed(failedState), DeletingFailed() => deletingFailed(failedState), }; } @@ -681,12 +749,14 @@ extension FailedStatePatternMatching on FailedState { TResult? mapOrNull({ TResult? Function(SendingFailed value)? sendingFailed, TResult? Function(UpdatingFailed value)? updatingFailed, + TResult? Function(PartialUpdatingFailed value)? partialUpdatingFailed, TResult? Function(DeletingFailed value)? deletingFailed, }) { final failedState = this; return switch (failedState) { SendingFailed() => sendingFailed?.call(failedState), UpdatingFailed() => updatingFailed?.call(failedState), + PartialUpdatingFailed() => partialUpdatingFailed?.call(failedState), DeletingFailed() => deletingFailed?.call(failedState), }; } @@ -696,6 +766,7 @@ extension FailedStatePatternMatching on FailedState { TResult maybeMap({ TResult Function(SendingFailed value)? sendingFailed, TResult Function(UpdatingFailed value)? updatingFailed, + TResult Function(PartialUpdatingFailed value)? partialUpdatingFailed, TResult Function(DeletingFailed value)? deletingFailed, required TResult orElse(), }) { @@ -703,6 +774,7 @@ extension FailedStatePatternMatching on FailedState { final result = switch (failedState) { SendingFailed() => sendingFailed?.call(failedState), UpdatingFailed() => updatingFailed?.call(failedState), + PartialUpdatingFailed() => partialUpdatingFailed?.call(failedState), DeletingFailed() => deletingFailed?.call(failedState), }; diff --git a/packages/stream_chat/lib/src/core/models/message_state.freezed.dart b/packages/stream_chat/lib/src/core/models/message_state.freezed.dart index 079d8b3696..06cb77449e 100644 --- a/packages/stream_chat/lib/src/core/models/message_state.freezed.dart +++ b/packages/stream_chat/lib/src/core/models/message_state.freezed.dart @@ -736,6 +736,8 @@ FailedState _$FailedStateFromJson(Map json) { return SendingFailed.fromJson(json); case 'updatingFailed': return UpdatingFailed.fromJson(json); + case 'partialUpdatingFailed': + return PartialUpdatingFailed.fromJson(json); case 'deletingFailed': return DeletingFailed.fromJson(json); @@ -774,13 +776,27 @@ class $FailedStateCopyWith<$Res> { /// @nodoc @JsonSerializable() class SendingFailed implements FailedState { - const SendingFailed({final String? $type}) : $type = $type ?? 'sendingFailed'; + const SendingFailed( + {this.skipPush = false, this.skipEnrichUrl = false, final String? $type}) + : $type = $type ?? 'sendingFailed'; factory SendingFailed.fromJson(Map json) => _$SendingFailedFromJson(json); + @JsonKey() + final bool skipPush; + @JsonKey() + final bool skipEnrichUrl; + @JsonKey(name: 'runtimeType') final String $type; + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $SendingFailedCopyWith get copyWith => + _$SendingFailedCopyWithImpl(this, _$identity); + @override Map toJson() { return _$SendingFailedToJson( @@ -791,30 +807,86 @@ class SendingFailed implements FailedState { @override bool operator ==(Object other) { return identical(this, other) || - (other.runtimeType == runtimeType && other is SendingFailed); + (other.runtimeType == runtimeType && + other is SendingFailed && + (identical(other.skipPush, skipPush) || + other.skipPush == skipPush) && + (identical(other.skipEnrichUrl, skipEnrichUrl) || + other.skipEnrichUrl == skipEnrichUrl)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => runtimeType.hashCode; + int get hashCode => Object.hash(runtimeType, skipPush, skipEnrichUrl); @override String toString() { - return 'FailedState.sendingFailed()'; + return 'FailedState.sendingFailed(skipPush: $skipPush, skipEnrichUrl: $skipEnrichUrl)'; + } +} + +/// @nodoc +abstract mixin class $SendingFailedCopyWith<$Res> + implements $FailedStateCopyWith<$Res> { + factory $SendingFailedCopyWith( + SendingFailed value, $Res Function(SendingFailed) _then) = + _$SendingFailedCopyWithImpl; + @useResult + $Res call({bool skipPush, bool skipEnrichUrl}); +} + +/// @nodoc +class _$SendingFailedCopyWithImpl<$Res> + implements $SendingFailedCopyWith<$Res> { + _$SendingFailedCopyWithImpl(this._self, this._then); + + final SendingFailed _self; + final $Res Function(SendingFailed) _then; + + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? skipPush = null, + Object? skipEnrichUrl = null, + }) { + return _then(SendingFailed( + skipPush: null == skipPush + ? _self.skipPush + : skipPush // ignore: cast_nullable_to_non_nullable + as bool, + skipEnrichUrl: null == skipEnrichUrl + ? _self.skipEnrichUrl + : skipEnrichUrl // ignore: cast_nullable_to_non_nullable + as bool, + )); } } /// @nodoc @JsonSerializable() class UpdatingFailed implements FailedState { - const UpdatingFailed({final String? $type}) + const UpdatingFailed( + {this.skipPush = false, this.skipEnrichUrl = false, final String? $type}) : $type = $type ?? 'updatingFailed'; factory UpdatingFailed.fromJson(Map json) => _$UpdatingFailedFromJson(json); + @JsonKey() + final bool skipPush; + @JsonKey() + final bool skipEnrichUrl; + @JsonKey(name: 'runtimeType') final String $type; + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $UpdatingFailedCopyWith get copyWith => + _$UpdatingFailedCopyWithImpl(this, _$identity); + @override Map toJson() { return _$UpdatingFailedToJson( @@ -825,16 +897,181 @@ class UpdatingFailed implements FailedState { @override bool operator ==(Object other) { return identical(this, other) || - (other.runtimeType == runtimeType && other is UpdatingFailed); + (other.runtimeType == runtimeType && + other is UpdatingFailed && + (identical(other.skipPush, skipPush) || + other.skipPush == skipPush) && + (identical(other.skipEnrichUrl, skipEnrichUrl) || + other.skipEnrichUrl == skipEnrichUrl)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => runtimeType.hashCode; + int get hashCode => Object.hash(runtimeType, skipPush, skipEnrichUrl); @override String toString() { - return 'FailedState.updatingFailed()'; + return 'FailedState.updatingFailed(skipPush: $skipPush, skipEnrichUrl: $skipEnrichUrl)'; + } +} + +/// @nodoc +abstract mixin class $UpdatingFailedCopyWith<$Res> + implements $FailedStateCopyWith<$Res> { + factory $UpdatingFailedCopyWith( + UpdatingFailed value, $Res Function(UpdatingFailed) _then) = + _$UpdatingFailedCopyWithImpl; + @useResult + $Res call({bool skipPush, bool skipEnrichUrl}); +} + +/// @nodoc +class _$UpdatingFailedCopyWithImpl<$Res> + implements $UpdatingFailedCopyWith<$Res> { + _$UpdatingFailedCopyWithImpl(this._self, this._then); + + final UpdatingFailed _self; + final $Res Function(UpdatingFailed) _then; + + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? skipPush = null, + Object? skipEnrichUrl = null, + }) { + return _then(UpdatingFailed( + skipPush: null == skipPush + ? _self.skipPush + : skipPush // ignore: cast_nullable_to_non_nullable + as bool, + skipEnrichUrl: null == skipEnrichUrl + ? _self.skipEnrichUrl + : skipEnrichUrl // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class PartialUpdatingFailed implements FailedState { + const PartialUpdatingFailed( + {final Map? set, + final List? unset, + this.skipEnrichUrl = false, + final String? $type}) + : _set = set, + _unset = unset, + $type = $type ?? 'partialUpdatingFailed'; + factory PartialUpdatingFailed.fromJson(Map json) => + _$PartialUpdatingFailedFromJson(json); + + final Map? _set; + Map? get set { + final value = _set; + if (value == null) return null; + if (_set is EqualUnmodifiableMapView) return _set; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + final List? _unset; + List? get unset { + final value = _unset; + if (value == null) return null; + if (_unset is EqualUnmodifiableListView) return _unset; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @JsonKey() + final bool skipEnrichUrl; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $PartialUpdatingFailedCopyWith get copyWith => + _$PartialUpdatingFailedCopyWithImpl( + this, _$identity); + + @override + Map toJson() { + return _$PartialUpdatingFailedToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is PartialUpdatingFailed && + const DeepCollectionEquality().equals(other._set, _set) && + const DeepCollectionEquality().equals(other._unset, _unset) && + (identical(other.skipEnrichUrl, skipEnrichUrl) || + other.skipEnrichUrl == skipEnrichUrl)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_set), + const DeepCollectionEquality().hash(_unset), + skipEnrichUrl); + + @override + String toString() { + return 'FailedState.partialUpdatingFailed(set: $set, unset: $unset, skipEnrichUrl: $skipEnrichUrl)'; + } +} + +/// @nodoc +abstract mixin class $PartialUpdatingFailedCopyWith<$Res> + implements $FailedStateCopyWith<$Res> { + factory $PartialUpdatingFailedCopyWith(PartialUpdatingFailed value, + $Res Function(PartialUpdatingFailed) _then) = + _$PartialUpdatingFailedCopyWithImpl; + @useResult + $Res call( + {Map? set, List? unset, bool skipEnrichUrl}); +} + +/// @nodoc +class _$PartialUpdatingFailedCopyWithImpl<$Res> + implements $PartialUpdatingFailedCopyWith<$Res> { + _$PartialUpdatingFailedCopyWithImpl(this._self, this._then); + + final PartialUpdatingFailed _self; + final $Res Function(PartialUpdatingFailed) _then; + + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? set = freezed, + Object? unset = freezed, + Object? skipEnrichUrl = null, + }) { + return _then(PartialUpdatingFailed( + set: freezed == set + ? _self._set + : set // ignore: cast_nullable_to_non_nullable + as Map?, + unset: freezed == unset + ? _self._unset + : unset // ignore: cast_nullable_to_non_nullable + as List?, + skipEnrichUrl: null == skipEnrichUrl + ? _self.skipEnrichUrl + : skipEnrichUrl // ignore: cast_nullable_to_non_nullable + as bool, + )); } } diff --git a/packages/stream_chat/lib/src/core/models/message_state.g.dart b/packages/stream_chat/lib/src/core/models/message_state.g.dart index e39b40667b..3161a4f8c2 100644 --- a/packages/stream_chat/lib/src/core/models/message_state.g.dart +++ b/packages/stream_chat/lib/src/core/models/message_state.g.dart @@ -108,21 +108,48 @@ Map _$DeletedToJson(Deleted instance) => { SendingFailed _$SendingFailedFromJson(Map json) => SendingFailed( + skipPush: json['skip_push'] as bool? ?? false, + skipEnrichUrl: json['skip_enrich_url'] as bool? ?? false, $type: json['runtimeType'] as String?, ); Map _$SendingFailedToJson(SendingFailed instance) => { + 'skip_push': instance.skipPush, + 'skip_enrich_url': instance.skipEnrichUrl, 'runtimeType': instance.$type, }; UpdatingFailed _$UpdatingFailedFromJson(Map json) => UpdatingFailed( + skipPush: json['skip_push'] as bool? ?? false, + skipEnrichUrl: json['skip_enrich_url'] as bool? ?? false, $type: json['runtimeType'] as String?, ); Map _$UpdatingFailedToJson(UpdatingFailed instance) => { + 'skip_push': instance.skipPush, + 'skip_enrich_url': instance.skipEnrichUrl, + 'runtimeType': instance.$type, + }; + +PartialUpdatingFailed _$PartialUpdatingFailedFromJson( + Map json) => + PartialUpdatingFailed( + set: json['set'] as Map?, + unset: + (json['unset'] as List?)?.map((e) => e as String).toList(), + skipEnrichUrl: json['skip_enrich_url'] as bool? ?? false, + $type: json['runtimeType'] as String?, + ); + +Map _$PartialUpdatingFailedToJson( + PartialUpdatingFailed instance) => + { + 'set': instance.set, + 'unset': instance.unset, + 'skip_enrich_url': instance.skipEnrichUrl, 'runtimeType': instance.$type, }; diff --git a/packages/stream_chat/lib/src/core/models/reaction.dart b/packages/stream_chat/lib/src/core/models/reaction.dart index 474b6dff6a..dc62fd7ce7 100644 --- a/packages/stream_chat/lib/src/core/models/reaction.dart +++ b/packages/stream_chat/lib/src/core/models/reaction.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/core/util/serializer.dart'; @@ -6,18 +7,21 @@ part 'reaction.g.dart'; /// The class that defines a reaction @JsonSerializable() -class Reaction { +class Reaction extends Equatable { /// Constructor used for json serialization Reaction({ this.messageId, - DateTime? createdAt, required this.type, this.user, String? userId, - this.score = 0, + this.score = 1, + this.emojiCode, + DateTime? createdAt, + DateTime? updatedAt, this.extraData = const {}, }) : userId = userId ?? user?.id, - createdAt = createdAt ?? DateTime.now(); + createdAt = createdAt ?? DateTime.timestamp(), + updatedAt = updatedAt ?? DateTime.timestamp(); /// Create a new instance from a json factory Reaction.fromJson(Map json) => @@ -27,37 +31,48 @@ class Reaction { )); /// The messageId to which the reaction belongs + @JsonKey(includeToJson: false) final String? messageId; /// The type of the reaction final String type; - /// The date of the reaction - @JsonKey(includeToJson: false) - final DateTime createdAt; + /// The score of the reaction (ie. number of reactions sent) + final int score; + + /// The emoji code of the reaction (used for notifications) + @JsonKey(includeIfNull: false) + final String? emojiCode; /// The user that sent the reaction @JsonKey(includeToJson: false) final User? user; - /// The score of the reaction (ie. number of reactions sent) - final int score; - /// The userId that sent the reaction @JsonKey(includeToJson: false) final String? userId; + /// The date of the reaction + @JsonKey(includeToJson: false) + final DateTime createdAt; + + /// The date of the reaction update + @JsonKey(includeToJson: false) + final DateTime updatedAt; + /// Reaction custom extraData final Map extraData; /// Map of custom user extraData static const topLevelFields = [ 'message_id', - 'created_at', 'type', 'user', 'user_id', 'score', + 'emoji_code', + 'created_at', + 'updated_at', ]; /// Serialize to json @@ -68,20 +83,24 @@ class Reaction { /// Creates a copy of [Reaction] with specified attributes overridden. Reaction copyWith({ String? messageId, - DateTime? createdAt, String? type, User? user, String? userId, int? score, + String? emojiCode, + DateTime? createdAt, + DateTime? updatedAt, Map? extraData, }) => Reaction( messageId: messageId ?? this.messageId, - createdAt: createdAt ?? this.createdAt, type: type ?? this.type, user: user ?? this.user, userId: userId ?? this.userId, score: score ?? this.score, + emojiCode: emojiCode ?? this.emojiCode, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, extraData: extraData ?? this.extraData, ); @@ -89,11 +108,26 @@ class Reaction { /// given [other] reaction. Reaction merge(Reaction other) => copyWith( messageId: other.messageId, - createdAt: other.createdAt, type: other.type, user: other.user, userId: other.userId, score: other.score, + emojiCode: other.emojiCode, + createdAt: other.createdAt, + updatedAt: other.updatedAt, extraData: other.extraData, ); + + @override + List get props => [ + messageId, + type, + user, + userId, + score, + emojiCode, + createdAt, + updatedAt, + extraData, + ]; } diff --git a/packages/stream_chat/lib/src/core/models/reaction.g.dart b/packages/stream_chat/lib/src/core/models/reaction.g.dart index 3be01abc33..f29c4e0931 100644 --- a/packages/stream_chat/lib/src/core/models/reaction.g.dart +++ b/packages/stream_chat/lib/src/core/models/reaction.g.dart @@ -8,21 +8,25 @@ part of 'reaction.dart'; Reaction _$ReactionFromJson(Map json) => Reaction( messageId: json['message_id'] as String?, - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), type: json['type'] as String, user: json['user'] == null ? null : User.fromJson(json['user'] as Map), userId: json['user_id'] as String?, - score: (json['score'] as num?)?.toInt() ?? 0, + score: (json['score'] as num?)?.toInt() ?? 1, + emojiCode: json['emoji_code'] as String?, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), extraData: json['extra_data'] as Map? ?? const {}, ); Map _$ReactionToJson(Reaction instance) => { - 'message_id': instance.messageId, 'type': instance.type, 'score': instance.score, + if (instance.emojiCode case final value?) 'emoji_code': value, 'extra_data': instance.extraData, }; diff --git a/packages/stream_chat/lib/src/core/models/reaction_group.dart b/packages/stream_chat/lib/src/core/models/reaction_group.dart index 4410500acf..480156de42 100644 --- a/packages/stream_chat/lib/src/core/models/reaction_group.dart +++ b/packages/stream_chat/lib/src/core/models/reaction_group.dart @@ -57,3 +57,26 @@ class ReactionGroup extends Equatable { lastReactionAt, ]; } + +/// A group of comparators for sorting [ReactionGroup]s. +final class ReactionSorting { + /// Sorts [ReactionGroup]s by the sum of their scores. + static int byScore(ReactionGroup a, ReactionGroup b) { + return a.sumScores.compareTo(b.sumScores); + } + + /// Sorts [ReactionGroup]s by the count of reactions. + static int byCount(ReactionGroup a, ReactionGroup b) { + return a.count.compareTo(b.count); + } + + /// Sorts [ReactionGroup]s by the date of their first reaction. + static int byFirstReactionAt(ReactionGroup a, ReactionGroup b) { + return a.firstReactionAt.compareTo(b.firstReactionAt); + } + + /// Sorts [ReactionGroup]s by the date of their last reaction. + static int byLastReactionAt(ReactionGroup a, ReactionGroup b) { + return a.lastReactionAt.compareTo(b.lastReactionAt); + } +} diff --git a/packages/stream_chat/lib/src/core/util/event_controller.dart b/packages/stream_chat/lib/src/core/util/event_controller.dart new file mode 100644 index 0000000000..4e98d33b9a --- /dev/null +++ b/packages/stream_chat/lib/src/core/util/event_controller.dart @@ -0,0 +1,77 @@ +import 'dart:async'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat/src/core/models/event.dart'; + +/// A function that inspects an event and optionally resolves it into a +/// more specific or refined version of the same type. +/// +/// If the resolver does not recognize or handle the event, +/// it returns `null`, allowing other resolvers to attempt resolution. +typedef EventResolver = T? Function(T event); + +/// {@template eventController} +/// A reactive event stream controller for [Event]s that supports conditional +/// resolution before emitting events to subscribers. +/// +/// When an event is added: +/// - Each resolver is evaluated in order. +/// - The first resolver that returns a non-null result is used to produce +/// the resolved event that gets emitted. +/// - If no resolver returns a result, the original event is emitted unchanged. +/// +/// This is useful for normalizing or refining generic events into more +/// specific ones (e.g. rewriting `pollVoteCasted` into `pollAnswerCasted`) +/// before they reach business logic or state layers. +/// {@endtemplate} +class EventController extends Subject { + /// {@macro eventController} + factory EventController({ + bool sync = false, + void Function()? onListen, + void Function()? onCancel, + List> resolvers = const [], + }) { + // ignore: close_sinks + final controller = StreamController.broadcast( + sync: sync, + onListen: onListen, + onCancel: onCancel, + ); + + return EventController._( + controller, + controller.stream, + resolvers, + ); + } + + EventController._( + super.controller, + super.stream, + this._resolvers, + ); + + /// The list of resolvers used to inspect and optionally resolve events + /// before they are emitted. + /// + /// Resolvers are evaluated in order. The first to return a non-null result + /// determines the event that will be emitted. If none apply, the original + /// event is emitted as-is. + final List> _resolvers; + + /// Adds an [event] to the stream. + /// + /// Each [EventResolver] is applied in order until one returns a non-null + /// result. That resolved event is emitted, and no further resolvers are + /// evaluated. If all resolvers return `null`, the original event is emitted. + @override + void add(T event) { + for (final resolver in _resolvers) { + final result = resolver(event); + if (result != null) return super.add(result); + } + + // No resolver matched โ€” emit the event as-is. + return super.add(event); + } +} diff --git a/packages/stream_chat/lib/src/db/chat_persistence_client.dart b/packages/stream_chat/lib/src/db/chat_persistence_client.dart index c91a5ffa1e..2b1a138fb0 100644 --- a/packages/stream_chat/lib/src/db/chat_persistence_client.dart +++ b/packages/stream_chat/lib/src/db/chat_persistence_client.dart @@ -7,6 +7,7 @@ import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/draft.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/location.dart'; import 'package:stream_chat/src/core/models/member.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/poll.dart'; @@ -83,6 +84,12 @@ abstract class ChatPersistenceClient { /// [parentId] for thread messages. Future getDraftMessageByCid(String cid, {String? parentId}); + /// Get stored [Location]s by providing channel [cid] + Future> getLocationsByCid(String cid); + + /// Get stored [Location] by providing [messageId] + Future getLocationByMessageId(String messageId); + /// Get [ChannelState] data by providing channel [cid] Future getChannelStateByCid( String cid, { @@ -168,6 +175,12 @@ abstract class ChatPersistenceClient { /// [DraftMessages.parentId]. Future deleteDraftMessageByCid(String cid, {String? parentId}); + /// Removes locations by channel [cid] + Future deleteLocationsByCid(String cid); + + /// Removes locations by message [messageIds] + Future deleteLocationsByMessageIds(List messageIds); + /// Updates the message data of a particular channel [cid] with /// the new [messages] data Future updateMessages(String cid, List messages) => @@ -228,6 +241,9 @@ abstract class ChatPersistenceClient { /// Updates the draft messages data with the new [draftMessages] data Future updateDraftMessages(List draftMessages); + /// Updates the locations data with the new [locations] data + Future updateLocations(List locations); + /// Deletes all the reactions by [messageIds] Future deleteReactionsByMessageId(List messageIds); @@ -297,6 +313,8 @@ abstract class ChatPersistenceClient { final drafts = []; final draftsToDeleteCids = []; + final locations = []; + for (final state in channelStates) { final channel = state.channel; // Continue if channel is not available. @@ -347,6 +365,11 @@ abstract class ChatPersistenceClient { ...?pinnedMessages?.map((it) => it.draft), ].nonNulls); + locations.addAll([ + ...?messages?.map((it) => it.sharedLocation), + ...?pinnedMessages?.map((it) => it.sharedLocation), + ].nonNulls); + users.addAll([ channel.createdBy, ...?messages?.map((it) => it.user), @@ -391,6 +414,7 @@ abstract class ChatPersistenceClient { updatePinnedMessageReactions(pinnedReactions), updatePollVotes(pollVotes), updateDraftMessages(drafts), + updateLocations(locations), ]); } diff --git a/packages/stream_chat/lib/src/event_type.dart b/packages/stream_chat/lib/src/event_type.dart index 29ab5b1de8..189350d959 100644 --- a/packages/stream_chat/lib/src/event_type.dart +++ b/packages/stream_chat/lib/src/event_type.dart @@ -122,6 +122,9 @@ class EventType { /// Event sent when the AI indicator is cleared static const String aiIndicatorClear = 'ai_indicator.clear'; + /// Event sent when a new poll is created. + static const String pollCreated = 'poll.created'; + /// Event sent when a poll is updated. static const String pollUpdated = 'poll.updated'; @@ -170,4 +173,13 @@ class EventType { /// Event sent when a message reminder is due. static const String notificationReminderDue = 'notification.reminder_due'; + + /// Event sent when a new shared location is created. + static const String locationShared = 'location.shared'; + + /// Event sent when a live shared location is updated. + static const String locationUpdated = 'location.updated'; + + /// Event sent when a live shared location is expired. + static const String locationExpired = 'location.expired'; } diff --git a/packages/stream_chat/lib/src/permission_type.dart b/packages/stream_chat/lib/src/permission_type.dart deleted file mode 100644 index dcf0c5ef49..0000000000 --- a/packages/stream_chat/lib/src/permission_type.dart +++ /dev/null @@ -1,103 +0,0 @@ -/// Describes capabilities of a user vis-a-vis a channel -@Deprecated("Use 'ChannelCapability' instead") -class PermissionType { - /// Capability required to send a message in the channel - /// Channel is not frozen (or user has UseFrozenChannel permission) - /// and user has CreateMessage permission. - static const String sendMessage = 'send-message'; - - /// Capability required to receive connect events in the channel - static const String connectEvents = 'connect-events'; - - /// Capability required to send a message - /// Reactions are enabled for the channel, channel is not frozen - /// (or user has UseFrozenChannel permission) and user has - /// CreateReaction permission - static const String sendReaction = 'send-reaction'; - - /// Capability required to send links in a channel - /// send-message + user has AddLinks permission - static const String sendLinks = 'send-links'; - - /// Capability required to send thread reply - /// send-message + channel has replies enabled - static const String sendReply = 'send-reply'; - - /// Capability to freeze a channel - /// User has UpdateChannelFrozen permission. - /// The name implies freezing, - /// but unfreezing is also allowed when this capability is present - static const String freezeChannel = 'freeze-channel'; - - /// User has UpdateChannelCooldown permission. - /// Allows to enable/disable slow mode in the channel - static const String setChannelCooldown = 'set-channel-cooldown'; - - /// User has the ability to skip slow mode when it's active. - static const String skipSlowMode = 'skip-slow-mode'; - - /// User has RemoveOwnChannelMembership or UpdateChannelMembers permission - static const String leaveChannel = 'leave-channel'; - - /// User can mute channel - static const String muteChannel = 'mute-channel'; - - /// Ability to receive read events - static const String readEvents = 'read-events'; - - /// Capability required to pin a message in a channel - /// Corresponds to PinMessage permission - static const String pinMessage = 'pin-message'; - - /// Capability required to quote a message in a channel - static const String quoteMessage = 'quote-message'; - - /// Capability required to flag a message in a channel - static const String flagMessage = 'flag-message'; - - /// User has ability to delete any message in the channel - /// User has DeleteMessage permission - /// which applies to any message in the channel - static const String deleteAnyMessage = 'delete-any-message'; - - /// User has ability to delete their own message in the channel - /// User has DeleteMessage permission which applies only to owned messages - static const String deleteOwnMessage = 'delete-own-message'; - - /// User has ability to update/edit any message in the channel - /// User has UpdateMessage permission which - /// applies to any message in the channel - static const String updateAnyMessage = 'update-any-message'; - - /// User has ability to update/edit their own message in the channel - /// User has UpdateMessage permission which applies only to owned messages - static const String updateOwnMessage = 'update-own-message'; - - /// User can search for message in a channel - /// Search feature is enabled (it will also have - /// permission check in the future) - static const String searchMessages = 'search-messages'; - - /// Capability required to send typing events in a channel - /// (Typing events are enabled) - static const String sendTypingEvents = 'send-typing-events'; - - /// Capability required to upload a file in a channel - /// Uploads are enabled and user has UploadAttachment - static const String uploadFile = 'upload-file'; - - /// Capability required to delete channel - /// User has DeleteChannel permission - static const String deleteChannel = 'delete-channel'; - - /// Capability required update/edit channel info - /// User has UpdateChannel permission - static const String updateChannel = 'update-channel'; - - /// Capability required to update/edit channel members - /// Channel is not distinct and user has UpdateChannelMembers permission - static const String updateChannelMembers = 'update-channel-members'; - - /// Capability required to send a poll in a channel. - static const String sendPoll = 'send-poll'; -} diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 9e7e2372e1..01b114e5e0 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -43,6 +43,8 @@ export 'src/core/models/draft.dart'; export 'src/core/models/draft_message.dart'; export 'src/core/models/event.dart'; export 'src/core/models/filter.dart' show Filter; +export 'src/core/models/location.dart'; +export 'src/core/models/location_coordinates.dart'; export 'src/core/models/member.dart'; export 'src/core/models/message.dart'; export 'src/core/models/message_reminder.dart'; @@ -67,6 +69,5 @@ export 'src/core/platform_detector/platform_detector.dart'; export 'src/core/util/extension.dart'; export 'src/db/chat_persistence_client.dart'; export 'src/event_type.dart'; -export 'src/permission_type.dart'; export 'src/system_environment.dart'; export 'src/ws/connection_status.dart'; diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index bbcc539993..615f2e53f5 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -9,4 +9,4 @@ /// Current package version /// Used in [SystemEnvironmentManager] to build the `x-stream-client` header -const PACKAGE_VERSION = '9.16.0'; +const PACKAGE_VERSION = '10.0.0-beta.5'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 12fba2290c..adfaed364b 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 9.16.0 +version: 10.0.0-beta.5 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues diff --git a/packages/stream_chat/test/fixtures/channel_state_to_json.json b/packages/stream_chat/test/fixtures/channel_state_to_json.json index 95f5d56933..2169cba65f 100644 --- a/packages/stream_chat/test/fixtures/channel_state_to_json.json +++ b/packages/stream_chat/test/fixtures/channel_state_to_json.json @@ -33,9 +33,7 @@ "poll_id": null, "restricted_visibility": [ "user-id-3" - ], - "draft": null, - "reminder": null + ] }, { "id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f", @@ -50,9 +48,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0", @@ -67,9 +63,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "dry-meadow-0-80925be0-786e-40a5-b225-486518dafd35", @@ -84,9 +78,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe", @@ -101,9 +93,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "withered-cell-0-84cbd760-cf55-4f7e-9207-c5f66cccc6dc", @@ -118,9 +108,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "dry-meadow-0-e9203588-43c3-40b1-91f7-f217fc42aa53", @@ -135,9 +123,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "withered-cell-0-7e3552d7-7a0d-45f2-a856-e91b23a7e240", @@ -152,9 +138,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "dry-meadow-0-1ffeafd4-e4fc-4c84-9394-9d7cb10fff42", @@ -169,9 +153,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "dry-meadow-0-3f147324-12c8-4b41-9fb5-2db88d065efa", @@ -186,9 +168,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "dry-meadow-0-51a348ae-0c0a-44de-a556-eac7891c0cf0", @@ -203,9 +183,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "icy-recipe-7-a29e237b-8d81-4a97-9bc8-d42bca3f1356", @@ -220,9 +198,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055", @@ -237,9 +213,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "throbbing-boat-5-1e4d5730-5ff0-4d25-9948-9f34ffda43e4", @@ -254,9 +228,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21", @@ -271,9 +243,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "snowy-credit-3-3319537e-2d0e-4876-8170-a54f046e4b7d", @@ -288,9 +258,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d", @@ -305,9 +273,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "snowy-credit-3-cebe25a7-a3a3-49fc-9919-91c6725e81f3", @@ -322,9 +288,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "divine-glade-9-0cea9262-5766-48e9-8b22-311870aed3bf", @@ -339,9 +303,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "red-firefly-9-c4e9007b-bb7d-4238-ae08-5f8e3cd03d73", @@ -356,9 +318,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "bitter-glade-2-02aee4eb-4093-4736-808b-2de75820e854", @@ -373,9 +333,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "morning-sea-1-0c700bcb-46dd-4224-b590-e77bdbccc480", @@ -390,9 +348,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "ancient-salad-0-53e8b4e6-5b7b-43ad-aeee-8bfb6a9ed0be", @@ -407,9 +363,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "ancient-salad-0-8c225075-bd4c-42e2-8024-530aae13cd40", @@ -424,9 +378,7 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null }, { "id": "proud-sea-7-17802096-cbf8-4e3c-addd-4ee31f4c8b5c", @@ -441,12 +393,11 @@ "silent": false, "pinned": false, "pin_expires": null, - "poll_id": null, - "draft": null, - "reminder": null + "poll_id": null } ], "pinned_messages": [], "members": [], + "active_live_locations": [], "watcher_count": 5 } diff --git a/packages/stream_chat/test/fixtures/message.json b/packages/stream_chat/test/fixtures/message.json index 6b99f2fbc1..8e52ea2390 100644 --- a/packages/stream_chat/test/fixtures/message.json +++ b/packages/stream_chat/test/fixtures/message.json @@ -48,11 +48,13 @@ } ], "own_reactions": [], - "reaction_counts": { - "love": 1 - }, - "reaction_scores": { - "love": 1 + "reaction_groups": { + "love": { + "count": 1, + "sum_scores": 1, + "first_reaction_at": "2020-01-28T22:17:31.107978Z", + "last_reaction_at": "2020-01-28T22:17:31.107978Z" + } }, "pinned": false, "pinned_at": null, diff --git a/packages/stream_chat/test/fixtures/message_to_json.json b/packages/stream_chat/test/fixtures/message_to_json.json index 8803f1ed1d..c4568628d3 100644 --- a/packages/stream_chat/test/fixtures/message_to_json.json +++ b/packages/stream_chat/test/fixtures/message_to_json.json @@ -26,7 +26,5 @@ "restricted_visibility": [ "user-id-3" ], - "draft": null, - "reminder": null, "hey": "test" } \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/reaction.json b/packages/stream_chat/test/fixtures/reaction.json index ce87ca1d20..c0e8ef35c5 100644 --- a/packages/stream_chat/test/fixtures/reaction.json +++ b/packages/stream_chat/test/fixtures/reaction.json @@ -13,6 +13,7 @@ }, "type": "wow", "score": 1, + "emoji_code": "\uD83D\uDE2E", "created_at": "2020-01-28T22:17:31.108742Z", "updated_at": "2020-01-28T22:17:31.108742Z" } \ No newline at end of file diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 9aed5cea17..9fd0662e36 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -215,6 +215,7 @@ void main() { tearDown(() { channel.dispose(); + clearInteractions(client); }); test('should throw if trying to set `extraData`', () { @@ -288,6 +289,265 @@ void main() { )).called(1); }); + test( + 'should handle StreamChatNetworkError by adding message to retry queue with skipPush: true, skipEnrichUrl: false', + () async { + final message = Message( + id: 'test-message-id', + user: client.state.currentUser, + ); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipPush: true, + )).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.sending), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.sendingFailed( + skipPush: true, + skipEnrichUrl: false, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.sendMessage( + message, + skipPush: true, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + + test( + 'should handle StreamChatNetworkError by adding message to retry queue with skipPush: true, skipEnrichUrl: true', + () async { + final message = Message( + id: 'test-message-id-2', + user: client.state.currentUser, + ); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipPush: true, + skipEnrichUrl: true, + )).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.sending), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.sendingFailed( + skipPush: true, + skipEnrichUrl: true, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.sendMessage( + message, + skipPush: true, + skipEnrichUrl: true, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + + test( + 'should handle StreamChatNetworkError by adding message to retry queue with skipPush: false, skipEnrichUrl: true', + () async { + final message = Message( + id: 'test-message-id-3', + user: client.state.currentUser, + ); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipEnrichUrl: true, + )).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.sending), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: true, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.sendMessage( + message, + skipEnrichUrl: true, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + + test( + 'should handle StreamChatNetworkError by adding message to retry queue with skipPush: false, skipEnrichUrl: false', + () async { + final message = Message( + id: 'test-message-id-4', + user: client.state.currentUser, + ); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + )).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.sending), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: false, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.sendMessage( + message, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + + test('should update message state even when non-retriable error occurs', + () async { + final message = Message( + id: 'test-message-id', + user: client.state.currentUser, + ); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + )).thenThrow(StreamChatNetworkError.raw( + code: ChatErrorCode.inputError.code, + message: 'Input error', + data: ErrorResponse() + ..code = ChatErrorCode.inputError.code + ..message = 'Input error' + ..statusCode = 400, + )); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.sending), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: false, + ), + ), + matchMessageState: true, + ), + ] + ]), + ); + + try { + await channel.sendMessage(message); + } catch (e) { + expect(e, isA()); + } + }); + test('with attachments should work just fine', () async { final attachments = List.generate( 3, @@ -461,6 +721,109 @@ void main() { }); }); + group('`.sendStaticLocation`', () { + const deviceId = 'test-device-id'; + const locationId = 'test-location-id'; + const coordinates = LocationCoordinates( + latitude: 40.7128, + longitude: -74.0060, + ); + + test('should create a static location and call sendMessage', () async { + when( + () => client.sendMessage(any(), channelId, channelType), + ).thenAnswer( + (_) async => SendMessageResponse() + ..message = Message( + id: locationId, + text: 'Location shared', + extraData: const {'custom': 'data'}, + sharedLocation: Location( + channelCid: channel.cid, + messageId: locationId, + userId: client.state.currentUser?.id, + latitude: coordinates.latitude, + longitude: coordinates.longitude, + createdByDeviceId: deviceId, + ), + ), + ); + + final response = await channel.sendStaticLocation( + id: locationId, + messageText: 'Location shared', + createdByDeviceId: deviceId, + location: coordinates, + extraData: {'custom': 'data'}, + ); + + expect(response, isNotNull); + expect(response.message.id, locationId); + expect(response.message.text, 'Location shared'); + expect(response.message.extraData['custom'], 'data'); + expect(response.message.sharedLocation, isNotNull); + + verify( + () => client.sendMessage(any(), channelId, channelType), + ).called(1); + }); + }); + + group('`.startLiveLocationSharing`', () { + const deviceId = 'test-device-id'; + const locationId = 'test-location-id'; + final endSharingAt = DateTime.now().add(const Duration(hours: 1)); + const coordinates = LocationCoordinates( + latitude: 40.7128, + longitude: -74.0060, + ); + + test( + 'should create message with live location and call sendMessage', + () async { + when( + () => client.sendMessage(any(), channelId, channelType), + ).thenAnswer( + (_) async => SendMessageResponse() + ..message = Message( + id: locationId, + text: 'Location shared', + extraData: const {'custom': 'data'}, + sharedLocation: Location( + channelCid: channel.cid, + messageId: locationId, + userId: client.state.currentUser?.id, + latitude: coordinates.latitude, + longitude: coordinates.longitude, + createdByDeviceId: deviceId, + endAt: endSharingAt, + ), + ), + ); + + final response = await channel.startLiveLocationSharing( + id: locationId, + messageText: 'Location shared', + createdByDeviceId: deviceId, + location: coordinates, + endSharingAt: endSharingAt, + extraData: {'custom': 'data'}, + ); + + expect(response, isNotNull); + expect(response.message.id, locationId); + expect(response.message.text, 'Location shared'); + expect(response.message.extraData['custom'], 'data'); + expect(response.message.sharedLocation, isNotNull); + expect(response.message.sharedLocation?.endAt, endSharingAt); + + verify( + () => client.sendMessage(any(), channelId, channelType), + ).called(1); + }, + ); + }); + group('`.createDraft`', () { final draftMessage = DraftMessage(text: 'Draft message text'); @@ -887,26 +1250,273 @@ void main() { any(that: isSameMessageAs(message)), )).called(1); }); - }); - - test('`.partialUpdateMessage`', () async { - final message = Message( - id: 'test-message-id', - state: MessageState.sent, - ); - - const set = {'text': 'Update Message text'}; - const unset = ['pinExpires']; - final updateMessageResponse = UpdateMessageResponse() - ..message = message.copyWith(text: set['text'], pinExpires: null); + test( + 'should update message state even when error is not StreamChatNetworkError', + () async { + final message = Message( + id: 'test-message-id-error-1', + state: MessageState.sent, + ); - when( - () => client.partialUpdateMessage(message.id, set: set, unset: unset), - ).thenAnswer((_) async => updateMessageResponse); + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + skipEnrichUrl: true, + )).thenThrow(ArgumentError('Invalid argument')); - expectLater( - // skipping first seed message list -> [] messages + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.updating), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updatingFailed( + skipPush: false, + skipEnrichUrl: true, + ), + ), + matchMessageState: true, + ), + ] + ]), + ); + + try { + await channel.updateMessage(message, skipEnrichUrl: true); + } catch (e) { + expect(e, isA()); + } + }); + + test( + 'should add message to retry queue when retriable StreamChatNetworkError occurs with skipPush: false, skipEnrichUrl: true', + () async { + final message = Message( + id: 'test-message-id-retry-1', + state: MessageState.sent, + ); + + // Create a retriable error (data == null) + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + skipEnrichUrl: true, + )).thenThrow(StreamChatNetworkError.raw( + code: ChatErrorCode.requestTimeout.code, + message: 'Request timed out', + )); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.updating), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updatingFailed( + skipPush: false, + skipEnrichUrl: true, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.updateMessage(message, skipEnrichUrl: true); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.requestTimeout.code)); + expect(networkError.isRetriable, isTrue); + } + }); + + test( + 'should add message to retry queue when retriable StreamChatNetworkError occurs with skipPush: true, skipEnrichUrl: false', + () async { + final message = Message( + id: 'test-message-id-retry-2', + state: MessageState.sent, + ); + + // Create a retriable error (data == null) + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + skipPush: true, + )).thenThrow(StreamChatNetworkError.raw( + code: ChatErrorCode.internalSystemError.code, + message: 'Internal system error', + )); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.updating), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updatingFailed( + skipPush: true, + skipEnrichUrl: false, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.updateMessage(message, skipPush: true); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, + equals(ChatErrorCode.internalSystemError.code)); + expect(networkError.isRetriable, isTrue); + } + }); + + test( + 'should handle non-retriable StreamChatNetworkError with skipPush: true, skipEnrichUrl: true', + () async { + final message = Message( + id: 'test-message-id-error-2', + state: MessageState.sent, + ); + + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + skipPush: true, + skipEnrichUrl: true, + )).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.updating), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updatingFailed( + skipPush: true, + skipEnrichUrl: true, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.updateMessage( + message, + skipPush: true, + skipEnrichUrl: true, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + + test( + 'should handle non-retriable StreamChatNetworkError with skipPush: false, skipEnrichUrl: false', + () async { + final message = Message( + id: 'test-message-id-error-3', + state: MessageState.sent, + ); + + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + )).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.updating), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updatingFailed( + skipPush: false, + skipEnrichUrl: false, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.updateMessage(message); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + }); + + test('`.partialUpdateMessage`', () async { + final message = Message( + id: 'test-message-id', + state: MessageState.sent, + ); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + final updateMessageResponse = UpdateMessageResponse() + ..message = message.copyWith(text: set['text'], pinExpires: null); + + when( + () => client.partialUpdateMessage(message.id, set: set, unset: unset), + ).thenAnswer((_) async => updateMessageResponse); + + expectLater( + // skipping first seed message list -> [] messages channel.state?.messagesStream.skip(1), emitsInOrder([ [ @@ -914,37 +1524,376 @@ void main() { message.copyWith( state: MessageState.updating, ), - matchText: true, - matchMessageState: true, - ), - ], - [ - isSameMessageAs( - updateMessageResponse.message.copyWith( - state: MessageState.updated, + matchText: true, + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + updateMessageResponse.message.copyWith( + state: MessageState.updated, + ), + matchText: true, + matchMessageState: true, + ), + ], + ]), + ); + + final res = await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + ); + + expect(res, isNotNull); + expect(res.message.id, message.id); + expect(res.message.id, message.id); + expect(res.message.text, set['text']); + expect(res.message.pinExpires, isNull); + + verify( + () => client.partialUpdateMessage(message.id, set: set, unset: unset), + ).called(1); + }); + + group('`.partialUpdateMessage` error handling', () { + test( + 'should update message state even when error is not StreamChatNetworkError', + () async { + final message = Message( + id: 'test-message-id-error-partial-1', + state: MessageState.sent, + ); + + // Add message to channel state first + channel.state?.updateMessage(message); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + when( + () => client.partialUpdateMessage( + message.id, + set: set, + unset: unset, + ), + ).thenThrow(ArgumentError('Invalid argument')); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updating, + ), + matchText: true, + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: false, + ), + ), + matchText: true, + matchMessageState: true, + ), + ] + ]), + ); + + try { + await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + ); + } catch (e) { + expect(e, isA()); + } + }); + + test( + 'should add message to retry queue when retriable StreamChatNetworkError occurs with skipEnrichUrl: true', + () async { + final message = Message( + id: 'test-message-id-retry-partial-1', + state: MessageState.sent, + ); + + // Add message to channel state first + channel.state?.updateMessage(message); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + // Create a retriable error (data == null) + when( + () => client.partialUpdateMessage( + message.id, + set: set, + unset: unset, + skipEnrichUrl: true, + ), + ).thenThrow(StreamChatNetworkError.raw( + code: ChatErrorCode.requestTimeout.code, + message: 'Request timed out', + )); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updating, + ), + matchText: true, + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: true, + ), + ), + matchText: true, + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + skipEnrichUrl: true, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.requestTimeout.code)); + expect(networkError.isRetriable, isTrue); + } + }); + + test( + 'should add message to retry queue when retriable StreamChatNetworkError occurs with skipEnrichUrl: false', + () async { + final message = Message( + id: 'test-message-id-retry-partial-2', + state: MessageState.sent, + ); + + // Add message to channel state first + channel.state?.updateMessage(message); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + // Create a retriable error (data == null) + when( + () => client.partialUpdateMessage( + message.id, + set: set, + unset: unset, + ), + ).thenThrow(StreamChatNetworkError.raw( + code: ChatErrorCode.internalSystemError.code, + message: 'Internal system error', + )); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updating, + ), + matchText: true, + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: false, + ), + ), + matchText: true, + matchMessageState: true, ), - matchText: true, - matchMessageState: true, - ), - ], - ]), - ); + ], + ]), + ); - final res = await channel.partialUpdateMessage( - message, - set: set, - unset: unset, - ); + try { + await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + ); + } catch (e) { + expect(e, isA()); - expect(res, isNotNull); - expect(res.message.id, message.id); - expect(res.message.id, message.id); - expect(res.message.text, set['text']); - expect(res.message.pinExpires, isNull); + final networkError = e as StreamChatNetworkError; + expect(networkError.code, + equals(ChatErrorCode.internalSystemError.code)); + expect(networkError.isRetriable, isTrue); + } + }); - verify( - () => client.partialUpdateMessage(message.id, set: set, unset: unset), - ).called(1); + test( + 'should handle non-retriable StreamChatNetworkError with skipEnrichUrl: true', + () async { + final message = Message( + id: 'test-message-id-error-partial-2', + state: MessageState.sent, + ); + + // Add message to channel state first + channel.state?.updateMessage(message); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + when( + () => client.partialUpdateMessage( + message.id, + set: set, + unset: unset, + skipEnrichUrl: true, + ), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updating, + ), + matchText: true, + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: true, + ), + ), + matchText: true, + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + skipEnrichUrl: true, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + + test( + 'should handle non-retriable StreamChatNetworkError with skipEnrichUrl: false', + () async { + final message = Message( + id: 'test-message-id-error-partial-3', + state: MessageState.sent, + ); + + // Add message to channel state first + channel.state?.updateMessage(message); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + when( + () => client.partialUpdateMessage( + message.id, + set: set, + unset: unset, + ), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updating, + ), + matchText: true, + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: false, + ), + ), + matchText: true, + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); }); group('`.deleteMessage`', () { @@ -1402,139 +2351,24 @@ void main() { group('`.sendReaction`', () { test('should work fine', () async { - const type = 'test-reaction-type'; final message = Message( id: 'test-message-id', state: MessageState.sent, ); - final reaction = Reaction(type: type, messageId: message.id); - - when(() => client.sendReaction(message.id, type)).thenAnswer( - (_) async => SendReactionResponse() - ..message = message - ..reaction = reaction, - ); - - expectLater( - // skipping first seed message list -> [] messages - channel.state?.messagesStream.skip(1), - emitsInOrder([ - [ - isSameMessageAs( - message.copyWith( - state: MessageState.sent, - reactionGroups: {type: ReactionGroup(count: 1, sumScores: 1)}, - latestReactions: [reaction], - ownReactions: [reaction], - ), - matchReactions: true, - matchMessageState: true, - ), - ], - ]), - ); - - final res = await channel.sendReaction(message, type); - - expect(res, isNotNull); - expect(res.reaction.type, type); - expect(res.reaction.messageId, message.id); - - verify(() => client.sendReaction(message.id, type)).called(1); - }); - - test('should work fine with score passed explicitly', () async { - const type = 'test-reaction-type'; - final message = Message( - id: 'test-message-id', - state: MessageState.sent, - ); + const type = 'like'; + const emojiCode = '๐Ÿ‘'; + const score = 4; - const score = 5; final reaction = Reaction( type: type, messageId: message.id, + emojiCode: emojiCode, score: score, + user: client.state.currentUser, ); - when(() => client.sendReaction( - message.id, - type, - score: score, - )).thenAnswer( - (_) async => SendReactionResponse() - ..message = message - ..reaction = reaction, - ); - - expectLater( - // skipping first seed message list -> [] messages - channel.state?.messagesStream.skip(1), - emitsInOrder([ - [ - isSameMessageAs( - message.copyWith( - state: MessageState.sent, - reactionGroups: { - type: ReactionGroup( - count: 1, - sumScores: score, - ) - }, - latestReactions: [reaction], - ownReactions: [reaction], - ), - matchReactions: true, - matchMessageState: true, - ), - ], - ]), - ); - - final res = await channel.sendReaction( - message, - type, - score: score, - ); - - expect(res, isNotNull); - expect(res.reaction.type, type); - expect(res.reaction.messageId, message.id); - expect(res.reaction.score, score); - - verify(() => client.sendReaction( - message.id, - type, - score: score, - )).called(1); - }); - - test('should work fine with score passed explicitly and in extraData', - () async { - const type = 'test-reaction-type'; - final message = Message( - id: 'test-message-id', - state: MessageState.sent, - ); - - const score = 5; - const extraDataScore = 3; - const extraData = { - 'score': extraDataScore, - }; - final reaction = Reaction( - type: type, - messageId: message.id, - score: extraDataScore, - ); - - when(() => client.sendReaction( - message.id, - type, - score: score, - extraData: extraData, - )).thenAnswer( + when(() => client.sendReaction(message.id, reaction)).thenAnswer( (_) async => SendReactionResponse() ..message = message ..reaction = reaction, @@ -1548,12 +2382,7 @@ void main() { isSameMessageAs( message.copyWith( state: MessageState.sent, - reactionGroups: { - type: ReactionGroup( - count: 1, - sumScores: extraDataScore, - ) - }, + reactionGroups: {type: ReactionGroup(count: 1, sumScores: 1)}, latestReactions: [reaction], ownReactions: [reaction], ), @@ -1564,27 +2393,15 @@ void main() { ]), ); - final res = await channel.sendReaction( - message, - type, - score: score, - extraData: extraData, - ); + final res = await channel.sendReaction(message, reaction); expect(res, isNotNull); expect(res.reaction.type, type); expect(res.reaction.messageId, message.id); - expect( - res.reaction.score, - extraDataScore, - ); + expect(res.reaction.emojiCode, emojiCode); + expect(res.reaction.score, score); - verify(() => client.sendReaction( - message.id, - type, - score: score, - extraData: extraData, - )).called(1); + verify(() => client.sendReaction(message.id, reaction)).called(1); }); test( @@ -1596,9 +2413,13 @@ void main() { state: MessageState.sent, ); - final reaction = Reaction(type: type, messageId: message.id); + final reaction = Reaction( + type: type, + messageId: message.id, + user: client.state.currentUser, + ); - when(() => client.sendReaction(message.id, type)) + when(() => client.sendReaction(message.id, reaction)) .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); expectLater( @@ -1633,25 +2454,24 @@ void main() { ); try { - await channel.sendReaction(message, type); + await channel.sendReaction(message, reaction); } catch (e) { expect(e, isA()); } - verify(() => client.sendReaction(message.id, type)).called(1); + verify(() => client.sendReaction(message.id, reaction)).called(1); }, ); test( '''should override previous reaction if present and `enforceUnique` is true''', () async { - const userId = 'test-user-id'; const messageId = 'test-message-id'; const prevType = 'test-reaction-type'; final prevReaction = Reaction( type: prevType, messageId: messageId, - userId: userId, + user: client.state.currentUser, ); final message = Message( id: messageId, @@ -1670,7 +2490,7 @@ void main() { final newReaction = Reaction( type: type, messageId: messageId, - userId: userId, + user: client.state.currentUser, ); final newMessage = message.copyWith( ownReactions: [newReaction], @@ -1681,7 +2501,7 @@ void main() { when(() => client.sendReaction( messageId, - type, + newReaction, enforceUnique: enforceUnique, )).thenAnswer( (_) async => SendReactionResponse() @@ -1705,7 +2525,7 @@ void main() { final res = await channel.sendReaction( message, - type, + newReaction, enforceUnique: enforceUnique, ); @@ -1715,7 +2535,7 @@ void main() { verify(() => client.sendReaction( messageId, - type, + newReaction, enforceUnique: enforceUnique, )).called(1); }, @@ -1731,9 +2551,13 @@ void main() { state: MessageState.sent, ); - final reaction = Reaction(type: type, messageId: message.id); + final reaction = Reaction( + type: type, + messageId: message.id, + user: client.state.currentUser, + ); - when(() => client.sendReaction(message.id, type)).thenAnswer( + when(() => client.sendReaction(message.id, reaction)).thenAnswer( (_) async => SendReactionResponse() ..message = message ..reaction = reaction, @@ -1766,13 +2590,13 @@ void main() { ]), ); - final res = await channel.sendReaction(message, type); + final res = await channel.sendReaction(message, reaction); expect(res, isNotNull); expect(res.reaction.type, type); expect(res.reaction.messageId, message.id); - verify(() => client.sendReaction(message.id, type)).called(1); + verify(() => client.sendReaction(message.id, reaction)).called(1); }); test( @@ -1785,9 +2609,13 @@ void main() { state: MessageState.sent, ); - final reaction = Reaction(type: type, messageId: message.id); + final reaction = Reaction( + type: type, + messageId: message.id, + user: client.state.currentUser, + ); - when(() => client.sendReaction(message.id, type)) + when(() => client.sendReaction(message.id, reaction)) .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); expectLater( @@ -1826,26 +2654,25 @@ void main() { ); try { - await channel.sendReaction(message, type); + await channel.sendReaction(message, reaction); } catch (e) { expect(e, isA()); } - verify(() => client.sendReaction(message.id, type)).called(1); + verify(() => client.sendReaction(message.id, reaction)).called(1); }, ); test( '''should override previous thread reaction if present and `enforceUnique` is true''', () async { - const userId = 'test-user-id'; const messageId = 'test-message-id'; const parentId = 'test-parent-id'; const prevType = 'test-reaction-type'; final prevReaction = Reaction( type: prevType, messageId: messageId, - userId: userId, + user: client.state.currentUser, ); final message = Message( id: messageId, @@ -1865,7 +2692,7 @@ void main() { final newReaction = Reaction( type: type, messageId: messageId, - userId: userId, + user: client.state.currentUser, ); final newMessage = message.copyWith( ownReactions: [newReaction], @@ -1876,7 +2703,7 @@ void main() { when(() => client.sendReaction( messageId, - type, + newReaction, enforceUnique: enforceUnique, )).thenAnswer( (_) async => SendReactionResponse() @@ -1903,7 +2730,7 @@ void main() { final res = await channel.sendReaction( message, - type, + newReaction, enforceUnique: enforceUnique, ); @@ -1913,7 +2740,7 @@ void main() { verify(() => client.sendReaction( messageId, - type, + newReaction, enforceUnique: enforceUnique, )).called(1); }, @@ -4755,63 +5582,543 @@ void main() { // Wait for the event to be processed await Future.delayed(Duration.zero); - // Verify thread message reminder was updated - final updatedMessage = channel.state?.threads[parentId]?.firstWhere( - (m) => m.id == messageId, - ); - expect(updatedMessage?.reminder, isNotNull); - expect(updatedMessage?.reminder?.messageId, messageId); - expect(updatedMessage?.reminder?.remindAt, updatedRemindAt); + // Verify thread message reminder was updated + final updatedMessage = channel.state?.threads[parentId]?.firstWhere( + (m) => m.id == messageId, + ); + expect(updatedMessage?.reminder, isNotNull); + expect(updatedMessage?.reminder?.messageId, messageId); + expect(updatedMessage?.reminder?.remindAt, updatedRemindAt); + }); + + test('should handle reminder.deleted event for thread messages', + () async { + const messageId = 'test-message-id'; + const parentId = 'test-parent-id'; + + // Setup initial state with a thread message with existing reminder + final remindAt = DateTime.now().add(const Duration(days: 30)); + final initialReminder = MessageReminder( + messageId: messageId, + channelCid: channel.cid!, + userId: 'test-user-id', + remindAt: remindAt, + ); + + final threadMessage = Message( + id: messageId, + parentId: parentId, + user: client.state.currentUser, + text: 'Thread message', + reminder: initialReminder, + ); + + channel.state?.updateMessage(threadMessage); + + // Verify initial state + final initialMessage = channel.state?.threads[parentId]?.firstWhere( + (m) => m.id == messageId, + ); + expect(initialMessage?.reminder, isNotNull); + + // Create reminder.deleted event + final reminderDeletedEvent = Event( + cid: channel.cid, + type: EventType.reminderDeleted, + reminder: initialReminder, + ); + + // Dispatch event + client.addEvent(reminderDeletedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify thread message reminder was removed + final updatedMessage = channel.state?.threads[parentId]?.firstWhere( + (m) => m.id == messageId, + ); + expect(updatedMessage?.reminder, isNull); + }); + }); + + group('Location events', () { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + late Channel channel; + + setUp(() { + final channelState = _generateChannelState(channelId, channelType); + channel = Channel.fromState(client, channelState); + }); + + tearDown(() { + channel.dispose(); + }); + + test('should handle location.shared event', () async { + // Verify initial state + expect(channel.state?.activeLiveLocations, isEmpty); + + // Create live location + final liveLocation = Location( + channelCid: channel.cid, + userId: 'user1', + messageId: 'msg1', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final locationMessage = Message( + id: 'msg1', + text: 'Live location shared', + sharedLocation: liveLocation, + ); + + // Create location.shared event + final locationSharedEvent = Event( + cid: channel.cid, + type: EventType.locationShared, + message: locationMessage, + ); + + // Dispatch event + client.addEvent(locationSharedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Check if message was added + final messages = channel.state?.messages; + final message = messages?.firstWhere((m) => m.id == 'msg1'); + expect(message, isNotNull); + + // Check if active live location was updated + final activeLiveLocations = channel.state?.activeLiveLocations; + expect(activeLiveLocations, hasLength(1)); + expect(activeLiveLocations?.first.messageId, equals('msg1')); + }); + + test('should handle location.updated event', () async { + // Setup initial state with location message + final liveLocation = Location( + channelCid: channel.cid, + userId: 'user1', + messageId: 'msg1', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final locationMessage = Message( + id: 'msg1', + text: 'Live location shared', + sharedLocation: liveLocation, + ); + + // Add initial message + channel.state?.addNewMessage(locationMessage); + + // Create updated location + final updatedLocation = liveLocation.copyWith( + latitude: 40.7500, // Updated latitude + longitude: -74.1000, // Updated longitude + ); + + final updatedMessage = locationMessage.copyWith( + sharedLocation: updatedLocation, + ); + + // Create location.updated event + final locationUpdatedEvent = Event( + cid: channel.cid, + type: EventType.locationUpdated, + message: updatedMessage, + ); + + // Dispatch event + client.addEvent(locationUpdatedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Check if message was updated + final messages = channel.state?.messages; + final message = messages?.firstWhere((m) => m.id == 'msg1'); + expect(message?.sharedLocation?.latitude, equals(40.7500)); + expect(message?.sharedLocation?.longitude, equals(-74.1000)); + + // Check if active live location was updated + final activeLiveLocations = channel.state?.activeLiveLocations; + expect(activeLiveLocations, hasLength(1)); + expect(activeLiveLocations?.first.latitude, equals(40.7500)); + expect(activeLiveLocations?.first.longitude, equals(-74.1000)); + }); + + test('should handle location.expired event', () async { + // Setup initial state with location message + final liveLocation = Location( + channelCid: channel.cid, + userId: 'user1', + messageId: 'msg1', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final locationMessage = Message( + id: 'msg1', + text: 'Live location shared', + sharedLocation: liveLocation, + ); + + // Add initial message + channel.state?.addNewMessage(locationMessage); + expect(channel.state?.activeLiveLocations, hasLength(1)); + + // Create expired location + final expiredLocation = liveLocation.copyWith( + endAt: DateTime.now().subtract(const Duration(hours: 1)), + ); + + final expiredMessage = locationMessage.copyWith( + sharedLocation: expiredLocation, + ); + + // Create location.expired event + final locationExpiredEvent = Event( + cid: channel.cid, + type: EventType.locationExpired, + message: expiredMessage, + ); + + // Dispatch event + client.addEvent(locationExpiredEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Check if message was updated + final messages = channel.state?.messages; + final message = messages?.firstWhere((m) => m.id == 'msg1'); + expect(message?.sharedLocation?.isExpired, isTrue); + + // Check if active live location was removed + expect(channel.state?.activeLiveLocations, isEmpty); + }); + + test('should not add static location to active locations', () async { + final staticLocation = Location( + channelCid: channel.cid, + userId: 'user1', + messageId: 'msg1', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device1', + // No endAt - static location + ); + + final staticMessage = Message( + id: 'msg1', + text: 'Static location shared', + sharedLocation: staticLocation, + ); + + // Create location.shared event + final locationSharedEvent = Event( + cid: channel.cid, + type: EventType.locationShared, + message: staticMessage, + ); + + // Dispatch event + client.addEvent(locationSharedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Check if message was added + final messages = channel.state?.messages; + final message = messages?.firstWhere((m) => m.id == 'msg1'); + expect(message?.sharedLocation, isNotNull); + + // Check if active live location was NOT updated (should remain empty) + expect(channel.state?.activeLiveLocations, isEmpty); + }); + + test( + 'should update active locations when location message is deleted', + () async { + final liveLocation = Location( + channelCid: channel.cid, + userId: 'user1', + messageId: 'msg1', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final locationMessage = Message( + id: 'msg1', + text: 'Live location shared', + sharedLocation: liveLocation, + ); + + // Verify initial state + channel.state?.addNewMessage(locationMessage); + expect(channel.state?.activeLiveLocations, hasLength(1)); + + final messageDeletedEvent = Event( + type: EventType.messageDeleted, + cid: channel.cid, + message: locationMessage.copyWith( + type: MessageType.deleted, + deletedAt: DateTime.timestamp(), + ), + ); + + // Dispatch event + client.addEvent(messageDeletedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify active locations are updated + expect(channel.state?.activeLiveLocations, isEmpty); + }, + ); + + test('should merge locations with same key', () async { + final liveLocation = Location( + channelCid: channel.cid, + userId: 'user1', + messageId: 'msg1', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final locationMessage = Message( + id: 'msg1', + text: 'Live location shared', + sharedLocation: liveLocation, + ); + + // Add initial location for setup + channel.state?.addNewMessage(locationMessage); + expect(channel.state?.activeLiveLocations, hasLength(1)); + + // Create new location with same user, channel, and device + final newLocation = Location( + channelCid: channel.cid, + userId: 'user1', // Same user + messageId: 'msg2', // Different message + latitude: 40.7500, + longitude: -74.1000, + createdByDeviceId: 'device1', // Same device + endAt: DateTime.now().add(const Duration(hours: 2)), + ); + + final newMessage = Message( + id: 'msg2', + text: 'Updated location', + sharedLocation: newLocation, + ); + + // Create location.shared event for the new message + final locationSharedEvent = Event( + cid: channel.cid, + type: EventType.locationShared, + message: newMessage, + ); + + // Dispatch event + client.addEvent(locationSharedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Should still have only one active location (merged) + final activeLiveLocations = channel.state?.activeLiveLocations; + expect(activeLiveLocations, hasLength(1)); + expect(activeLiveLocations?.first.messageId, equals('msg2')); + expect(activeLiveLocations?.first.latitude, equals(40.7500)); + }); + + test( + 'should handle multiple active locations from different devices', + () async { + final liveLocation = Location( + channelCid: channel.cid, + userId: 'user1', + messageId: 'msg1', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final locationMessage = Message( + id: 'msg1', + text: 'Live location shared', + sharedLocation: liveLocation, + ); + + // Add first location for setup + channel.state?.addNewMessage(locationMessage); + expect(channel.state?.activeLiveLocations, hasLength(1)); + + // Create location from different device + final location2 = Location( + channelCid: channel.cid, + userId: 'user1', // Same user + messageId: 'msg2', + latitude: 34.0522, + longitude: -118.2437, + createdByDeviceId: 'device2', // Different device + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final message2 = Message( + id: 'msg2', + text: 'Location from device 2', + sharedLocation: location2, + ); + + // Create location.shared event for the second message + final locationSharedEvent = Event( + cid: channel.cid, + type: EventType.locationShared, + message: message2, + ); + + // Dispatch event + client.addEvent(locationSharedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Should have two active locations + expect(channel.state?.activeLiveLocations, hasLength(2)); + }, + ); + + test('should handle location messages in threads', () async { + final parentMessage = Message( + id: 'parent1', + text: 'Thread parent', + ); + + // Add parent message first for setup + channel.state?.addNewMessage(parentMessage); + + final liveLocation = Location( + channelCid: channel.cid, + userId: 'user1', + messageId: 'thread-msg1', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final threadLocationMessage = Message( + id: 'thread-msg1', + text: 'Live location in thread', + parentId: 'parent1', + sharedLocation: liveLocation, + ); + + // Create location.shared event for the thread message + final locationSharedEvent = Event( + cid: channel.cid, + type: EventType.locationShared, + message: threadLocationMessage, + ); + + // Dispatch event + client.addEvent(locationSharedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Check if thread message was added + final thread = channel.state?.threads['parent1']; + expect(thread, contains(threadLocationMessage)); + + // Check if location was added to active locations + final activeLiveLocations = channel.state?.activeLiveLocations; + expect(activeLiveLocations, hasLength(1)); + expect(activeLiveLocations?.first.messageId, equals('thread-msg1')); }); - test('should handle reminder.deleted event for thread messages', - () async { - const messageId = 'test-message-id'; - const parentId = 'test-parent-id'; + test('should update thread location messages', () async { + final parentMessage = Message( + id: 'parent1', + text: 'Thread parent', + ); - // Setup initial state with a thread message with existing reminder - final remindAt = DateTime.now().add(const Duration(days: 30)); - final initialReminder = MessageReminder( - messageId: messageId, - channelCid: channel.cid!, - userId: 'test-user-id', - remindAt: remindAt, + final liveLocation = Location( + channelCid: channel.cid, + userId: 'user1', + messageId: 'thread-msg1', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device1', + endAt: DateTime.now().add(const Duration(hours: 1)), ); - final threadMessage = Message( - id: messageId, - parentId: parentId, - user: client.state.currentUser, - text: 'Thread message', - reminder: initialReminder, + final threadLocationMessage = Message( + id: 'thread-msg1', + text: 'Live location in thread', + parentId: 'parent1', + sharedLocation: liveLocation, ); - channel.state?.updateMessage(threadMessage); + // Add messages + channel.state?.addNewMessage(parentMessage); + channel.state?.addNewMessage(threadLocationMessage); - // Verify initial state - final initialMessage = channel.state?.threads[parentId]?.firstWhere( - (m) => m.id == messageId, + // Update the location + final updatedLocation = liveLocation.copyWith( + latitude: 40.7500, + longitude: -74.1000, ); - expect(initialMessage?.reminder, isNotNull); - // Create reminder.deleted event - final reminderDeletedEvent = Event( + final updatedThreadMessage = threadLocationMessage.copyWith( + sharedLocation: updatedLocation, + ); + + // Create location.updated event for the thread message + final locationUpdatedEvent = Event( cid: channel.cid, - type: EventType.reminderDeleted, - reminder: initialReminder, + type: EventType.locationUpdated, + message: updatedThreadMessage, ); // Dispatch event - client.addEvent(reminderDeletedEvent); + client.addEvent(locationUpdatedEvent); // Wait for the event to be processed await Future.delayed(Duration.zero); - // Verify thread message reminder was removed - final updatedMessage = channel.state?.threads[parentId]?.firstWhere( - (m) => m.id == messageId, - ); - expect(updatedMessage?.reminder, isNull); + // Check if thread message was updated + final thread = channel.state?.threads['parent1']; + final threadMessage = thread?.firstWhere((m) => m.id == 'thread-msg1'); + expect(threadMessage?.sharedLocation?.latitude, equals(40.7500)); + expect(threadMessage?.sharedLocation?.longitude, equals(-74.1000)); + + // Check if active location was updated + final activeLiveLocations = channel.state?.activeLiveLocations; + expect(activeLiveLocations, hasLength(1)); + expect(activeLiveLocations?.first.latitude, equals(40.7500)); + expect(activeLiveLocations?.first.longitude, equals(-74.1000)); }); }); }); @@ -5076,6 +6383,12 @@ void main() { (channel) => channel.canQueryPollVotes, ); + testCapability( + 'ShareLocation', + ChannelCapability.shareLocation, + (channel) => channel.canShareLocation, + ); + test('returns correct values with multiple capabilities', () { final channelState = _generateChannelState( channelId, @@ -5257,4 +6570,295 @@ void main() { ); }); }); + + group('Retry functionality with parameter preservation', () { + late final client = MockStreamChatClient(); + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + late Channel channel; + + setUpAll(() { + registerFallbackValue(FakeMessage()); + registerFallbackValue([]); + registerFallbackValue(FakeAttachmentFile()); + + when(() => client.detachedLogger(any())).thenAnswer((invocation) { + final name = invocation.positionalArguments.first; + return _createLogger(name); + }); + + when(() => client.logger).thenReturn(_createLogger('mock-client-logger')); + + final clientState = FakeClientState(); + when(() => client.state).thenReturn(clientState); + + final retryPolicy = RetryPolicy( + shouldRetry: (_, __, error) { + return error is StreamChatNetworkError && error.isRetriable; + }, + ); + when(() => client.retryPolicy).thenReturn(retryPolicy); + }); + + setUp(() { + final channelState = _generateChannelState(channelId, channelType); + channel = Channel.fromState(client, channelState); + }); + + tearDown(() { + channel.dispose(); + }); + + group('retryMessage method', () { + test( + 'should call sendMessage with preserved skipPush and skipEnrichUrl parameters', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.sendingFailed( + skipPush: true, + skipEnrichUrl: true, + ), + ); + + final sendMessageResponse = SendMessageResponse() + ..message = message.copyWith(state: MessageState.sent); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipPush: true, + skipEnrichUrl: true, + )).thenAnswer((_) async => sendMessageResponse); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipPush: true, + skipEnrichUrl: true, + )).called(1); + }); + + test('should call sendMessage with preserved skipPush parameter', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.sendingFailed( + skipPush: true, + skipEnrichUrl: false, + ), + ); + + final sendMessageResponse = SendMessageResponse() + ..message = message.copyWith(state: MessageState.sent); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipPush: true, + )).thenAnswer((_) async => sendMessageResponse); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipPush: true, + )).called(1); + }); + + test('should call sendMessage with preserved skipEnrichUrl parameter', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: true, + ), + ); + + final sendMessageResponse = SendMessageResponse() + ..message = message.copyWith(state: MessageState.sent); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipEnrichUrl: true, + )).thenAnswer((_) async => sendMessageResponse); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipEnrichUrl: true, + )).called(1); + }); + + test( + 'should call sendMessage with preserved false skipPush and skipEnrichUrl parameters', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: false, + ), + ); + + final sendMessageResponse = SendMessageResponse() + ..message = message.copyWith(state: MessageState.sent); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + )).thenAnswer((_) async => sendMessageResponse); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + )).called(1); + }); + + test( + 'should call updateMessage with preserved skipPush, skipEnrichUrl parameter', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.updatingFailed( + skipPush: true, + skipEnrichUrl: true, + ), + ); + + final updateMessageResponse = UpdateMessageResponse() + ..message = message.copyWith(state: MessageState.updated); + + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + skipPush: true, + skipEnrichUrl: true, + )).thenAnswer((_) async => updateMessageResponse); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.updateMessage( + any(that: isSameMessageAs(message)), + skipPush: true, + skipEnrichUrl: true, + )).called(1); + }); + + test( + 'should call updateMessage with preserved false skipPush, skipEnrichUrl parameter', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.updatingFailed( + skipPush: false, + skipEnrichUrl: false, + ), + ); + + final updateMessageResponse = UpdateMessageResponse() + ..message = message.copyWith(state: MessageState.updated); + + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + )).thenAnswer((_) async => updateMessageResponse); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.updateMessage( + any(that: isSameMessageAs(message)), + )).called(1); + }); + + test('should call deleteMessage with preserved hard parameter', () async { + final message = Message( + id: 'test-message-id', + createdAt: DateTime.now(), + state: MessageState.deletingFailed(hard: true), + ); + + when(() => client.deleteMessage( + message.id, + hard: true, + )).thenAnswer((_) async => EmptyResponse()); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.deleteMessage( + message.id, + hard: true, + )).called(1); + }); + + test('should call deleteMessage with preserved false hard parameter', + () async { + final message = Message( + id: 'test-message-id', + createdAt: DateTime.now(), + state: MessageState.deletingFailed(hard: false), + ); + + when(() => client.deleteMessage( + message.id, + )).thenAnswer((_) async => EmptyResponse()); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.deleteMessage( + message.id, + )).called(1); + }); + + test('should throw AssertionError when message state is not failed', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.sent, + ); + + expect(() => channel.retryMessage(message), + throwsA(isA())); + }); + }); + }); } diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index 4eaad9f838..92a4591bb2 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -2826,6 +2826,361 @@ void main() { verifyNoMoreInteractions(api.moderation); }); + test('`.getActiveLiveLocations`', () async { + final locations = [ + Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ), + Location( + latitude: 34.0522, + longitude: -118.2437, + createdByDeviceId: 'device-2', + endAt: DateTime.now().add(const Duration(hours: 2)), + ), + ]; + + when(() => api.user.getActiveLiveLocations()).thenAnswer( + (_) async => GetActiveLiveLocationsResponse() // + ..activeLiveLocations = locations, + ); + + // Initial state should be empty + expect(client.state.activeLiveLocations, isEmpty); + + final res = await client.getActiveLiveLocations(); + + expect(res, isNotNull); + expect(res.activeLiveLocations, hasLength(2)); + expect(res.activeLiveLocations, equals(locations)); + expect(client.state.activeLiveLocations, equals(locations)); + + verify(() => api.user.getActiveLiveLocations()).called(1); + verifyNoMoreInteractions(api.user); + }); + + test('`.updateLiveLocation`', () async { + const messageId = 'test-message-id'; + const createdByDeviceId = 'test-device-id'; + final endAt = DateTime.timestamp().add(const Duration(hours: 1)); + const location = LocationCoordinates( + latitude: 40.7128, + longitude: -74.0060, + ); + + final expectedLocation = Location( + latitude: location.latitude, + longitude: location.longitude, + createdByDeviceId: createdByDeviceId, + endAt: endAt, + ); + + when( + () => api.user.updateLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + location: location, + endAt: endAt, + ), + ).thenAnswer((_) async => expectedLocation); + + final res = await client.updateLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + location: location, + endAt: endAt, + ); + + expect(res, isNotNull); + expect(res, equals(expectedLocation)); + + verify( + () => api.user.updateLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + location: location, + endAt: endAt, + ), + ).called(1); + verifyNoMoreInteractions(api.user); + }); + + test('`.stopLiveLocation`', () async { + const messageId = 'test-message-id'; + const createdByDeviceId = 'test-device-id'; + + final expectedLocation = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: createdByDeviceId, + endAt: DateTime.now(), // Should be expired + ); + + when( + () => api.user.updateLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + endAt: any(named: 'endAt'), + ), + ).thenAnswer((_) async => expectedLocation); + + final res = await client.stopLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + ); + + expect(res, isNotNull); + expect(res, equals(expectedLocation)); + + verify( + () => api.user.updateLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + endAt: any(named: 'endAt'), + ), + ).called(1); + verifyNoMoreInteractions(api.user); + }); + + group('Live Location Event Handling', () { + test('should handle location.shared event', () async { + final location = Location( + channelCid: 'test-channel:123', + messageId: 'message-123', + userId: userId, + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final event = Event( + type: EventType.locationShared, + cid: 'test-channel:123', + message: Message( + id: 'message-123', + sharedLocation: location, + ), + ); + + // Initially empty + expect(client.state.activeLiveLocations, isEmpty); + + // Trigger the event + client.handleEvent(event); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + // Should add location to active live locations + final activeLiveLocations = client.state.activeLiveLocations; + expect(activeLiveLocations, hasLength(1)); + expect(activeLiveLocations.first.messageId, equals('message-123')); + }); + + test('should handle location.updated event', () async { + final initialLocation = Location( + channelCid: 'test-channel:123', + messageId: 'message-123', + userId: userId, + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + // Set initial location + client.state.activeLiveLocations = [initialLocation]; + + final updatedLocation = Location( + channelCid: 'test-channel:123', + messageId: 'message-123', + userId: userId, + latitude: 40.7500, // Updated latitude + longitude: -74.1000, // Updated longitude + createdByDeviceId: 'device-1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final event = Event( + type: EventType.locationUpdated, + cid: 'test-channel:123', + message: Message( + id: 'message-123', + sharedLocation: updatedLocation, + ), + ); + + // Trigger the event + client.handleEvent(event); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + // Should update the location + final activeLiveLocations = client.state.activeLiveLocations; + expect(activeLiveLocations, hasLength(1)); + expect(activeLiveLocations.first.latitude, equals(40.7500)); + expect(activeLiveLocations.first.longitude, equals(-74.1000)); + }); + + test('should handle location.expired event', () async { + final location = Location( + channelCid: 'test-channel:123', + messageId: 'message-123', + userId: userId, + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + // Set initial location + client.state.activeLiveLocations = [location]; + expect(client.state.activeLiveLocations, hasLength(1)); + + final expiredLocation = location.copyWith( + endAt: DateTime.now().subtract(const Duration(hours: 1)), + ); + + final event = Event( + type: EventType.locationExpired, + cid: 'test-channel:123', + message: Message( + id: 'message-123', + sharedLocation: expiredLocation, + ), + ); + + // Trigger the event + client.handleEvent(event); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + // Should remove the location + expect(client.state.activeLiveLocations, isEmpty); + }); + + test('should ignore location events for other users', () async { + final location = Location( + channelCid: 'test-channel:123', + messageId: 'message-123', + userId: 'other-user', // Different user + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final event = Event( + type: EventType.locationShared, + cid: 'test-channel:123', + message: Message( + id: 'message-123', + sharedLocation: location, + ), + ); + + // Trigger the event + client.handleEvent(event); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + // Should not add location from other user + expect(client.state.activeLiveLocations, isEmpty); + }); + + test('should ignore static location events', () async { + final staticLocation = Location( + channelCid: 'test-channel:123', + messageId: 'message-123', + userId: userId, + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-1', + // No endAt means it's static + ); + + final event = Event( + type: EventType.locationShared, + cid: 'test-channel:123', + message: Message( + id: 'message-123', + sharedLocation: staticLocation, + ), + ); + + // Trigger the event + client.handleEvent(event); + + // Wait for the event to get processed + await Future.delayed(Duration.zero); + + // Should not add static location + expect(client.state.activeLiveLocations, isEmpty); + }); + + test('should merge locations with same key', () async { + final location1 = Location( + channelCid: 'test-channel:123', + messageId: 'message-123', + userId: userId, + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-1', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final location2 = Location( + channelCid: 'test-channel:123', + messageId: 'message-456', + userId: userId, + latitude: 40.7500, + longitude: -74.1000, + createdByDeviceId: 'device-1', // Same device, should merge + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final event1 = Event( + type: EventType.locationShared, + cid: 'test-channel:123', + message: Message( + id: 'message-123', + sharedLocation: location1, + ), + ); + + final event2 = Event( + type: EventType.locationShared, + cid: 'test-channel:123', + message: Message( + id: 'message-456', + sharedLocation: location2, + ), + ); + + // Trigger first event + client.handleEvent(event1); + await Future.delayed(Duration.zero); + + final activeLiveLocations = client.state.activeLiveLocations; + expect(activeLiveLocations, hasLength(1)); + expect(activeLiveLocations.first.messageId, equals('message-123')); + + // Trigger second event - should merge/update + client.handleEvent(event2); + await Future.delayed(Duration.zero); + + final activeLiveLocations2 = client.state.activeLiveLocations; + expect(activeLiveLocations2, hasLength(1)); + expect(activeLiveLocations2.first.messageId, equals('message-456')); + }); + }); + test('`.markAllRead`', () async { when(() => api.channel.markAllRead()) .thenAnswer((_) async => EmptyResponse()); @@ -2861,109 +3216,35 @@ void main() { verifyNoMoreInteractions(api.channel); }); - group('`.sendReaction`', () { - test('`.sendReaction with default params`', () async { - const messageId = 'test-message-id'; - const reactionType = 'like'; - const extraData = {'score': 1}; - - when(() => api.message.sendReaction( - messageId, - reactionType, - extraData: extraData, - )).thenAnswer((_) async => SendReactionResponse() - ..message = Message(id: messageId) - ..reaction = Reaction(type: reactionType, messageId: messageId)); - - final res = await client.sendReaction(messageId, reactionType); - expect(res, isNotNull); - expect(res.message.id, messageId); - expect(res.reaction.type, reactionType); - expect(res.reaction.messageId, messageId); - - verify(() => api.message.sendReaction( - messageId, - reactionType, - extraData: extraData, - )).called(1); - verifyNoMoreInteractions(api.message); - }); - - test('`.sendReaction with score`', () async { - const messageId = 'test-message-id'; - const reactionType = 'like'; - const score = 3; - const extraData = {'score': score}; - - when(() => api.message.sendReaction( - messageId, - reactionType, - extraData: extraData, - )).thenAnswer((_) async => SendReactionResponse() - ..message = Message(id: messageId) - ..reaction = Reaction( - type: reactionType, - messageId: messageId, - score: score, - )); - - final res = await client.sendReaction( - messageId, - reactionType, - score: score, - ); - expect(res, isNotNull); - expect(res.message.id, messageId); - expect(res.reaction.type, reactionType); - expect(res.reaction.messageId, messageId); - expect(res.reaction.score, score); - - verify(() => api.message.sendReaction( - messageId, - reactionType, - extraData: extraData, - )).called(1); - verifyNoMoreInteractions(api.message); - }); + test('`.sendReaction`', () async { + const messageId = 'test-message-id'; + const reactionType = 'like'; + const emojiCode = '๐Ÿ‘'; + const score = 4; - test('`.sendReaction with score passed in extradata also`', () async { - const messageId = 'test-message-id'; - const reactionType = 'like'; - const score = 3; - const extraDataScore = 5; - const extraData = {'score': extraDataScore}; + final reaction = Reaction( + type: reactionType, + messageId: messageId, + emojiCode: emojiCode, + score: score, + ); - when(() => api.message.sendReaction( - messageId, - reactionType, - extraData: extraData, - )).thenAnswer((_) async => SendReactionResponse() + when(() => api.message.sendReaction(messageId, reaction)).thenAnswer( + (_) async => SendReactionResponse() ..message = Message(id: messageId) - ..reaction = Reaction( - type: reactionType, - messageId: messageId, - score: extraDataScore, - )); + ..reaction = reaction, + ); - final res = await client.sendReaction( - messageId, - reactionType, - score: score, - extraData: extraData, - ); - expect(res, isNotNull); - expect(res.message.id, messageId); - expect(res.reaction.type, reactionType); - expect(res.reaction.messageId, messageId); - expect(res.reaction.score, extraDataScore); + final res = await client.sendReaction(messageId, reaction); + expect(res, isNotNull); + expect(res.message.id, messageId); + expect(res.reaction.type, reactionType); + expect(res.reaction.emojiCode, emojiCode); + expect(res.reaction.score, score); + expect(res.reaction.messageId, messageId); - verify(() => api.message.sendReaction( - messageId, - reactionType, - extraData: extraData, - )).called(1); - verifyNoMoreInteractions(api.message); - }); + verify(() => api.message.sendReaction(messageId, reaction)).called(1); + verifyNoMoreInteractions(api.message); }); test('`.deleteReaction`', () async { diff --git a/packages/stream_chat/test/src/client/event_resolvers_test.dart b/packages/stream_chat/test/src/client/event_resolvers_test.dart new file mode 100644 index 0000000000..0d8155ccf4 --- /dev/null +++ b/packages/stream_chat/test/src/client/event_resolvers_test.dart @@ -0,0 +1,669 @@ +// ignore_for_file: avoid_redundant_argument_values, lines_longer_than_80_chars + +import 'package:stream_chat/src/client/event_resolvers.dart'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/models/location.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/poll.dart'; +import 'package:stream_chat/src/core/models/poll_option.dart'; +import 'package:stream_chat/src/core/models/poll_vote.dart'; +import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/event_type.dart'; +import 'package:test/test.dart'; + +void main() { + group('Poll resolver events', () { + group('pollCreatedResolver', () { + test('should resolve messageNew event with poll to pollCreated', () { + final poll = Poll( + id: 'poll-123', + name: 'Test Poll', + options: const [ + PollOption(id: 'option-1', text: 'Option 1'), + PollOption(id: 'option-2', text: 'Option 2'), + ], + ); + + final event = Event( + type: EventType.messageNew, + poll: poll, + cid: 'channel-123', + ); + + final resolved = pollCreatedResolver(event); + + expect(resolved, isNotNull); + expect(resolved!.type, EventType.pollCreated); + expect(resolved.poll, equals(poll)); + expect(resolved.cid, equals('channel-123')); + }); + + test( + 'should resolve notificationMessageNew event with poll to pollCreated', + () { + final poll = Poll( + id: 'poll-123', + name: 'Test Poll', + options: const [ + PollOption(id: 'option-1', text: 'Option 1'), + ], + ); + + final event = Event( + type: EventType.notificationMessageNew, + poll: poll, + cid: 'channel-123', + ); + + final resolved = pollCreatedResolver(event); + + expect(resolved, isNotNull); + expect(resolved!.type, EventType.pollCreated); + expect(resolved.poll, equals(poll)); + }, + ); + + test('should return null for messageNew event without poll', () { + final event = Event( + type: EventType.messageNew, + cid: 'channel-123', + ); + + final resolved = pollCreatedResolver(event); + + expect(resolved, isNull); + }); + + test('should return null for invalid event types', () { + final poll = Poll( + id: 'poll-123', + name: 'Test Poll', + options: const [ + PollOption(id: 'option-1', text: 'Option 1'), + ], + ); + + final event = Event( + type: EventType.messageUpdated, + poll: poll, + cid: 'channel-123', + ); + + final resolved = pollCreatedResolver(event); + + expect(resolved, isNull); + }); + + test('should return null for event with null poll', () { + final event = Event( + type: EventType.messageNew, + poll: null, + cid: 'channel-123', + ); + + final resolved = pollCreatedResolver(event); + + expect(resolved, isNull); + }); + }); + + group('pollAnswerCastedResolver', () { + test( + 'should resolve pollVoteCasted event with answer to pollAnswerCasted', + () { + final pollVote = PollVote( + id: 'vote-123', + answerText: 'My answer', + pollId: 'poll-123', + ); + + final event = Event( + type: EventType.pollVoteCasted, + pollVote: pollVote, + cid: 'channel-123', + ); + + final resolved = pollAnswerCastedResolver(event); + + expect(resolved, isNotNull); + expect(resolved!.type, EventType.pollAnswerCasted); + expect(resolved.pollVote, equals(pollVote)); + expect(resolved.cid, equals('channel-123')); + }, + ); + + test( + 'should resolve pollVoteChanged event with answer to pollAnswerCasted', + () { + final pollVote = PollVote( + id: 'vote-123', + answerText: 'My updated answer', + pollId: 'poll-123', + ); + + final event = Event( + type: EventType.pollVoteChanged, + pollVote: pollVote, + cid: 'channel-123', + ); + + final resolved = pollAnswerCastedResolver(event); + + expect(resolved, isNotNull); + expect(resolved!.type, EventType.pollAnswerCasted); + expect(resolved.pollVote, equals(pollVote)); + }, + ); + + test('should return null for pollVoteCasted event with option vote', () { + final pollVote = PollVote( + id: 'vote-123', + optionId: 'option-1', + pollId: 'poll-123', + ); + + final event = Event( + type: EventType.pollVoteCasted, + pollVote: pollVote, + cid: 'channel-123', + ); + + final resolved = pollAnswerCastedResolver(event); + + expect(resolved, isNull); + }); + + test('should return null for invalid event types', () { + final pollVote = PollVote( + id: 'vote-123', + answerText: 'My answer', + pollId: 'poll-123', + ); + + final event = Event( + type: EventType.pollVoteRemoved, + pollVote: pollVote, + cid: 'channel-123', + ); + + final resolved = pollAnswerCastedResolver(event); + + expect(resolved, isNull); + }); + + test('should return resolved event for event with null pollVote', () { + final event = Event( + type: EventType.pollVoteCasted, + pollVote: null, + cid: 'channel-123', + ); + + final resolved = pollAnswerCastedResolver(event); + + expect(resolved, isNotNull); + expect(resolved!.type, EventType.pollAnswerCasted); + expect(resolved.pollVote, isNull); + }); + }); + + group('pollAnswerRemovedResolver', () { + test( + 'should resolve pollVoteRemoved event with answer to pollAnswerRemoved', + () { + final pollVote = PollVote( + id: 'vote-123', + answerText: 'My answer', + pollId: 'poll-123', + ); + + final event = Event( + type: EventType.pollVoteRemoved, + pollVote: pollVote, + cid: 'channel-123', + ); + + final resolved = pollAnswerRemovedResolver(event); + + expect(resolved, isNotNull); + expect(resolved!.type, EventType.pollAnswerRemoved); + expect(resolved.pollVote, equals(pollVote)); + expect(resolved.cid, equals('channel-123')); + }, + ); + + test('should return null for pollVoteRemoved event with option vote', () { + final pollVote = PollVote( + id: 'vote-123', + optionId: 'option-1', + pollId: 'poll-123', + ); + + final event = Event( + type: EventType.pollVoteRemoved, + pollVote: pollVote, + cid: 'channel-123', + ); + + final resolved = pollAnswerRemovedResolver(event); + + expect(resolved, isNull); + }); + + test('should return null for invalid event types', () { + final pollVote = PollVote( + id: 'vote-123', + answerText: 'My answer', + pollId: 'poll-123', + ); + + final event = Event( + type: EventType.pollVoteCasted, + pollVote: pollVote, + cid: 'channel-123', + ); + + final resolved = pollAnswerRemovedResolver(event); + + expect(resolved, isNull); + }); + + test('should return resolved event for event with null pollVote', () { + final event = Event( + type: EventType.pollVoteRemoved, + pollVote: null, + cid: 'channel-123', + ); + + final resolved = pollAnswerRemovedResolver(event); + + expect(resolved, isNotNull); + expect(resolved!.type, EventType.pollAnswerRemoved); + expect(resolved.pollVote, isNull); + }); + }); + }); + + group('Location resolver events', () { + group('locationSharedResolver', () { + test( + 'should resolve messageNew event with sharedLocation to locationShared', + () { + final location = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-123', + ); + + final message = Message( + id: 'message-123', + text: 'Check out this location', + sharedLocation: location, + user: User(id: 'user-123'), + ); + + final event = Event( + type: EventType.messageNew, + message: message, + cid: 'channel-123', + ); + + final resolved = locationSharedResolver(event); + + expect(resolved, isNotNull); + expect(resolved!.type, EventType.locationShared); + expect(resolved.message, equals(message)); + expect(resolved.cid, equals('channel-123')); + }, + ); + + test( + 'should resolve notificationMessageNew event with sharedLocation to locationShared', + () { + final location = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-123', + ); + + final message = Message( + id: 'message-123', + text: 'Check out this location', + sharedLocation: location, + user: User(id: 'user-123'), + ); + + final event = Event( + type: EventType.notificationMessageNew, + message: message, + cid: 'channel-123', + ); + + final resolved = locationSharedResolver(event); + + expect(resolved, isNotNull); + expect(resolved!.type, EventType.locationShared); + expect(resolved.message, equals(message)); + }, + ); + + test( + 'should return null for messageNew event without sharedLocation', + () { + final message = Message( + id: 'message-123', + text: 'Just a regular message', + user: User(id: 'user-123'), + ); + + final event = Event( + type: EventType.messageNew, + message: message, + cid: 'channel-123', + ); + + final resolved = locationSharedResolver(event); + + expect(resolved, isNull); + }, + ); + + test('should return null for invalid event types', () { + final location = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-123', + ); + + final message = Message( + id: 'message-123', + text: 'Check out this location', + sharedLocation: location, + user: User(id: 'user-123'), + ); + + final event = Event( + type: EventType.messageUpdated, + message: message, + cid: 'channel-123', + ); + + final resolved = locationSharedResolver(event); + + expect(resolved, isNull); + }); + + test('should return null for event with null message', () { + final event = Event( + type: EventType.messageNew, + message: null, + cid: 'channel-123', + ); + + final resolved = locationSharedResolver(event); + + expect(resolved, isNull); + }); + }); + + group('locationUpdatedResolver', () { + test( + 'should resolve messageUpdated event with active live location to locationUpdated', + () { + final location = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-123', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final message = Message( + id: 'message-123', + text: 'Live location sharing', + sharedLocation: location, + user: User(id: 'user-123'), + ); + + final event = Event( + type: EventType.messageUpdated, + message: message, + cid: 'channel-123', + ); + + final resolved = locationUpdatedResolver(event); + + expect(resolved, isNotNull); + expect(resolved!.type, EventType.locationUpdated); + expect(resolved.message, equals(message)); + expect(resolved.cid, equals('channel-123')); + }, + ); + + test( + 'should resolve messageUpdated event with static location to locationUpdated', + () { + final location = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-123', + // No endAt means static location + ); + + final message = Message( + id: 'message-123', + text: 'Static location sharing', + sharedLocation: location, + user: User(id: 'user-123'), + ); + + final event = Event( + type: EventType.messageUpdated, + message: message, + cid: 'channel-123', + ); + + final resolved = locationUpdatedResolver(event); + + expect(resolved, isNotNull); + expect(resolved!.type, EventType.locationUpdated); + expect(resolved.message, equals(message)); + }, + ); + + test( + 'should return null for messageUpdated event with expired live location', + () { + final location = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-123', + endAt: DateTime.now().subtract(const Duration(hours: 1)), + ); + + final message = Message( + id: 'message-123', + text: 'Expired location sharing', + sharedLocation: location, + user: User(id: 'user-123'), + ); + + final event = Event( + type: EventType.messageUpdated, + message: message, + cid: 'channel-123', + ); + + final resolved = locationUpdatedResolver(event); + + expect(resolved, isNull); + }, + ); + + test('should return null for invalid event types', () { + final location = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-123', + ); + + final message = Message( + id: 'message-123', + text: 'Check out this location', + sharedLocation: location, + user: User(id: 'user-123'), + ); + + final event = Event( + type: EventType.messageNew, + message: message, + cid: 'channel-123', + ); + + final resolved = locationUpdatedResolver(event); + + expect(resolved, isNull); + }); + + test('should return null for event with null message', () { + final event = Event( + type: EventType.messageUpdated, + message: null, + cid: 'channel-123', + ); + + final resolved = locationUpdatedResolver(event); + + expect(resolved, isNull); + }); + }); + + group('locationExpiredResolver', () { + test( + 'should resolve messageUpdated event with expired live location to locationExpired', + () { + final location = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-123', + endAt: DateTime.now().subtract(const Duration(hours: 1)), + ); + + final message = Message( + id: 'message-123', + text: 'Expired location sharing', + sharedLocation: location, + user: User(id: 'user-123'), + ); + + final event = Event( + type: EventType.messageUpdated, + message: message, + cid: 'channel-123', + ); + + final resolved = locationExpiredResolver(event); + + expect(resolved, isNotNull); + expect(resolved!.type, EventType.locationExpired); + expect(resolved.message, equals(message)); + expect(resolved.cid, equals('channel-123')); + }, + ); + + test( + 'should return null for messageUpdated event with active live location', + () { + final location = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-123', + endAt: DateTime.now().add(const Duration(hours: 1)), + ); + + final message = Message( + id: 'message-123', + text: 'Active location sharing', + sharedLocation: location, + user: User(id: 'user-123'), + ); + + final event = Event( + type: EventType.messageUpdated, + message: message, + cid: 'channel-123', + ); + + final resolved = locationExpiredResolver(event); + + expect(resolved, isNull); + }, + ); + + test( + 'should return null for messageUpdated event with static location', + () { + final location = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-123', + // No endAt means static location + ); + + final message = Message( + id: 'message-123', + text: 'Static location sharing', + sharedLocation: location, + user: User(id: 'user-123'), + ); + + final event = Event( + type: EventType.messageUpdated, + message: message, + cid: 'channel-123', + ); + + final resolved = locationExpiredResolver(event); + + expect(resolved, isNull); + }, + ); + + test('should return null for invalid event types', () { + final location = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-123', + endAt: DateTime.now().subtract(const Duration(hours: 1)), + ); + + final message = Message( + id: 'message-123', + text: 'Expired location sharing', + sharedLocation: location, + user: User(id: 'user-123'), + ); + + final event = Event( + type: EventType.messageNew, + message: message, + cid: 'channel-123', + ); + + final resolved = locationExpiredResolver(event); + + expect(resolved, isNull); + }); + + test('should return null for event with null message', () { + final event = Event( + type: EventType.messageUpdated, + message: null, + cid: 'channel-123', + ); + + final resolved = locationExpiredResolver(event); + + expect(resolved, isNull); + }); + }); + }); +} diff --git a/packages/stream_chat/test/src/client/retry_queue_test.dart b/packages/stream_chat/test/src/client/retry_queue_test.dart index 1150c40b85..59e2e51f47 100644 --- a/packages/stream_chat/test/src/client/retry_queue_test.dart +++ b/packages/stream_chat/test/src/client/retry_queue_test.dart @@ -46,7 +46,10 @@ void main() { final message = Message( id: 'test-message-id', text: 'Sample message test', - state: MessageState.sendingFailed, + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: false, + ), ); retryQueue.add([message]); expect(() => retryQueue.add([message]), returnsNormally); @@ -58,7 +61,10 @@ void main() { final message = Message( id: 'test-message-id', text: 'Sample message test', - state: MessageState.sendingFailed, + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: false, + ), ); retryQueue.add([message]); expect(retryQueue.hasMessages, isTrue); diff --git a/packages/stream_chat/test/src/core/api/call_api_test.dart b/packages/stream_chat/test/src/core/api/call_api_test.dart deleted file mode 100644 index 60c0c24adb..0000000000 --- a/packages/stream_chat/test/src/core/api/call_api_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/core/api/call_api.dart'; -import 'package:test/test.dart'; - -import '../../mocks.dart'; - -@Deprecated('Will be removed in the next major version') -void main() { - Response successResponse(String path, {Object? data}) => Response( - data: data, - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); - - late final client = MockHttpClient(); - late CallApi callApi; - - setUp(() { - callApi = CallApi(client); - }); - - test('getCallToken should work', () async { - const callId = 'test-call-id'; - const path = '/calls/$callId'; - - when(() => client.post(path, data: {})).thenAnswer( - (_) async => successResponse(path, data: {})); - - final res = await callApi.getCallToken(callId); - - expect(res, isNotNull); - - verify(() => client.post(path, data: any(named: 'data'))).called(1); - verifyNoMoreInteractions(client); - }); - - test('createCall should work', () async { - const callId = 'test-call-id'; - const callType = 'test-call-type'; - const channelType = 'test-channel-type'; - const channelId = 'test-channel-id'; - const path = '/channels/$channelType/$channelId/call'; - - when(() => client.post( - path, - data: { - 'id': callId, - 'type': callType, - }, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); - - final res = await callApi.createCall( - callId: callId, - callType: callType, - channelType: channelType, - channelId: channelId, - ); - - expect(res, isNotNull); - - verify(() => client.post(path, data: any(named: 'data'))).called(1); - verifyNoMoreInteractions(client); - }); -} diff --git a/packages/stream_chat/test/src/core/api/message_api_test.dart b/packages/stream_chat/test/src/core/api/message_api_test.dart index 2fa313f0aa..e0572cb911 100644 --- a/packages/stream_chat/test/src/core/api/message_api_test.dart +++ b/packages/stream_chat/test/src/core/api/message_api_test.dart @@ -254,7 +254,6 @@ void main() { test('sendReaction', () async { const messageId = 'test-message-id'; const reactionType = 'test-reaction-type'; - const extraData = {'test-key': 'test-data'}; const path = '/messages/$messageId/reaction'; @@ -263,21 +262,17 @@ void main() { when(() => client.post( path, - data: { - 'reaction': Map.from(extraData) - ..addAll({'type': reactionType}), + data: jsonEncode({ + 'reaction': reaction.toJson(), + 'skip_push': false, 'enforce_unique': false, - }, + }), )).thenAnswer((_) async => successResponse(path, data: { 'message': message.toJson(), - 'reaction': reaction.toJson(), + 'reaction': {...reaction.toJson(), 'message_id': messageId}, })); - final res = await messageApi.sendReaction( - messageId, - reactionType, - extraData: extraData, - ); + final res = await messageApi.sendReaction(messageId, reaction); expect(res, isNotNull); expect(res.message.id, messageId); @@ -291,7 +286,6 @@ void main() { test('sendReaction with enforceUnique: true', () async { const messageId = 'test-message-id'; const reactionType = 'test-reaction-type'; - const extraData = {'test-key': 'test-data'}; const path = '/messages/$messageId/reaction'; @@ -300,20 +294,19 @@ void main() { when(() => client.post( path, - data: { - 'reaction': Map.from(extraData) - ..addAll({'type': reactionType}), + data: jsonEncode({ + 'reaction': reaction.toJson(), + 'skip_push': false, 'enforce_unique': true, - }, + }), )).thenAnswer((_) async => successResponse(path, data: { 'message': message.toJson(), - 'reaction': reaction.toJson(), + 'reaction': {...reaction.toJson(), 'message_id': messageId}, })); final res = await messageApi.sendReaction( messageId, - reactionType, - extraData: extraData, + reaction, enforceUnique: true, ); @@ -363,7 +356,11 @@ void main() { ...const PaginationParams().toJson(), }, )).thenAnswer((_) async => successResponse(path, data: { - 'reactions': [...reactions.map((it) => it.toJson())] + 'reactions': [ + ...reactions.map( + (it) => {...it.toJson(), 'message_id': messageId}, + ), + ] })); final res = await messageApi.getReactions(messageId, pagination: options); diff --git a/packages/stream_chat/test/src/core/api/responses_test.dart b/packages/stream_chat/test/src/core/api/responses_test.dart index 4f9d45c740..5e3430f38c 100644 --- a/packages/stream_chat/test/src/core/api/responses_test.dart +++ b/packages/stream_chat/test/src/core/api/responses_test.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:stream_chat/src/core/models/call_payload.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; @@ -4364,37 +4363,6 @@ void main() { expect(response.message, isA()); }); - test('CallTokenPayload', () { - const jsonExample = ''' - {"duration": "3ms", - "agora_app_id":"test", - "agora_uid": 12, - "token": "token"} - '''; - - // ignore: deprecated_member_use_from_same_package - final response = CallTokenPayload.fromJson(json.decode(jsonExample)); - expect(response.agoraAppId, isA()); - expect(response.agoraUid, isA()); - expect(response.token, isA()); - }, skip: 'Deprecated, Will be removed in the next major version'); - - test('CreateCallPayload', () { - const jsonExample = ''' - {"call": - {"id":"test", - "provider": "test", - "agora": {"channel":"test"}, - "hms":{"room_id":"test", "room_name":"test"} - }} - '''; - - // ignore: deprecated_member_use_from_same_package - final response = CreateCallPayload.fromJson(json.decode(jsonExample)); - // ignore: deprecated_member_use_from_same_package - expect(response.call, isA()); - }, skip: 'Deprecated, Will be removed in the next major version'); - test('UserBlockResponse', () { const jsonExample = ''' { diff --git a/packages/stream_chat/test/src/core/api/sort_order_test.dart b/packages/stream_chat/test/src/core/api/sort_order_test.dart index ac1200621d..6dc5d91473 100644 --- a/packages/stream_chat/test/src/core/api/sort_order_test.dart +++ b/packages/stream_chat/test/src/core/api/sort_order_test.dart @@ -64,13 +64,6 @@ void main() { expect(option.field, 'age'); expect(option.direction, SortOption.DESC); }); - - test('should correctly deserialize from JSON', () { - final json = {'field': 'age', 'direction': 1}; - final option = SortOption.fromJson(json); - expect(option.field, 'age'); - expect(option.direction, SortOption.ASC); - }); }); group('SortOption single field', () { diff --git a/packages/stream_chat/test/src/core/api/user_api_test.dart b/packages/stream_chat/test/src/core/api/user_api_test.dart index 97eda70148..9a77907e64 100644 --- a/packages/stream_chat/test/src/core/api/user_api_test.dart +++ b/packages/stream_chat/test/src/core/api/user_api_test.dart @@ -246,4 +246,80 @@ void main() { verify(() => client.get(path)).called(1); verifyNoMoreInteractions(client); }); + + test('getActiveLiveLocations', () async { + const path = '/users/live_locations'; + + when(() => client.get(path)).thenAnswer( + (_) async => successResponse( + path, + data: {'active_live_locations': []}, + ), + ); + + final res = await userApi.getActiveLiveLocations(); + + expect(res, isNotNull); + + verify(() => client.get(path)).called(1); + verifyNoMoreInteractions(client); + }); + + test('updateLiveLocation', () async { + const path = '/users/live_locations'; + const messageId = 'test-message-id'; + const createdByDeviceId = 'test-device-id'; + final endAt = DateTime.timestamp().add(const Duration(hours: 1)); + const coordinates = LocationCoordinates( + latitude: 40.7128, + longitude: -74.0060, + ); + + when( + () => client.put( + path, + data: json.encode({ + 'message_id': messageId, + 'created_by_device_id': createdByDeviceId, + 'latitude': coordinates.latitude, + 'longitude': coordinates.longitude, + 'end_at': endAt.toIso8601String(), + }), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'message_id': messageId, + 'created_by_device_id': createdByDeviceId, + 'latitude': coordinates.latitude, + 'longitude': coordinates.longitude, + 'end_at': endAt.toIso8601String(), + }, + ), + ); + + final res = await userApi.updateLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + location: coordinates, + endAt: endAt, + ); + + expect(res, isNotNull); + + verify( + () => client.put( + path, + data: json.encode({ + 'message_id': messageId, + 'created_by_device_id': createdByDeviceId, + 'latitude': coordinates.latitude, + 'longitude': coordinates.longitude, + 'end_at': endAt.toIso8601String(), + }), + ), + ).called(1); + verifyNoMoreInteractions(client); + }); } diff --git a/packages/stream_chat/test/src/core/models/call_payload_test.dart b/packages/stream_chat/test/src/core/models/call_payload_test.dart deleted file mode 100644 index c369122e91..0000000000 --- a/packages/stream_chat/test/src/core/models/call_payload_test.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:convert'; - -import 'package:stream_chat/src/core/models/call_payload.dart'; -import 'package:test/test.dart'; - -@Deprecated('Will be removed in the next major version') -void main() { - test('CallPayload', () { - const jsonExample = ''' - {"id":"test", - "provider": "test", - "agora": {"channel":"test"}, - "hms":{"room_id":"test", "room_name":"test"} - } - '''; - final response = CallPayload.fromJson(json.decode(jsonExample)); - expect(response.agora, isA()); - expect(response.hms, isA()); - expect(response.id, isA()); - expect(response.provider, isA()); - }); - - test('AgoraPayload', () { - const jsonExample = ''' - {"channel":"test"} - '''; - final response = AgoraPayload.fromJson(json.decode(jsonExample)); - expect(response.channel, isA()); - }); - - test('HMSPayload', () { - const jsonExample = ''' - {"room_id":"test", "room_name":"test"} - '''; - final response = HMSPayload.fromJson(json.decode(jsonExample)); - expect(response.roomId, isA()); - expect(response.roomName, isA()); - }); -} diff --git a/packages/stream_chat/test/src/core/models/channel_state_test.dart b/packages/stream_chat/test/src/core/models/channel_state_test.dart index e822e0ed1c..8d7410fcbd 100644 --- a/packages/stream_chat/test/src/core/models/channel_state_test.dart +++ b/packages/stream_chat/test/src/core/models/channel_state_test.dart @@ -63,6 +63,7 @@ void main() { chatLevel: ChatLevel.all, disabledUntil: DateTime.parse('2020-01-30T13:43:41.062362Z'), ), + activeLiveLocations: [], ); expect( diff --git a/packages/stream_chat/test/src/core/models/location_test.dart b/packages/stream_chat/test/src/core/models/location_test.dart new file mode 100644 index 0000000000..2ddcfbaf5d --- /dev/null +++ b/packages/stream_chat/test/src/core/models/location_test.dart @@ -0,0 +1,200 @@ +import 'package:stream_chat/src/core/models/channel_model.dart'; +import 'package:stream_chat/src/core/models/location.dart'; +import 'package:stream_chat/src/core/models/location_coordinates.dart'; +import 'package:stream_chat/src/core/models/message.dart'; +import 'package:test/test.dart'; + +void main() { + group('Location', () { + const latitude = 37.7749; + const longitude = -122.4194; + const createdByDeviceId = 'device_123'; + + final location = Location( + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + ); + + test('should create a valid instance with minimal parameters', () { + expect(location.latitude, equals(latitude)); + expect(location.longitude, equals(longitude)); + expect(location.createdByDeviceId, equals(createdByDeviceId)); + expect(location.endAt, isNull); + expect(location.channelCid, isNull); + expect(location.channel, isNull); + expect(location.messageId, isNull); + expect(location.message, isNull); + expect(location.userId, isNull); + expect(location.createdAt, isA()); + expect(location.updatedAt, isA()); + }); + + test('should create a valid instance with all parameters', () { + final createdAt = DateTime.parse('2023-01-01T00:00:00.000Z'); + final updatedAt = DateTime.parse('2023-01-01T01:00:00.000Z'); + final endAt = DateTime.parse('2024-12-31T23:59:59.999Z'); + final channel = ChannelModel( + cid: 'test:channel', + id: 'channel', + type: 'test', + createdAt: createdAt, + updatedAt: updatedAt, + ); + final message = Message( + id: 'message_123', + text: 'Test message', + createdAt: createdAt, + updatedAt: updatedAt, + ); + + final fullLocation = Location( + channelCid: 'test:channel', + channel: channel, + messageId: 'message_123', + message: message, + userId: 'user_123', + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: endAt, + createdAt: createdAt, + updatedAt: updatedAt, + ); + + expect(fullLocation.channelCid, equals('test:channel')); + expect(fullLocation.channel, equals(channel)); + expect(fullLocation.messageId, equals('message_123')); + expect(fullLocation.message, equals(message)); + expect(fullLocation.userId, equals('user_123')); + expect(fullLocation.latitude, equals(latitude)); + expect(fullLocation.longitude, equals(longitude)); + expect(fullLocation.createdByDeviceId, equals(createdByDeviceId)); + expect(fullLocation.endAt, equals(endAt)); + expect(fullLocation.createdAt, equals(createdAt)); + expect(fullLocation.updatedAt, equals(updatedAt)); + }); + + test('should correctly serialize to JSON', () { + final json = location.toJson(); + + expect(json['latitude'], equals(latitude)); + expect(json['longitude'], equals(longitude)); + expect(json['created_by_device_id'], equals(createdByDeviceId)); + expect(json['end_at'], isNull); + expect(json.containsKey('channel_cid'), isFalse); + expect(json.containsKey('channel'), isFalse); + expect(json.containsKey('message_id'), isFalse); + expect(json.containsKey('message'), isFalse); + expect(json.containsKey('user_id'), isFalse); + expect(json.containsKey('created_at'), isFalse); + expect(json.containsKey('updated_at'), isFalse); + }); + + test('should serialize live location with endAt correctly', () { + final endAt = DateTime.parse('2024-12-31T23:59:59.999Z'); + final liveLocation = Location( + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: endAt, + ); + + final json = liveLocation.toJson(); + + expect(json['latitude'], equals(latitude)); + expect(json['longitude'], equals(longitude)); + expect(json['created_by_device_id'], equals(createdByDeviceId)); + expect(json['end_at'], equals('2024-12-31T23:59:59.999Z')); + }); + + test('should return correct coordinates', () { + final coordinates = location.coordinates; + + expect(coordinates, isA()); + expect(coordinates.latitude, equals(latitude)); + expect(coordinates.longitude, equals(longitude)); + }); + + test('isActive should return true for active live location', () { + final futureDate = DateTime.now().add(const Duration(hours: 1)); + final activeLocation = Location( + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: futureDate, + ); + + expect(activeLocation.isActive, isTrue); + expect(activeLocation.isExpired, isFalse); + }); + + test('isActive should return false for expired live location', () { + final pastDate = DateTime.now().subtract(const Duration(hours: 1)); + final expiredLocation = Location( + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: pastDate, + ); + + expect(expiredLocation.isActive, isFalse); + expect(expiredLocation.isExpired, isTrue); + }); + + test('isLive should return true for live location', () { + final futureDate = DateTime.now().add(const Duration(hours: 1)); + final liveLocation = Location( + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: futureDate, + ); + + expect(liveLocation.isLive, isTrue); + expect(liveLocation.isStatic, isFalse); + }); + + test('equality should work correctly', () { + final location1 = Location( + channelCid: 'test:channel', + messageId: 'message_123', + userId: 'user_123', + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: DateTime.parse('2024-12-31T23:59:59.999Z'), + createdAt: DateTime.parse('2023-01-01T00:00:00.000Z'), + updatedAt: DateTime.parse('2023-01-01T00:00:00.000Z'), + ); + + final location2 = Location( + channelCid: 'test:channel', + messageId: 'message_123', + userId: 'user_123', + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: DateTime.parse('2024-12-31T23:59:59.999Z'), + createdAt: DateTime.parse('2023-01-01T00:00:00.000Z'), + updatedAt: DateTime.parse('2023-01-01T00:00:00.000Z'), + ); + + final location3 = Location( + channelCid: 'test:channel', + messageId: 'message_123', + userId: 'user_123', + latitude: 40.7128, // Different latitude + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: DateTime.parse('2024-12-31T23:59:59.999Z'), + createdAt: DateTime.parse('2023-01-01T00:00:00.000Z'), + updatedAt: DateTime.parse('2023-01-01T00:00:00.000Z'), + ); + + expect(location1, equals(location2)); + expect(location1.hashCode, equals(location2.hashCode)); + expect(location1, isNot(equals(location3))); + }); + }); +} diff --git a/packages/stream_chat/test/src/core/models/message_state_test.dart b/packages/stream_chat/test/src/core/models/message_state_test.dart index 9bdb250af9..4d5ac13a31 100644 --- a/packages/stream_chat/test/src/core/models/message_state_test.dart +++ b/packages/stream_chat/test/src/core/models/message_state_test.dart @@ -270,7 +270,10 @@ void main() { test( 'MessageState.sendingFailed should create a MessageFailed instance with SendingFailed state', () { - const messageState = MessageState.sendingFailed; + final messageState = MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: false, + ); expect(messageState, isA()); expect((messageState as MessageFailed).state, isA()); }, @@ -279,12 +282,29 @@ void main() { test( 'MessageState.updatingFailed should create a MessageFailed instance with UpdatingFailed state', () { - const messageState = MessageState.updatingFailed; + final messageState = MessageState.updatingFailed( + skipPush: false, + skipEnrichUrl: false, + ); expect(messageState, isA()); expect((messageState as MessageFailed).state, isA()); }, ); + test( + 'MessageState.partialUpdatingFailed should create a MessageFailed instance with UpdatingFailed state', + () { + final messageState = MessageState.partialUpdatingFailed( + skipEnrichUrl: false, + ); + expect(messageState, isA()); + expect( + (messageState as MessageFailed).state, + isA(), + ); + }, + ); + test( 'MessageState.softDeletingFailed should create a MessageFailed instance with DeletingFailed state and not hard deleting', () { diff --git a/packages/stream_chat/test/src/core/models/message_test.dart b/packages/stream_chat/test/src/core/models/message_test.dart index 8365d7fdce..0e27a2d561 100644 --- a/packages/stream_chat/test/src/core/models/message_test.dart +++ b/packages/stream_chat/test/src/core/models/message_test.dart @@ -23,11 +23,9 @@ void main() { expect(message.attachments, isA>()); expect(message.latestReactions, isA>()); expect(message.ownReactions, isA>()); - // ignore: deprecated_member_use_from_same_package - expect(message.reactionCounts, {'love': 1}); - // ignore: deprecated_member_use_from_same_package - expect(message.reactionScores, {'love': 1}); expect(message.reactionGroups, isA>()); + expect(message.reactionGroups?['love']?.count, 1); + expect(message.reactionGroups?['love']?.sumScores, 1); expect(message.createdAt, DateTime.parse('2020-01-28T22:17:31.107978Z')); expect(message.updatedAt, DateTime.parse('2020-01-28T22:17:31.130506Z')); expect(message.mentionedUsers, isA>()); @@ -289,13 +287,23 @@ void main() { }); test( - 'is derived from reactionCounts and reactionScores if not provided directly in constructor', + 'uses reactionGroups when provided directly in constructor', () { final message = Message( - // ignore: deprecated_member_use_from_same_package - reactionCounts: const {'like': 1, 'love': 2}, - // ignore: deprecated_member_use_from_same_package - reactionScores: const {'like': 1, 'love': 5}, + reactionGroups: { + 'like': ReactionGroup( + count: 1, + sumScores: 1, + firstReactionAt: DateTime.now(), + lastReactionAt: DateTime.now(), + ), + 'love': ReactionGroup( + count: 2, + sumScores: 5, + firstReactionAt: DateTime.now(), + lastReactionAt: DateTime.now(), + ), + }, ); expect(message.reactionGroups, isNotNull); diff --git a/packages/stream_chat/test/src/core/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart index 285d8a4614..6abd278d1e 100644 --- a/packages/stream_chat/test/src/core/models/reaction_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_test.dart @@ -10,6 +10,7 @@ void main() { final reaction = Reaction.fromJson(jsonFixture('reaction.json')); expect(reaction.messageId, '76cd8c82-b557-4e48-9d12-87995d3a0e04'); expect(reaction.createdAt, DateTime.parse('2020-01-28T22:17:31.108742Z')); + expect(reaction.updatedAt, DateTime.parse('2020-01-28T22:17:31.108742Z')); expect(reaction.type, 'wow'); expect( reaction.user?.toJson(), @@ -21,13 +22,14 @@ void main() { ); expect(reaction.score, 1); expect(reaction.userId, '2de0297c-f3f2-489d-b930-ef77342edccf'); - expect(reaction.extraData, {'updated_at': '2020-01-28T22:17:31.108742Z'}); + expect(reaction.emojiCode, '๐Ÿ˜ฎ'); }); test('should serialize to json correctly', () { final reaction = Reaction( messageId: '76cd8c82-b557-4e48-9d12-87995d3a0e04', createdAt: DateTime.parse('2020-01-28T22:17:31.108742Z'), + updatedAt: DateTime.parse('2020-01-28T22:17:31.108742Z'), type: 'wow', user: User( id: '2de0297c-f3f2-489d-b930-ef77342edccf', @@ -35,16 +37,16 @@ void main() { name: 'Daisy Morgan', ), userId: '2de0297c-f3f2-489d-b930-ef77342edccf', - extraData: {'bananas': 'yes'}, - score: 1, + extraData: const {'bananas': 'yes'}, + emojiCode: '๐Ÿ˜ฎ', ); expect( reaction.toJson(), { - 'message_id': '76cd8c82-b557-4e48-9d12-87995d3a0e04', 'type': 'wow', 'score': 1, + 'emoji_code': '๐Ÿ˜ฎ', 'bananas': 'yes', }, ); @@ -56,6 +58,8 @@ void main() { expect(newReaction.messageId, '76cd8c82-b557-4e48-9d12-87995d3a0e04'); expect( newReaction.createdAt, DateTime.parse('2020-01-28T22:17:31.108742Z')); + expect( + newReaction.updatedAt, DateTime.parse('2020-01-28T22:17:31.108742Z')); expect(newReaction.type, 'wow'); expect( newReaction.user?.toJson(), @@ -67,14 +71,15 @@ void main() { ); expect(newReaction.score, 1); expect(newReaction.userId, '2de0297c-f3f2-489d-b930-ef77342edccf'); - expect( - newReaction.extraData, {'updated_at': '2020-01-28T22:17:31.108742Z'}); + expect(newReaction.emojiCode, '๐Ÿ˜ฎ'); final newUserCreateTime = DateTime.now(); newReaction = reaction.copyWith( type: 'lol', + emojiCode: '๐Ÿ˜‚', createdAt: DateTime.parse('2021-01-28T22:17:31.108742Z'), + updatedAt: DateTime.parse('2021-01-28T22:17:31.108742Z'), extraData: {}, messageId: 'test', score: 2, @@ -87,10 +92,15 @@ void main() { ); expect(newReaction.type, 'lol'); + expect(newReaction.emojiCode, '๐Ÿ˜‚'); expect( newReaction.createdAt, DateTime.parse('2021-01-28T22:17:31.108742Z'), ); + expect( + newReaction.updatedAt, + DateTime.parse('2021-01-28T22:17:31.108742Z'), + ); expect(newReaction.extraData, {}); expect(newReaction.messageId, 'test'); expect(newReaction.score, 2); @@ -112,8 +122,9 @@ void main() { final newReaction = reaction.merge( Reaction( type: 'lol', + emojiCode: '๐Ÿ˜‚', createdAt: DateTime.parse('2021-01-28T22:17:31.108742Z'), - extraData: {}, + updatedAt: DateTime.parse('2021-01-28T22:17:31.108742Z'), messageId: 'test', score: 2, user: User( @@ -126,10 +137,15 @@ void main() { ); expect(newReaction.type, 'lol'); + expect(newReaction.emojiCode, '๐Ÿ˜‚'); expect( newReaction.createdAt, DateTime.parse('2021-01-28T22:17:31.108742Z'), ); + expect( + newReaction.updatedAt, + DateTime.parse('2021-01-28T22:17:31.108742Z'), + ); expect(newReaction.extraData, {}); expect(newReaction.messageId, 'test'); expect(newReaction.score, 2); diff --git a/packages/stream_chat/test/src/core/util/event_controller_test.dart b/packages/stream_chat/test/src/core/util/event_controller_test.dart new file mode 100644 index 0000000000..a789d2826b --- /dev/null +++ b/packages/stream_chat/test/src/core/util/event_controller_test.dart @@ -0,0 +1,337 @@ +// ignore_for_file: cascade_invocations, avoid_redundant_argument_values + +import 'dart:async'; +import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/util/event_controller.dart'; +import 'package:stream_chat/src/event_type.dart'; +import 'package:test/test.dart'; + +void main() { + group('EventController events', () { + late EventController controller; + + setUp(() { + controller = EventController(); + }); + + tearDown(() { + controller.close(); + }); + + test('should emit events without resolvers', () async { + final event = Event(type: EventType.messageNew); + + final streamEvents = []; + controller.listen(streamEvents.add); + + controller.add(event); + + await Future.delayed(Duration.zero); + + expect(streamEvents, hasLength(1)); + expect(streamEvents.first.type, EventType.messageNew); + }); + + test('should apply resolvers in order', () async { + Event? firstResolver(Event event) { + if (event.type == EventType.messageNew) { + return event.copyWith(type: EventType.pollCreated); + } + return null; + } + + Event? secondResolver(Event event) { + if (event.type == EventType.pollCreated) { + return event.copyWith(type: EventType.locationShared); + } + return null; + } + + controller = EventController( + resolvers: [firstResolver, secondResolver], + ); + + final streamEvents = []; + controller.listen(streamEvents.add); + + final originalEvent = Event(type: EventType.messageNew); + controller.add(originalEvent); + + await Future.delayed(Duration.zero); + + expect(streamEvents, hasLength(1)); + expect(streamEvents.first.type, EventType.pollCreated); + }); + + test('should stop at first matching resolver', () async { + var firstResolverCalled = false; + var secondResolverCalled = false; + + Event? firstResolver(Event event) { + firstResolverCalled = true; + if (event.type == EventType.messageNew) { + return event.copyWith(type: EventType.pollCreated); + } + return null; + } + + Event? secondResolver(Event event) { + secondResolverCalled = true; + return event.copyWith(type: EventType.locationShared); + } + + controller = EventController( + resolvers: [firstResolver, secondResolver], + ); + + final streamEvents = []; + controller.listen(streamEvents.add); + + final originalEvent = Event(type: EventType.messageNew); + controller.add(originalEvent); + + await Future.delayed(Duration.zero); + + expect(firstResolverCalled, isTrue); + expect(secondResolverCalled, isFalse); + expect(streamEvents, hasLength(1)); + expect(streamEvents.first.type, EventType.pollCreated); + }); + + test('should emit original event when no resolver matches', () async { + Event? resolver(Event event) { + if (event.type == EventType.pollCreated) { + return event.copyWith(type: EventType.locationShared); + } + return null; + } + + controller = EventController( + resolvers: [resolver], + ); + + final streamEvents = []; + controller.listen(streamEvents.add); + + final originalEvent = Event(type: EventType.messageNew); + controller.add(originalEvent); + + await Future.delayed(Duration.zero); + + expect(streamEvents, hasLength(1)); + expect(streamEvents.first.type, EventType.messageNew); + }); + + test('should work with multiple resolvers that return null', () async { + Event? firstResolver(Event event) => null; + Event? secondResolver(Event event) => null; + Event? thirdResolver(Event event) => null; + + controller = EventController( + resolvers: [firstResolver, secondResolver, thirdResolver], + ); + + final streamEvents = []; + controller.listen(streamEvents.add); + + final originalEvent = Event(type: EventType.messageNew); + controller.add(originalEvent); + + await Future.delayed(Duration.zero); + + expect(streamEvents, hasLength(1)); + expect(streamEvents.first.type, EventType.messageNew); + }); + + test('should handle empty resolvers list', () async { + controller = EventController(resolvers: []); + + final streamEvents = []; + controller.listen(streamEvents.add); + + final originalEvent = Event(type: EventType.messageNew); + controller.add(originalEvent); + + await Future.delayed(Duration.zero); + + expect(streamEvents, hasLength(1)); + expect(streamEvents.first.type, EventType.messageNew); + }); + + test('should support custom onListen callback', () async { + var onListenCalled = false; + + controller = EventController( + onListen: () => onListenCalled = true, + ); + + expect(onListenCalled, isFalse); + + controller.listen((_) {}); + + expect(onListenCalled, isTrue); + }); + + test('should support custom onCancel callback', () async { + var onCancelCalled = false; + + controller = EventController( + onCancel: () => onCancelCalled = true, + ); + + final subscription = controller.listen((_) {}); + + expect(onCancelCalled, isFalse); + + await subscription.cancel(); + + expect(onCancelCalled, isTrue); + }); + + test('should support sync mode', () async { + controller = EventController(sync: true); + + final streamEvents = []; + controller.listen(streamEvents.add); + + final originalEvent = Event(type: EventType.messageNew); + controller.add(originalEvent); + + // In sync mode, events should be available immediately + expect(streamEvents, hasLength(1)); + expect(streamEvents.first.type, EventType.messageNew); + }); + + test('should handle resolver exceptions gracefully', () async { + Event? failingResolver(Event event) { + throw Exception('Resolver failed'); + } + + Event? workingResolver(Event event) { + return event.copyWith(type: EventType.pollCreated); + } + + controller = EventController( + resolvers: [failingResolver, workingResolver], + ); + + final streamEvents = []; + controller.listen(streamEvents.add); + + final originalEvent = Event(type: EventType.messageNew); + + // This should throw an exception because the resolver throws + expect(() => controller.add(originalEvent), throwsException); + }); + + test('should be compatible with stream operations', () async { + final event1 = Event(type: EventType.messageNew); + final event2 = Event(type: EventType.messageUpdated); + + final streamEvents = []; + controller + .where((event) => event.type == EventType.messageNew) + .listen(streamEvents.add); + + controller.add(event1); + controller.add(event2); + + await Future.delayed(Duration.zero); + + expect(streamEvents, hasLength(1)); + expect(streamEvents.first.type, EventType.messageNew); + }); + + test('should work with multiple listeners', () async { + final streamEvents1 = []; + final streamEvents2 = []; + + controller.listen(streamEvents1.add); + controller.listen(streamEvents2.add); + + final originalEvent = Event(type: EventType.messageNew); + controller.add(originalEvent); + + await Future.delayed(Duration.zero); + + expect(streamEvents1, hasLength(1)); + expect(streamEvents2, hasLength(1)); + expect(streamEvents1.first.type, EventType.messageNew); + expect(streamEvents2.first.type, EventType.messageNew); + }); + + test('should preserve event properties through resolvers', () async { + final originalEvent = Event( + type: EventType.messageNew, + userId: 'user123', + cid: 'channel123', + connectionId: 'conn123', + me: null, + user: null, + extraData: {'custom': 'data'}, + ); + + Event? resolver(Event event) { + if (event.type == EventType.messageNew) { + return event.copyWith(type: EventType.pollCreated); + } + return null; + } + + controller = EventController( + resolvers: [resolver], + ); + + final streamEvents = []; + controller.listen(streamEvents.add); + + controller.add(originalEvent); + + await Future.delayed(Duration.zero); + + expect(streamEvents, hasLength(1)); + final resolvedEvent = streamEvents.first; + expect(resolvedEvent.type, EventType.pollCreated); + expect(resolvedEvent.userId, 'user123'); + expect(resolvedEvent.cid, 'channel123'); + expect(resolvedEvent.connectionId, 'conn123'); + expect(resolvedEvent.extraData, {'custom': 'data'}); + }); + + test('should handle resolver modifying event data', () async { + final originalEvent = Event( + type: EventType.messageNew, + userId: 'user123', + extraData: {'original': 'data'}, + ); + + Event? resolver(Event event) { + if (event.type == EventType.messageNew) { + return event.copyWith( + type: EventType.pollCreated, + userId: 'modified_user', + extraData: {'modified': 'data'}, + ); + } + return null; + } + + controller = EventController( + resolvers: [resolver], + ); + + final streamEvents = []; + controller.listen(streamEvents.add); + + controller.add(originalEvent); + + await Future.delayed(Duration.zero); + + expect(streamEvents, hasLength(1)); + final resolvedEvent = streamEvents.first; + expect(resolvedEvent.type, EventType.pollCreated); + expect(resolvedEvent.userId, 'modified_user'); + expect(resolvedEvent.extraData, {'modified': 'data'}); + }); + }); +} diff --git a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart index 3ebdfc32e4..7091becba6 100644 --- a/packages/stream_chat/test/src/db/chat_persistence_client_test.dart +++ b/packages/stream_chat/test/src/db/chat_persistence_client_test.dart @@ -6,6 +6,7 @@ import 'package:stream_chat/src/core/models/draft.dart'; import 'package:stream_chat/src/core/models/draft_message.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:stream_chat/src/core/models/location.dart'; import 'package:stream_chat/src/core/models/member.dart'; import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/poll.dart'; @@ -176,6 +177,22 @@ class TestPersistenceClient extends ChatPersistenceClient { @override Future updateDraftMessages(List draftMessages) => Future.value(); + + @override + Future> getLocationsByCid(String cid) async => []; + + @override + Future getLocationByMessageId(String messageId) async => null; + + @override + Future updateLocations(List locations) => Future.value(); + + @override + Future deleteLocationsByCid(String cid) => Future.value(); + + @override + Future deleteLocationsByMessageIds(List messageIds) => + Future.value(); } void main() { diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index 26e000e13e..00aa1e0939 100644 --- a/packages/stream_chat/test/src/mocks.dart +++ b/packages/stream_chat/test/src/mocks.dart @@ -1,7 +1,6 @@ import 'package:dio/dio.dart'; import 'package:logging/logging.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/client/channel.dart'; import 'package:stream_chat/src/client/client.dart'; import 'package:stream_chat/src/core/api/attachment_file_uploader.dart'; @@ -18,6 +17,7 @@ import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/models/channel_config.dart'; import 'package:stream_chat/src/core/models/event.dart'; +import 'package:stream_chat/src/core/util/event_controller.dart'; import 'package:stream_chat/src/db/chat_persistence_client.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/ws/websocket.dart'; @@ -98,7 +98,7 @@ class MockStreamChatClient extends Mock implements StreamChatClient { @override Stream get eventStream => _eventController.stream; - final _eventController = PublishSubject(); + final _eventController = EventController(); void addEvent(Event event) => _eventController.add(event); @override diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 51e960e342..c6daa911fb 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## 10.0.0-beta.5 + +- Included the changes from version [`9.16.0`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 9.16.0 ๐Ÿž Fixed @@ -9,7 +13,17 @@ โœ… Added -- Added `padding` and `textInputMargin` to `StreamMessageInput` to allow fine-tuning the layout. +- Added `padding` and `textInputMargin` to `StreamMessageInput` to allow fine-tuning the layout. + +## 10.0.0-beta.4 + +โœ… Added + +- Added `emojiCode` property to `StreamReactionIcon` to support custom emojis in reactions. +- Updated default reaction builders with standard emoji codes. (`โค๏ธ`, `๐Ÿ‘`, `๐Ÿ‘Ž`, `๐Ÿ˜‚`, `๐Ÿ˜ฎ`) +- Added `StreamChatConfiguration.maybeOf()` method for safe context access in async operations. + +- Included the changes from version [`9.15.0`](https://pub.dev/packages/stream_chat_flutter/changelog). ## 9.15.0 @@ -23,6 +37,32 @@ - Fixed `StreamMessageInput` crashes with "Null check operator used on a null value" when async operations continue after widget unmounting. +## 10.0.0-beta.3 + +๐Ÿ›‘๏ธ Breaking + +- **Deprecated API Cleanup**: Removed all deprecated classes, methods, and properties for the v10 major release: + - **Removed Classes**: `DmCheckbox` (use `DmCheckboxListTile`), `StreamIconThemeSvgIcon` (use `StreamSvgIcon`), `StreamVoiceRecordingThemeData` (use `StreamVoiceRecordingAttachmentThemeData`), `StreamVoiceRecordingLoading`, `StreamVoiceRecordingSlider` (use `StreamAudioWaveformSlider`), `StreamVoiceRecordingPlayer` (use `StreamVoiceRecordingAttachment`), `StreamVoiceRecordingListPlayer` (use `StreamVoiceRecordingAttachmentPlaylist`) + - **Removed Properties**: `reactionIcons` and `voiceRecordingTheme` from `StreamChatTheme`, `isThreadConversation` from `FloatingDateDivider`, `idleSendButton` and `activeSendButton` from `StreamMessageInput`, `isCommandEnabled` and `isEditEnabled` from `StreamMessageSendButton`, `assetName`, `width`, and `height` from `StreamSvgIcon` + - **Removed Constructor Parameters**: `useNativeAttachmentPickerOnMobile` from various components, `allowCompression` from `StreamAttachmentHandler.pickFile()` and `StreamFilePicker` (use `compressionQuality` instead), `cid` from `StreamUnreadIndicator` constructor + - **Removed Methods**: `lastUnreadMessage()` from message list extensions (use `StreamChannel.getFirstUnreadMessage`), `loadBuffer()` and `_loadAsync()` from `StreamVideoThumbnailImage` + - **StreamSvgIcon Refactoring**: Removed 80+ deprecated factory constructors. Use `StreamSvgIcon(icon: StreamSvgIcons.iconName)` instead of factory constructors like `StreamSvgIcon.add()` +- `PollMessage` widget has been removed and replaced with `PollAttachment` for better integration with the attachment system. Polls can now be customized through `PollAttachmentBuilder` or by creating custom poll attachment widgets via the attachment builder system. +- `AttachmentPickerType` enum has been replaced with a sealed class to support extensible custom types like contact and location pickers. Use built-in types like `AttachmentPickerType.images` or define your own via `CustomAttachmentPickerType`. +- `StreamAttachmentPickerOption` has been replaced with two sealed classes to support layout-specific picker options: `SystemAttachmentPickerOption` for system pickers (e.g. camera, files) and `TabbedAttachmentPickerOption` for tabbed pickers (e.g. gallery, polls, location). +- `showStreamAttachmentPickerModalBottomSheet` now returns a `StreamAttachmentPickerResult` instead of `AttachmentPickerValue` for improved type safety and clearer intent handling. +- `StreamMobileAttachmentPickerBottomSheet` has been renamed to `StreamTabbedAttachmentPickerBottomSheet`, and `StreamWebOrDesktopAttachmentPickerBottomSheet` has been renamed to `StreamSystemAttachmentPickerBottomSheet` to better reflect their respective layouts. + +For more details, please refer to the [migration guide](../../migrations/v10-migration.md). + +โœ… Added + +- Added `extraData` field to `AttachmentPickerValue` to support storing and retrieving custom picker state (e.g. tab-specific config). +- Added `customAttachmentPickerOptions` to `StreamMessageInput` to allow injecting custom picker tabs like contact and location pickers. +- Added `onCustomAttachmentPickerResult` callback to `StreamMessageInput` to handle results returned by custom picker tabs. + +- Included the changes from version [`9.14.0`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 9.14.0 ๐Ÿž Fixed @@ -30,6 +70,10 @@ - Fixed `StreamMessageInput` tries to expand to full height when used in a unconstrained environment. - Fixed `StreamCommandAutocompleteOptions` to style the command name with `textHighEmphasis` style. +## 10.0.0-beta.2 + +- Included the changes from version [`9.13.0`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 9.13.0 ๐Ÿž Fixed @@ -39,6 +83,38 @@ - Fixed `ScrollToBottom` button always showing when the latest message was too big and exceeded the viewport main axis size. +## 10.0.0-beta.1 + +๐Ÿ›‘๏ธ Breaking + +- `StreamReactionPicker` now requires reactions to be explicitly handled via `onReactionPicked`. *(Automatic handling is no longer supported.)* +- `StreamMessageAction` is now generic `(StreamMessageAction)`, enhancing type safety. Individual onTap callbacks have been removed; actions are now handled centrally by widgets like `StreamMessageWidget.onCustomActionTap` or modals using action types. +- `StreamMessageReactionsModal` no longer requires the `messageTheme` parameter. The theme now automatically derives from the `reverse` property. +- `StreamMessageWidget` no longer requires the `showReactionTail` parameter. The reaction picker tail is now always shown when the reaction picker is visible. + +For more details, please refer to the [migration guide](../../migrations/v10-migration.md). + +โœ… Added + +- Added new `StreamMessageActionsBuilder` which provides a list of actions to be displayed in the message actions modal. +- Added new `StreamMessageActionConfirmationModal` for confirming destructive actions like delete or flag. +- Added new `StreamMessageModal` and `showStreamMessageModal` for consistent message-related modals with improved transitions and backdrop effects. + ```dart + showStreamMessageModal( + context: context, + ...other parameters, + builder: (context) => StreamMessageModal( + ...other parameters, + headerBuilder: (context) => YourCustomHeader(), + contentBuilder: (context) => YourCustomContent(), + ), + ); + ``` +- Added `desktopOrWeb` parameter to `PlatformWidgetBuilder` to allow specifying a single builder for both desktop and web platforms. +- Added `reactionPickerBuilder` to `StreamMessageActionsModal`, `StreamMessageReactionsModal`, and `StreamMessageWidget` to enable custom reaction picker widgets. +- Added `StreamReactionIcon.defaultReactions` providing a predefined list of common reaction icons. +- Exported `StreamMessageActionsModal` and `StreamModeratedMessageActionsModal` which are now based on `StreamMessageModal` for consistent styling and behavior. + ## 9.12.0 โœ… Added diff --git a/packages/stream_chat_flutter/dart_test.yaml b/packages/stream_chat_flutter/dart_test.yaml new file mode 100644 index 0000000000..c329c9c85d --- /dev/null +++ b/packages/stream_chat_flutter/dart_test.yaml @@ -0,0 +1,5 @@ +# The existence of this file prevents warnings about unrecognized tags when running Alchemist tests. + +tags: + golden: + timeout: 15s \ No newline at end of file diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index 3455f2d76c..77cb9f91f0 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -25,9 +25,9 @@ dependencies: flutter: sdk: flutter responsive_builder: ^0.7.0 - stream_chat_flutter: ^9.16.0 - stream_chat_localizations: ^9.16.0 - stream_chat_persistence: ^9.16.0 + stream_chat_flutter: ^10.0.0-beta.5 + stream_chat_localizations: ^10.0.0-beta.5 + stream_chat_persistence: ^10.0.0-beta.5 flutter: uses-material-design: true diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_builder.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_builder.dart index 13b0a13cda..b3950d2f0c 100644 --- a/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_builder.dart +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_builder.dart @@ -25,6 +25,7 @@ class PlatformWidgetBuilder extends StatelessWidget { this.mobile, this.desktop, this.web, + this.desktopOrWeb, }); /// The child widget. @@ -39,12 +40,21 @@ class PlatformWidgetBuilder extends StatelessWidget { /// The widget to build for web platforms. final PlatformTargetBuilder? web; + /// The widget to build for desktop or web platforms. + /// + /// Note: The widget will prefer the [desktop] or [web] widget if a + /// combination of desktop/web and desktopOrWeb is provided. + final PlatformTargetBuilder? desktopOrWeb; + @override Widget build(BuildContext context) { + final webWidget = web ?? desktopOrWeb; + final desktopWidget = desktop ?? desktopOrWeb; + return PlatformWidget( - desktop: (context) => desktop?.call(context, child), + desktop: (context) => desktopWidget?.call(context, child), mobile: (context) => mobile?.call(context, child), - web: (context) => web?.call(context, child), + web: (context) => webWidget?.call(context, child), ); } } diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment.dart index 0b73a93c7c..bbc818e9b5 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment.dart @@ -3,5 +3,8 @@ export 'file_attachment.dart'; export 'gallery_attachment.dart'; export 'giphy_attachment.dart'; export 'image_attachment.dart'; +export 'poll_attachment.dart'; export 'url_attachment.dart'; export 'video_attachment.dart'; +export 'voice_recording_attachment.dart'; +export 'voice_recording_attachment_playlist.dart'; diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget_catalog.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget_catalog.dart index 5e51c7333b..78c0ba1f52 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget_catalog.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget_catalog.dart @@ -37,11 +37,6 @@ class AttachmentWidgetCatalog { Widget build(BuildContext context, Message message) { assert(!message.isDeleted, 'Cannot build attachment for deleted message'); - assert( - message.attachments.isNotEmpty, - 'Cannot build attachment for message without attachments', - ); - // The list of attachments to build the widget for. final attachments = message.attachments.grouped; for (final builder in builders) { diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/attachment_widget_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/attachment_widget_builder.dart index bbdc950ba0..24c1df2a5e 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/attachment_widget_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/attachment_widget_builder.dart @@ -11,7 +11,7 @@ part 'mixed_attachment_builder.dart'; part 'url_attachment_builder.dart'; part 'video_attachment_builder.dart'; part 'voice_recording_attachment_playlist_builder.dart'; -part 'voice_recording_attachment_builder/voice_recording_attachment_builder.dart'; +part 'poll_attachment_builder.dart'; /// {@template streamAttachmentWidgetTapCallback} /// Signature for a function that's called when the user taps on an attachment. @@ -43,6 +43,8 @@ abstract class StreamAttachmentWidgetBuilder { /// * [FileAttachmentBuilder] /// * [ImageAttachmentBuilder] /// * [VideoAttachmentBuilder] + /// * [VoiceRecordingAttachmentPlaylistBuilder] + /// * [PollAttachmentBuilder] /// * [UrlAttachmentBuilder] /// * [FallbackAttachmentBuilder] /// @@ -72,7 +74,14 @@ abstract class StreamAttachmentWidgetBuilder { return [ ...?customAttachmentBuilders, - // Handles a mix of image, gif, video, url and file attachments. + // Handles poll attachments. + PollAttachmentBuilder( + shape: shape, + padding: padding, + ), + + // Handles a mix of image, gif, video, url, file and voice recording + // attachments. MixedAttachmentBuilder( padding: padding, onAttachmentTap: onAttachmentTap, diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/poll_attachment_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/poll_attachment_builder.dart new file mode 100644 index 0000000000..53066b0c64 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/poll_attachment_builder.dart @@ -0,0 +1,55 @@ +part of 'attachment_widget_builder.dart'; + +const _kDefaultPollMessageConstraints = BoxConstraints( + maxWidth: 270, +); + +/// {@template pollAttachmentBuilder} +/// A widget builder for Poll attachment type. +/// +/// This builder is used when a message contains a poll. +/// {@endtemplate} +class PollAttachmentBuilder extends StreamAttachmentWidgetBuilder { + /// {@macro urlAttachmentBuilder} + const PollAttachmentBuilder({ + this.shape, + this.padding = const EdgeInsets.all(8), + this.constraints = _kDefaultPollMessageConstraints, + }); + + /// The shape of the poll attachment. + final ShapeBorder? shape; + + /// The constraints to apply to the poll attachment widget. + final BoxConstraints constraints; + + /// The padding to apply to the poll attachment widget. + final EdgeInsetsGeometry padding; + + @override + bool canHandle( + Message message, + Map> attachments, + ) { + final poll = message.poll; + return poll != null; + } + + @override + Widget build( + BuildContext context, + Message message, + Map> attachments, + ) { + assert(debugAssertCanHandle(message, attachments), ''); + + return Padding( + padding: padding, + child: PollAttachment( + message: message, + shape: shape, + constraints: constraints, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_list_player.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_list_player.dart deleted file mode 100644 index d13e2ec620..0000000000 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_list_player.dart +++ /dev/null @@ -1,139 +0,0 @@ -// coverage:ignore-file - -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:just_audio/just_audio.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template StreamVoiceRecordingListPlayer} -/// Display many audios and displays a list of AudioPlayerMessage. -/// {@endtemplate} -@Deprecated('Use StreamVoiceRecordingAttachmentPlaylist instead') -class StreamVoiceRecordingListPlayer extends StatefulWidget { - /// {@macro StreamVoiceRecordingListPlayer} - const StreamVoiceRecordingListPlayer({ - super.key, - required this.playList, - this.attachmentBorderRadiusGeometry, - this.constraints, - }); - - /// List of audio attachments. - final List playList; - - /// The border radius of each audio. - final BorderRadiusGeometry? attachmentBorderRadiusGeometry; - - /// Constraints of audio attachments - final BoxConstraints? constraints; - - @override - State createState() => - _StreamVoiceRecordingListPlayerState(); -} - -@Deprecated("Use 'StreamVoiceRecordingAttachmentPlaylist' instead") -class _StreamVoiceRecordingListPlayerState - extends State { - final _player = AudioPlayer(); - late StreamSubscription _playerStateChangedSubscription; - - Widget _createAudioPlayer(int index, PlayListItem item) { - final url = item.assetUrl; - Widget child; - - if (url == null) { - child = const StreamVoiceRecordingLoading(); - } else { - child = StreamVoiceRecordingPlayer( - player: _player, - duration: item.duration, - waveBars: item.waveForm, - index: index, - ); - } - - final theme = - StreamChatTheme.of(context).voiceRecordingTheme.listPlayerTheme; - - return Container( - margin: theme.margin, - constraints: widget.constraints, - decoration: BoxDecoration( - color: theme.backgroundColor, - border: Border.all( - color: theme.borderColor!, - ), - borderRadius: - widget.attachmentBorderRadiusGeometry ?? theme.borderRadius, - ), - child: child, - ); - } - - void _playerStateListener(PlayerState state) async { - if (state.processingState == ProcessingState.completed) { - await _player.stop(); - await _player.seek(Duration.zero, index: 0); - } - } - - @override - void initState() { - super.initState(); - - _playerStateChangedSubscription = - _player.playerStateStream.listen(_playerStateListener); - } - - @override - void dispose() { - super.dispose(); - - _playerStateChangedSubscription.cancel(); - _player.dispose(); - } - - @override - Widget build(BuildContext context) { - final playList = widget.playList - .where((attachment) => attachment.assetUrl != null) - .map((attachment) => AudioSource.uri(Uri.parse(attachment.assetUrl!))) - .toList(); - - final audioSource = ConcatenatingAudioSource(children: playList); - - _player - ..setShuffleModeEnabled(false) - ..setLoopMode(LoopMode.off) - ..setAudioSource(audioSource, preload: false); - - return Column( - children: widget.playList.mapIndexed(_createAudioPlayer).toList(), - ); - } -} - -/// {@template PlayListItem} -/// Represents an audio attachment meta data. -/// {@endtemplate} -@Deprecated("Use 'PlaylistTrack' instead") -class PlayListItem { - /// {@macro PlayListItem} - const PlayListItem({ - this.assetUrl, - required this.duration, - required this.waveForm, - }); - - /// The url of the audio. - final String? assetUrl; - - /// The duration of the audio. - final Duration duration; - - /// The wave form of the audio. - final List waveForm; -} diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_loading.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_loading.dart deleted file mode 100644 index 7f26f1b6ff..0000000000 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_loading.dart +++ /dev/null @@ -1,33 +0,0 @@ -// coverage:ignore-file - -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template StreamVoiceRecordingLoading} -/// Loading widget for audio message. Use this when the url from the audio -/// message is still not available. One use situation in when the audio is -/// still being uploaded. -/// {@endtemplate} -@Deprecated('Will be removed in the next major version') -class StreamVoiceRecordingLoading extends StatelessWidget { - /// {@macro StreamVoiceRecordingLoading} - const StreamVoiceRecordingLoading({super.key}); - - @override - Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context).voiceRecordingTheme.loadingTheme; - - return Padding( - padding: theme.padding!, - child: SizedBox( - height: theme.size!.height, - width: theme.size!.width, - child: CircularProgressIndicator( - // ignore: unnecessary_null_checks - strokeWidth: theme.strokeWidth!, - color: theme.color, - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_player.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_player.dart deleted file mode 100644 index 0136fd3f81..0000000000 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_player.dart +++ /dev/null @@ -1,320 +0,0 @@ -// coverage:ignore-file - -import 'dart:async'; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:just_audio/just_audio.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:stream_chat_flutter/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_slider.dart'; -import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template StreamVoiceRecordingPlayer} -/// Embedded player for audio messages. It displays the data for the audio -/// message and allow the user to interact with the player providing buttons -/// to play/pause, seek the audio and change the speed of reproduction. -/// -/// When waveBars are not provided they are shown as 0 bars. -/// {@endtemplate} -@Deprecated("Use 'StreamVoiceRecordingAttachment' instead") -class StreamVoiceRecordingPlayer extends StatefulWidget { - /// {@macro StreamVoiceRecordingPlayer} - const StreamVoiceRecordingPlayer({ - super.key, - required this.player, - required this.duration, - this.waveBars, - this.index = 0, - this.fileSize, - this.actionButton, - }); - - /// The player of the audio. - final AudioPlayer player; - - /// The wave bars of the recorded audio from 0 to 1. When not provided - /// this Widget shows then as small dots. - final List? waveBars; - - /// The duration of the audio. - final Duration duration; - - /// The index of the audio inside the play list. If not provided, this is - /// assumed to be zero. - final int index; - - /// The file size in bits. - final int? fileSize; - - /// An action button to be used. - final Widget? actionButton; - - @override - _StreamVoiceRecordingPlayerState createState() => - _StreamVoiceRecordingPlayerState(); -} - -@Deprecated("Use 'StreamVoiceRecordingAttachment' instead") -class _StreamVoiceRecordingPlayerState - extends State { - var _seeking = false; - - @override - void dispose() { - super.dispose(); - - widget.player.dispose(); - } - - @override - Widget build(BuildContext context) { - if (widget.duration != Duration.zero) { - return _content(widget.duration); - } else { - return StreamBuilder( - stream: widget.player.durationStream, - builder: (context, snapshot) { - if (snapshot.hasData) { - return _content(snapshot.data!); - } else if (snapshot.hasError) { - return const Center(child: Text('Error!!')); - } else { - return const StreamVoiceRecordingLoading(); - } - }, - ); - } - } - - Widget _content(Duration totalDuration) { - return Container( - padding: const EdgeInsets.all(8), - height: 60, - child: Row( - children: [ - SizedBox( - width: 36, - height: 36, - child: _controlButton(), - ), - Padding( - padding: const EdgeInsets.only(left: 8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _timer(totalDuration), - _fileSizeWidget(widget.fileSize), - ], - ), - ), - _audioWaveSlider(totalDuration), - _speedAndActionButton(), - ], - ), - ); - } - - Widget _controlButton() { - final theme = StreamChatTheme.of(context).voiceRecordingTheme.playerTheme; - - return StreamBuilder( - initialData: false, - stream: _playingThisStream(), - builder: (context, snapshot) { - final playingThis = snapshot.data == true; - - final icon = playingThis ? theme.pauseIcon : theme.playIcon; - - final processingState = widget.player.playerStateStream - .map((event) => event.processingState); - - return StreamBuilder( - stream: processingState, - initialData: ProcessingState.idle, - builder: (context, snapshot) { - final state = snapshot.data ?? ProcessingState.idle; - if (state == ProcessingState.ready || - state == ProcessingState.idle || - !playingThis) { - return ElevatedButton( - style: ElevatedButton.styleFrom( - elevation: theme.buttonElevation, - padding: theme.buttonPadding, - backgroundColor: theme.buttonBackgroundColor, - shape: theme.buttonShape, - ), - child: Icon(icon, color: theme.iconColor), - onPressed: () { - if (playingThis) { - _pause(); - } else { - _play(); - } - }, - ); - } else { - return const CircularProgressIndicator(strokeWidth: 3); - } - }, - ); - }, - ); - } - - Widget _speedAndActionButton() { - final theme = StreamChatTheme.of(context).voiceRecordingTheme.playerTheme; - - final speedStream = _playingThisStream().flatMap((showSpeed) => - widget.player.speedStream.map((speed) => showSpeed ? speed : -1.0)); - - return StreamBuilder( - initialData: -1, - stream: speedStream, - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data! > 0) { - final speed = snapshot.data!; - return SizedBox( - width: theme.speedButtonSize!.width, - height: theme.speedButtonSize!.height, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - elevation: theme.speedButtonElevation, - backgroundColor: theme.speedButtonBackgroundColor, - padding: theme.speedButtonPadding, - shape: theme.speedButtonShape, - ), - child: Text( - '${speed}x', - style: theme.speedButtonTextStyle, - ), - onPressed: () { - setState(() { - if (speed == 2) { - widget.player.setSpeed(1); - } else { - widget.player.setSpeed(speed + 0.5); - } - }); - }, - ), - ); - } else { - if (widget.actionButton != null) { - return widget.actionButton!; - } else { - return SizedBox( - width: theme.speedButtonSize!.width, - height: theme.speedButtonSize!.height, - child: theme.fileTypeIcon, - ); - } - } - }, - ); - } - - Widget _fileSizeWidget(int? fileSize) { - final theme = StreamChatTheme.of(context).voiceRecordingTheme.playerTheme; - - if (fileSize != null) { - return Text( - fileSize.toHumanReadableSize(), - style: theme.fileSizeTextStyle, - ); - } else { - return const Empty(); - } - } - - Widget _timer(Duration totalDuration) { - final theme = StreamChatTheme.of(context).voiceRecordingTheme.playerTheme; - - return StreamBuilder( - stream: widget.player.positionStream, - builder: (context, snapshot) { - if (snapshot.hasData && - (widget.player.currentIndex == widget.index && - (widget.player.playing || - snapshot.data!.inMilliseconds > 0 || - _seeking))) { - return Text( - snapshot.data!.toMinutesAndSeconds(), - style: theme.timerTextStyle, - ); - } else { - return Text( - totalDuration.toMinutesAndSeconds(), - style: theme.timerTextStyle, - ); - } - }, - ); - } - - Widget _audioWaveSlider(Duration totalDuration) { - final positionStream = widget.player.currentIndexStream.flatMap( - (index) => widget.player.positionStream.map((duration) => _sliderValue( - duration, - totalDuration, - index, - )), - ); - - return Expanded( - child: StreamVoiceRecordingSlider( - waves: widget.waveBars ?? List.filled(50, 0), - progressStream: positionStream, - onChangeStart: (val) { - setState(() { - _seeking = true; - }); - }, - onChanged: (val) { - widget.player.pause(); - widget.player.seek( - totalDuration * val, - index: widget.index, - ); - }, - onChangeEnd: () { - setState(() { - _seeking = false; - }); - }, - ), - ); - } - - double _sliderValue( - Duration duration, - Duration totalDuration, - int? currentIndex, - ) { - if (widget.index != currentIndex) { - return 0; - } else { - return min(duration.inMicroseconds / totalDuration.inMicroseconds, 1); - } - } - - Stream _playingThisStream() { - return widget.player.playingStream.flatMap((playing) { - return widget.player.currentIndexStream.map( - (index) => playing && index == widget.index, - ); - }); - } - - Future _play() async { - if (widget.index != widget.player.currentIndex) { - widget.player.seek(Duration.zero, index: widget.index); - } - - widget.player.play(); - } - - Future _pause() { - return widget.player.pause(); - } -} diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_slider.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_slider.dart deleted file mode 100644 index a3ed7ddbbb..0000000000 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_slider.dart +++ /dev/null @@ -1,239 +0,0 @@ -// coverage:ignore-file - -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template StreamVoiceRecordingSlider} -/// A Widget that draws the audio wave bars for an audio inside a Slider. -/// This Widget is indeed to be used to control the position of an audio message -/// and to get feedback of the position. -/// {@endtemplate} -@Deprecated("Use 'StreamAudioWaveformSlider' instead") -class StreamVoiceRecordingSlider extends StatefulWidget { - /// {@macro StreamVoiceRecordingSlider} - const StreamVoiceRecordingSlider({ - super.key, - required this.waves, - required this.progressStream, - this.onChangeStart, - this.onChanged, - this.onChangeEnd, - this.customSliderButton, - this.customSliderButtonWidth, - }); - - /// The audio bars from 0.0 to 1.0. - final List waves; - - /// The progress of the audio. - final Stream progressStream; - - /// Callback called when Slider drag starts. - final Function(double)? onChangeStart; - - /// Callback called when Slider drag updates. - final Function(double)? onChanged; - - /// Callback called when Slider drag ends. - final Function()? onChangeEnd; - - /// A custom Slider button. Use this to substitute the default rounded - /// rectangle. - final Widget? customSliderButton; - - /// The width of the customSliderButton. This should match the width of the - /// provided Widget. - final double? customSliderButtonWidth; - - @override - _StreamVoiceRecordingSliderState createState() => - _StreamVoiceRecordingSliderState(); -} - -@Deprecated("Use 'StreamAudioWaveformSlider' instead") -class _StreamVoiceRecordingSliderState - extends State { - var _dragging = false; - final _initialWidth = 7.0; - final _finalWidth = 14.0; - final _initialHeight = 30.0; - final _finalHeight = 35.0; - - Duration get animationDuration => - _dragging ? Duration.zero : const Duration(milliseconds: 300); - - double get _currentWidth { - if (widget.customSliderButtonWidth != null) { - return widget.customSliderButtonWidth!; - } else { - return _dragging ? _finalWidth : _initialWidth; - } - } - - double get _currentHeight => _dragging ? _finalHeight : _initialHeight; - - double _progressToWidth( - BoxConstraints constraints, double progress, double horizontalPadding) { - final availableWidth = constraints.maxWidth - horizontalPadding * 2; - - return availableWidth * progress - _currentWidth / 2 + horizontalPadding; - } - - @override - Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context).voiceRecordingTheme.sliderTheme; - - return StreamBuilder( - initialData: 0, - stream: widget.progressStream, - builder: (context, snapshot) { - final progress = snapshot.data ?? 0; - - final sliderButton = widget.customSliderButton ?? - Container( - width: _currentWidth, - height: _currentHeight, - decoration: BoxDecoration( - color: theme.buttonColor, - boxShadow: [ - theme.buttonShadow!, - ], - border: Border.all( - color: theme.buttonBorderColor!, - width: theme.buttonBorderWidth!, - ), - borderRadius: theme.buttonBorderRadius, - ), - ); - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Stack( - alignment: Alignment.center, - children: [ - CustomPaint( - size: Size(constraints.maxWidth, constraints.maxHeight), - painter: _AudioBarsPainter( - bars: widget.waves, - spacingRatio: theme.spacingRatio, - barHeightRatio: theme.waveHeightRatio, - colorLeft: theme.waveColorPlayed!, - colorRight: theme.waveColorUnplayed!, - progressPercentage: progress, - padding: theme.horizontalPadding, - ), - ), - AnimatedPositioned( - duration: animationDuration, - left: _progressToWidth( - constraints, progress, theme.horizontalPadding), - curve: const ElasticOutCurve(1.05), - child: sliderButton, - ), - GestureDetector( - onHorizontalDragStart: (details) { - widget.onChangeStart - ?.call(details.localPosition.dx / constraints.maxWidth); - - setState(() { - _dragging = true; - }); - }, - onHorizontalDragEnd: (details) { - widget.onChangeEnd?.call(); - - setState(() { - _dragging = false; - }); - }, - onHorizontalDragUpdate: (details) { - widget.onChanged?.call( - min( - max(details.localPosition.dx / constraints.maxWidth, 0), - 1, - ), - ); - }, - ), - ], - ); - }, - ); - }, - ); - } -} - -class _AudioBarsPainter extends CustomPainter { - _AudioBarsPainter({ - required this.bars, - required this.progressPercentage, - this.colorLeft = Colors.blueAccent, - this.colorRight = Colors.grey, - this.spacingRatio = 0.01, - this.barHeightRatio = 1, - this.padding = 20, - }); - - final List bars; - final double progressPercentage; - final Color colorRight; - final Color colorLeft; - final double spacingRatio; - final double barHeightRatio; - final double padding; - - /// barWidth should include spacing, not only the width of the bar. - /// progressX should be the middle of the moving button of the slider, not - /// initial X position. - Color _barColor(double buttonCenter, double progressX) { - return (progressX > buttonCenter) ? colorLeft : colorRight; - } - - double _barHeight(double barValue, totalHeight) { - return max(barValue * totalHeight * barHeightRatio, 2); - } - - double _progressToWidth(double totalWidth, double progress) { - final availableWidth = totalWidth; - - return availableWidth * progress + padding; - } - - @override - void paint(Canvas canvas, Size size) { - final totalWidth = size.width - padding * 2; - - final spacingWidth = totalWidth * spacingRatio; - final totalBarWidth = totalWidth - spacingWidth * (bars.length - 1); - final barWidth = totalBarWidth / bars.length; - final barY = size.height / 2; - - bars.forEachIndexed((i, barValue) { - final barHeight = _barHeight(barValue, size.height); - final barX = i * (barWidth + spacingWidth) + barWidth / 2 + padding; - - final rect = RRect.fromRectAndRadius( - Rect.fromCenter( - center: Offset(barX, barY), - width: barWidth, - height: barHeight, - ), - const Radius.circular(50), - ); - - final paint = Paint() - ..color = _barColor( - barX + barWidth / 2, - _progressToWidth(totalWidth, progressPercentage), - ); - canvas.drawRRect(rect, paint); - }); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => true; -} diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/voice_recording_attachment_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/voice_recording_attachment_builder.dart deleted file mode 100644 index 412b653cc0..0000000000 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_builder/voice_recording_attachment_builder.dart +++ /dev/null @@ -1,35 +0,0 @@ -// coverage:ignore-file - -part of '../attachment_widget_builder.dart'; - -/// The default attachment builder for voice recordings -@Deprecated("Use 'VoiceRecordingAttachmentPlaylistBuilder' instead") -class VoiceRecordingAttachmentBuilder extends StreamAttachmentWidgetBuilder { - @override - bool canHandle(Message message, Map> attachments) { - final recordings = attachments[AttachmentType.voiceRecording]; - if (recordings != null && recordings.length == 1) return true; - - return false; - } - - @override - Widget build(BuildContext context, Message message, - Map> attachments) { - final recordings = attachments[AttachmentType.voiceRecording]!; - - return StreamVoiceRecordingListPlayer( - playList: recordings - .map( - (r) => PlayListItem( - assetUrl: r.assetUrl, - duration: r.duration, - waveForm: r.waveform, - ), - ) - .toList(), - attachmentBorderRadiusGeometry: BorderRadius.circular(16), - constraints: const BoxConstraints.tightFor(width: 400), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_base.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_base.dart index 7879f63644..4f4605c677 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_base.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_base.dart @@ -31,7 +31,7 @@ abstract class StreamAttachmentHandlerBase { FileType type = FileType.any, List? allowedExtensions, Function(FilePickerStatus)? onFileLoading, - bool allowCompression = true, + int compressionQuality = 0, bool withData = true, bool withReadStream = false, bool lockParentWindow = true, diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart index 0c351f0326..3a9de9dde3 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_html.dart @@ -23,8 +23,6 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { FileType type = FileType.any, List? allowedExtensions, Function(FilePickerStatus)? onFileLoading, - @Deprecated('Has no effect, Use compressionQuality instead.') - bool allowCompression = true, int compressionQuality = 0, bool withData = true, bool withReadStream = false, diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart index 9d920d547f..5268922298 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/stream_attachment_handler_io.dart @@ -111,8 +111,6 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { FileType type = FileType.any, List? allowedExtensions, Function(FilePickerStatus)? onFileLoading, - @Deprecated('Has no effect, Use compressionQuality instead.') - bool allowCompression = true, int compressionQuality = 0, bool withData = true, bool withReadStream = false, diff --git a/packages/stream_chat_flutter/lib/src/message_widget/poll_message.dart b/packages/stream_chat_flutter/lib/src/attachment/poll_attachment.dart similarity index 78% rename from packages/stream_chat_flutter/lib/src/message_widget/poll_message.dart rename to packages/stream_chat_flutter/lib/src/attachment/poll_attachment.dart index aec0646c78..231d4ad455 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/poll_message.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/poll_attachment.dart @@ -8,38 +8,41 @@ import 'package:stream_chat_flutter/src/poll/stream_poll_comments_dialog.dart'; import 'package:stream_chat_flutter/src/poll/stream_poll_options_dialog.dart'; import 'package:stream_chat_flutter/src/poll/stream_poll_results_dialog.dart'; import 'package:stream_chat_flutter/src/stream_chat.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; const _maxVisibleOptionCount = 10; -const _kDefaultPollMessageConstraints = BoxConstraints( - maxWidth: 270, -); - /// {@template pollMessage} -/// A widget that displays a poll message. -/// -/// Used in [MessageCard] to display a poll message. +/// A widget that displays a message poll attachment in a [StreamMessageWidget]. /// {@endtemplate} -class PollMessage extends StatefulWidget { +class PollAttachment extends StatefulWidget { /// {@macro pollMessage} - const PollMessage({ + const PollAttachment({ super.key, required this.message, + this.shape, + this.constraints = const BoxConstraints(), }); /// The message with the poll to display. final Message message; + /// The shape of the poll attachment. + final ShapeBorder? shape; + + /// The constraints to apply to the poll attachment widget. + final BoxConstraints constraints; + @override - State createState() => _PollMessageState(); + State createState() => _PollAttachmentState(); } -class _PollMessageState extends State { +class _PollAttachmentState extends State { late final _messageNotifier = ValueNotifier(widget.message); @override - void didUpdateWidget(covariant PollMessage oldWidget) { + void didUpdateWidget(covariant PollAttachment oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.message != widget.message) { // If the message changes, schedule an update for the next frame @@ -57,6 +60,17 @@ class _PollMessageState extends State { @override Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + + final shape = widget.shape ?? + RoundedRectangleBorder( + side: BorderSide( + color: theme.colorTheme.borders, + strokeAlign: BorderSide.strokeAlignOutside, + ), + borderRadius: BorderRadius.circular(14), + ); + return ValueListenableBuilder( valueListenable: _messageNotifier, builder: (context, message, child) { @@ -96,8 +110,9 @@ class _PollMessageState extends State { channel.createPollOption(poll, PollOption(text: optionText)); } - return ConstrainedBox( - constraints: _kDefaultPollMessageConstraints, + return Container( + constraints: widget.constraints, + decoration: ShapeDecoration(shape: shape), child: StreamPollInteractor( poll: poll, currentUser: currentUser, diff --git a/packages/stream_chat_flutter/lib/src/attachment/voice_recording_attachment_playlist.dart b/packages/stream_chat_flutter/lib/src/attachment/voice_recording_attachment_playlist.dart index d4bb67c0f7..2bd55a8023 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/voice_recording_attachment_playlist.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/voice_recording_attachment_playlist.dart @@ -1,6 +1,5 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/attachment/voice_recording_attachment.dart'; import 'package:stream_chat_flutter/src/audio/audio_playlist_controller.dart'; import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; diff --git a/packages/stream_chat_flutter/lib/src/bottom_sheets/edit_message_sheet.dart b/packages/stream_chat_flutter/lib/src/bottom_sheets/edit_message_sheet.dart index b53db1f2cb..ced57b6ecc 100644 --- a/packages/stream_chat_flutter/lib/src/bottom_sheets/edit_message_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/bottom_sheets/edit_message_sheet.dart @@ -115,10 +115,6 @@ class _EditMessageSheetState extends State { StreamMessageInput( elevation: 0, messageInputController: controller, - // Disallow editing poll for now as it's not supported. - allowedAttachmentPickerTypes: [ - ...AttachmentPickerType.values, - ]..remove(AttachmentPickerType.poll), preMessageSending: (m) { FocusScope.of(context).unfocus(); Navigator.of(context).pop(); diff --git a/packages/stream_chat_flutter/lib/src/channel/stream_channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel/stream_channel_avatar.dart index 6c71eee4ee..536a45ff7a 100644 --- a/packages/stream_chat_flutter/lib/src/channel/stream_channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel/stream_channel_avatar.dart @@ -108,7 +108,7 @@ class StreamChannelAvatar extends StatelessWidget { final fallbackWidget = Center( child: Text( - channel.name?[0] ?? '', + channel.name?.characters.firstOrNull ?? '', style: TextStyle( color: colorTheme.barsBg, fontWeight: FontWeight.bold, diff --git a/packages/stream_chat_flutter/lib/src/channel/stream_message_preview_text.dart b/packages/stream_chat_flutter/lib/src/channel/stream_message_preview_text.dart index fdd0ce95d2..09cda58d55 100644 --- a/packages/stream_chat_flutter/lib/src/channel/stream_message_preview_text.dart +++ b/packages/stream_chat_flutter/lib/src/channel/stream_message_preview_text.dart @@ -86,6 +86,10 @@ class StreamMessagePreviewText extends StatelessWidget { return _pollPreviewText(context, poll, currentUser); } + if (message.sharedLocation case final location?) { + return translations.locationLabel(isLive: location.isLive); + } + final previewText = _previewMessageContextText(context, message); if (previewText == null) return translations.emptyMessagePreviewText; diff --git a/packages/stream_chat_flutter/lib/src/context_menu_items/context_menu_reaction_picker.dart b/packages/stream_chat_flutter/lib/src/context_menu_items/context_menu_reaction_picker.dart deleted file mode 100644 index fb22d44d27..0000000000 --- a/packages/stream_chat_flutter/lib/src/context_menu_items/context_menu_reaction_picker.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'package:ezanimation/ezanimation.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template contextMenuReactionPicker} -/// Allows the user to select reactions to a message on desktop & web via -/// context menu. -/// -/// This differs slightly from [StreamReactionPicker] in order to match our -/// design spec. -/// -/// Used by the `_buildContextMenu()` function found in `message_widget.dart`. -/// It is not recommended to use this widget directly. -/// {@endtemplate} -class ContextMenuReactionPicker extends StatefulWidget { - /// {@macro contextMenuReactionPicker} - const ContextMenuReactionPicker({ - super.key, - required this.message, - }); - - /// The message to react to. - final Message message; - - @override - State createState() => - _ContextMenuReactionPickerState(); -} - -class _ContextMenuReactionPickerState extends State - with TickerProviderStateMixin { - List animations = []; - - Future triggerAnimations() async { - for (final a in animations) { - a.start(); - await Future.delayed(const Duration(milliseconds: 100)); - } - } - - Future pop() async { - for (final a in animations) { - a.stop(); - } - Navigator.of(context).pop(); - } - - /// Add a reaction to the message - void sendReaction(BuildContext context, String reactionType) { - StreamChannel.of(context).channel.sendReaction( - widget.message, - reactionType, - enforceUnique: - StreamChatConfiguration.of(context).enforceUniqueReactions, - ); - pop(); - } - - /// Remove a reaction from the message - void removeReaction(BuildContext context, Reaction reaction) { - StreamChannel.of(context).channel.deleteReaction(widget.message, reaction); - pop(); - } - - @override - void dispose() { - for (final a in animations) { - a.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final reactionIcons = StreamChatConfiguration.of(context).reactionIcons; - - if (animations.isEmpty && reactionIcons.isNotEmpty) { - reactionIcons.forEach((element) { - animations.add( - EzAnimation.tween( - Tween(begin: 0.0, end: 1.0), - const Duration(milliseconds: 250), - curve: Curves.easeInOutBack, - ), - ); - }); - - triggerAnimations(); - } - - final child = Material( - color: StreamChatTheme.of(context).messageListViewTheme.backgroundColor ?? - Theme.of(context).scaffoldBackgroundColor, - //clipBehavior: Clip.hardEdge, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - spacing: 16, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.min, - children: reactionIcons.map((reactionIcon) { - final ownReactionIndex = widget.message.ownReactions?.indexWhere( - (reaction) => reaction.type == reactionIcon.type, - ) ?? - -1; - final index = reactionIcons.indexOf(reactionIcon); - - final child = reactionIcon.builder( - context, - ownReactionIndex != -1, - 24, - ); - - return ConstrainedBox( - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - child: RawMaterialButton( - elevation: 0, - shape: ContinuousRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction( - context, - widget.message.ownReactions![ownReactionIndex], - ); - } else { - sendReaction( - context, - reactionIcon.type, - ); - } - }, - child: AnimatedBuilder( - animation: animations[index], - builder: (context, child) => Transform.scale( - scale: animations[index].value, - child: child, - ), - child: child, - ), - ), - ); - }).toList(), - ), - ), - ); - - return TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - curve: Curves.easeInOutBack, - duration: const Duration(milliseconds: 500), - builder: (context, val, widget) => Transform.scale( - scale: val, - child: widget, - ), - child: child, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/context_menu_items/download_menu_item.dart b/packages/stream_chat_flutter/lib/src/context_menu_items/download_menu_item.dart deleted file mode 100644 index a3f4d5a207..0000000000 --- a/packages/stream_chat_flutter/lib/src/context_menu_items/download_menu_item.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/context_menu_items/stream_chat_context_menu_item.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template downloadMenuItem} -/// Defines a "download" context menu item that allows a user to download -/// a given attachment. -/// -/// Used in [DesktopFullscreenMedia]. -/// {@endtemplate} -class DownloadMenuItem extends StatelessWidget { - /// {@macro downloadMenuItem} - const DownloadMenuItem({ - super.key, - required this.attachment, - }); - - /// The attachment to download. - final Attachment attachment; - - @override - Widget build(BuildContext context) { - return StreamChatContextMenuItem( - leading: const StreamSvgIcon(icon: StreamSvgIcons.download), - title: Text(context.translations.downloadLabel), - onClick: () async { - Navigator.of(context).pop(); - StreamAttachmentHandler.instance.downloadAttachment(attachment); - }, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/context_menu_items/stream_chat_context_menu_item.dart b/packages/stream_chat_flutter/lib/src/context_menu_items/stream_chat_context_menu_item.dart deleted file mode 100644 index dfe64684e2..0000000000 --- a/packages/stream_chat_flutter/lib/src/context_menu_items/stream_chat_context_menu_item.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template streamChatContextMenuItem} -/// Builds a context menu item according to Stream design specification. -/// {@endtemplate} -class StreamChatContextMenuItem extends StatelessWidget { - /// {@macro streamChatContextMenuItem} - const StreamChatContextMenuItem({ - super.key, - this.child, - this.leading, - this.title, - this.onClick, - }); - - /// The child widget for this menu item. Usually a [DesktopReactionPicker]. - /// - /// Leave null in order to use the default menu item widget. - final Widget? child; - - /// The widget to lead the menu item with. Usually an [Icon]. - /// - /// If [child] is specified, this will be ignored. - final Widget? leading; - - /// The title of the menu item. Usually a [Text]. - /// - /// If [child] is specified, this will be ignored. - final Widget? title; - - /// The action to perform when the menu item is clicked. - /// - /// If [child] is specified, this will be ignored. - final VoidCallback? onClick; - - @override - Widget build(BuildContext context) { - return Ink( - color: StreamChatTheme.of(context).messageListViewTheme.backgroundColor ?? - Theme.of(context).scaffoldBackgroundColor, - child: child ?? - ListTile( - dense: true, - leading: leading, - title: title, - onTap: onClick, - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_desktop.dart b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_desktop.dart index 1296535ca9..f4006ca33d 100644 --- a/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_desktop.dart +++ b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_desktop.dart @@ -4,7 +4,6 @@ import 'package:media_kit_video/media_kit_video.dart'; import 'package:photo_view/photo_view.dart'; import 'package:stream_chat_flutter/src/context_menu/context_menu.dart'; import 'package:stream_chat_flutter/src/context_menu/context_menu_region.dart'; -import 'package:stream_chat_flutter/src/context_menu_items/download_menu_item.dart'; import 'package:stream_chat_flutter/src/fullscreen_media/full_screen_media_widget.dart'; import 'package:stream_chat_flutter/src/fullscreen_media/gallery_navigation_item.dart'; import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; @@ -123,15 +122,12 @@ class _FullScreenMediaDesktopState extends State { return Stack( children: [ ContextMenuRegion( - contextMenuBuilder: (_, anchor) { + contextMenuBuilder: (context, anchor) { + final index = _currentPage.value; + final mediaAttachment = widget.mediaAttachmentPackages[index]; return ContextMenu( anchor: anchor, - menuItems: [ - DownloadMenuItem( - attachment: widget - .mediaAttachmentPackages[_currentPage.value].attachment, - ), - ], + menuItems: [_DownloadMenuItem(mediaAttachment: mediaAttachment)], ); }, child: _PlaylistPlayer( @@ -366,8 +362,8 @@ class _FullScreenMediaDesktopState extends State { return ContextMenu( anchor: anchor, menuItems: [ - DownloadMenuItem( - attachment: attachment, + _DownloadMenuItem( + mediaAttachment: currentAttachmentPackage, ), ], ); @@ -393,6 +389,52 @@ class _FullScreenMediaDesktopState extends State { } } +/// {@template streamDownloadMenuItem} +/// A context menu item for downloading an attachment from a message. +/// +/// This widget displays a download option in a context menu, allowing users to +/// download the attachment associated with a message. +/// +/// It uses [StreamMessageActionItem] and [StreamMessageAction] to create a +/// consistent UI with other message actions. +/// {@endtemplate} +class _DownloadMenuItem extends StatelessWidget { + /// {@macro streamDownloadMenuItem} + const _DownloadMenuItem({ + required this.mediaAttachment, + }); + + /// The attachment package containing the message and attachment to download. + final StreamAttachmentPackage mediaAttachment; + static const String _attachmentKey = 'attachment'; + + @override + Widget build(BuildContext context) { + return StreamMessageActionItem( + action: StreamMessageAction( + leading: const StreamSvgIcon(icon: StreamSvgIcons.download), + title: Text(context.translations.downloadLabel), + action: CustomMessageAction( + message: mediaAttachment.message, + extraData: {_attachmentKey: mediaAttachment.attachment}, + ), + ), + // TODO: Use a callback to handle the action instead of onTap. + onTap: (action) async { + if (action is! CustomMessageAction) return; + final attachment = action.extraData[_attachmentKey] as Attachment?; + if (attachment == null) return; + + final popped = await Navigator.of(context).maybePop(); + if (popped) { + final handler = StreamAttachmentHandler.instance; + return handler.downloadAttachment(attachment).ignore(); + } + }, + ); + } +} + /// Class for packaging up things required for videos class DesktopVideoPackage { /// Constructor for creating [VideoPackage] diff --git a/packages/stream_chat_flutter/lib/src/icons/stream_svg_icon.dart b/packages/stream_chat_flutter/lib/src/icons/stream_svg_icon.dart index 03138b891e..46acd43a8d 100644 --- a/packages/stream_chat_flutter/lib/src/icons/stream_svg_icon.dart +++ b/packages/stream_chat_flutter/lib/src/icons/stream_svg_icon.dart @@ -1,5 +1,3 @@ -// ignore_for_file: deprecated_member_use_from_same_package - import 'package:flutter/material.dart'; import 'package:svg_icon_widget/svg_icon_widget.dart'; @@ -18,1150 +16,12 @@ class StreamSvgIcon extends StatelessWidget { const StreamSvgIcon({ super.key, this.icon, - @Deprecated("Use 'icon' instead") this.assetName, this.color, - double? size, - @Deprecated("Use 'size' instead") this.width, - @Deprecated("Use 'size' instead") this.height, + this.size, this.textDirection, this.semanticLabel, this.applyTextScaling, - }) : assert( - size == null || (width == null && height == null), - 'Cannot provide both a size and a width or height', - ), - size = size ?? width ?? height; - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.settings({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.settings, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.down({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.down, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.up({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.up, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.attach({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.attach, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.loveReaction({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.loveReaction, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.thumbsUpReaction({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.thumbsUpReaction, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.thumbsDownReaction({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.thumbsDownReaction, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.lolReaction({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.lolReaction, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.wutReaction({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.wutReaction, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.smile({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.smile, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.mentions({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.mentions, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.record({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.record, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.camera({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.camera, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.files({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.files, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.polls({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.polls, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.send({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.send, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.pictures({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.pictures, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.left({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.left, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.user({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.user, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.userAdd({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.userAdd, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.check({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.check, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.checkAll({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.checkAll, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.checkSend({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.checkSend, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.penWrite({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.penWrite, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.contacts({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.contacts, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.close({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.close, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.search({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.search, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.right({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.right, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.mute({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.mute, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.userRemove({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.userRemove, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.lightning({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.lightning, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.emptyCircleLeft({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.emptyCircleRight, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.message({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.message, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.messageUnread({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.messageUnread, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.thread({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.threadReply, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.reply({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.reply, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.edit({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.edit, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.download({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.download, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.cloudDownload({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.cloudDownload, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.copy({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.copy, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.delete({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.delete, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.eye({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.eye, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.arrowRight({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.arrowRight, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.closeSmall({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.closeSmall, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.iconCurveLineLeftUp({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.reply, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.iconMoon({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.moon, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.iconShare({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.share, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.iconGrid({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.grid, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.iconSendMessage({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.sendMessage, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.iconMenuPoint({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.menuPoint, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.iconSave({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.save, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.shareArrow({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.shareArrow, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeAac({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeAudioAac, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetype7z({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeCompression7z, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeCsv({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeCodeCsv, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeDoc({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeTextDoc, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeDocx({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeTextDocx, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeGeneric({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeOtherStandard, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeHtml({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeCodeHtml, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeMd({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeCodeMd, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeOdt({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeTextOdt, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypePdf({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeOtherPdf, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypePpt({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypePresentationPpt, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypePptx({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypePresentationPptx, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeRar({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeCompressionRar, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeRtf({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeTextRtf, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeTar({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeCodeTar, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeTxt({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeTextTxt, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeXls({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeSpreadsheetXls, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeXlsx({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeSpreadsheetXlsx, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.filetypeZip({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.filetypeCompressionZip, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.iconGroup({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.group, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.iconNotification({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.notification, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.iconUserDelete({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.userDelete, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.error({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.error, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.circleUp({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.circleUp, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.iconUserSettings({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.userSettings, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.giphyIcon({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.giphy, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.imgur({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.imgur, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.volumeUp({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.volumeUp, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.flag({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.flag, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.iconFlag({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.flag, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.retry({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.retry, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.pin({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.pin, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.videoCall({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.videoCall, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.award({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.award, - color: color, - size: size, - ); - } - - /// [StreamSvgIcon] type - @Deprecated("Use regular 'StreamSvgIcon' with 'icon' instead") - factory StreamSvgIcon.reload({ - double? size, - Color? color, - }) { - return StreamSvgIcon( - icon: StreamSvgIcons.reload, - color: color, - size: size, - ); - } + }); /// The icon to display. /// @@ -1169,21 +29,6 @@ class StreamSvgIcon extends StatelessWidget { /// space of the specified [size]. final StreamSvgIconData? icon; - /// The asset to display. - /// - /// The asset can be null, in which case the widget will render as an empty - /// space of the specified [size]. - @Deprecated("Use 'icon' instead") - final String? assetName; - - /// Width of icon - @Deprecated("Use 'size' instead") - final double? width; - - /// Height of icon - @Deprecated("Use 'size' instead") - final double? height; - /// The size of the icon in logical pixels. /// /// Icons occupy a square with width and height equal to size. @@ -1254,25 +99,8 @@ class StreamSvgIcon extends StatelessWidget { @override Widget build(BuildContext context) { - assert( - icon == null || assetName == null, - 'Cannot provide both an icon and an assetName', - ); - - const iconPackage = 'stream_chat_flutter'; - final iconData = switch (icon) { - final icon? => icon, - null => switch (assetName) { - final name? => SvgIconData( - 'lib/svgs/$name', - package: iconPackage, - ), - _ => null, - }, - }; - return SvgIcon( - iconData, + icon, size: size, color: color, textDirection: textDirection, @@ -1281,58 +109,3 @@ class StreamSvgIcon extends StatelessWidget { ); } } - -/// Alternative of [StreamSvgIcon] which follows the [IconTheme]. -@Deprecated("Use regular 'StreamSvgIcon' instead") -class StreamIconThemeSvgIcon extends StatelessWidget { - /// Creates a [StreamIconThemeSvgIcon]. - @Deprecated("Use regular 'StreamSvgIcon' instead") - const StreamIconThemeSvgIcon({ - super.key, - this.assetName, - this.width, - this.height, - this.color, - }); - - /// Factory constructor to create [StreamIconThemeSvgIcon] - /// from [StreamSvgIcon]. - @Deprecated("Use regular 'StreamSvgIcon' instead") - factory StreamIconThemeSvgIcon.fromSvgIcon( - StreamSvgIcon streamSvgIcon, - ) { - return StreamIconThemeSvgIcon( - assetName: streamSvgIcon.assetName, - width: streamSvgIcon.width, - height: streamSvgIcon.height, - color: streamSvgIcon.color, - ); - } - - /// Name of icon asset - final String? assetName; - - /// Width of icon - final double? width; - - /// Height of icon - final double? height; - - /// Color of icon - final Color? color; - - @override - Widget build(BuildContext context) { - final iconTheme = IconTheme.of(context); - final color = this.color ?? iconTheme.color; - final width = this.width ?? iconTheme.size; - final height = this.height ?? iconTheme.size; - - return StreamSvgIcon( - assetName: assetName, - width: width, - height: height, - color: color, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/indicators/unread_indicator.dart b/packages/stream_chat_flutter/lib/src/indicators/unread_indicator.dart index 728d54f659..f035edb700 100644 --- a/packages/stream_chat_flutter/lib/src/indicators/unread_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/indicators/unread_indicator.dart @@ -7,13 +7,9 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@endtemplate} class StreamUnreadIndicator extends StatelessWidget { /// Displays the total unread count. - StreamUnreadIndicator({ + const StreamUnreadIndicator({ super.key, - @Deprecated('Use StreamUnreadIndicator.channels instead') String? cid, - }) : _unreadType = switch (cid) { - final cid? => _UnreadChannels(cid: cid), - _ => const _TotalUnreadCount(), - }; + }) : _unreadType = const _TotalUnreadCount(); /// Displays the unreadChannel count. /// diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 87437b86a5..957ff7683e 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -1,3 +1,5 @@ +// ignore_for_file: lines_longer_than_80_chars + import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/message_list_view/message_list_view.dart'; import 'package:stream_chat_flutter/src/misc/connection_status_builder.dart'; @@ -543,6 +545,11 @@ abstract class Translations { /// The label for draft message String get draftLabel; + + /// The label for location attachment. + /// + /// [isLive] indicates if the location is live or not. + String locationLabel({bool isLive = false}); } /// Default implementation of Translation strings for the stream chat widgets @@ -727,8 +734,7 @@ class DefaultTranslations implements Translations { @override String get flagMessageQuestion => - 'Do you want to send a copy of this message to a' - '\nmoderator for further investigation?'; + 'Do you want to send a copy of this message to a moderator for further investigation?'; @override String get flagLabel => 'FLAG'; @@ -751,7 +757,7 @@ class DefaultTranslations implements Translations { @override String get deleteMessageQuestion => - 'Are you sure you want to permanently delete this\nmessage?'; + 'Are you sure you want to permanently delete this message?'; @override String get operationCouldNotBeCompletedText => @@ -1216,4 +1222,10 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments @override String get draftLabel => 'Draft'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ Live Location'; + return '๐Ÿ“ Location'; + } } diff --git a/packages/stream_chat_flutter/lib/src/message_action/message_action.dart b/packages/stream_chat_flutter/lib/src/message_action/message_action.dart new file mode 100644 index 0000000000..27e4fb00fa --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_action/message_action.dart @@ -0,0 +1,70 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +part 'message_action_type.dart'; + +/// {@template streamMessageAction} +/// A class that represents an action that can be performed on a message. +/// +/// This class is used to define actions that appear in message action menus +/// or option lists, providing a consistent structure for message-related +/// actions including their visual representation and behavior. +/// {@endtemplate} +class StreamMessageAction { + /// {@macro streamMessageAction} + const StreamMessageAction({ + required this.action, + this.isDestructive = false, + this.leading, + this.iconColor, + this.title, + this.titleTextColor, + this.titleTextStyle, + this.backgroundColor, + }); + + /// The [MessageAction] that this item represents. + final T action; + + /// Whether the action is destructive. + /// + /// Destructive actions are typically displayed with a red color to indicate + /// that they will remove or delete content. + /// + /// Defaults to `false`. + final bool isDestructive; + + /// A widget to display before the title. + /// + /// Typically an [Icon] or a [CircleAvatar] widget. + final Widget? leading; + + /// The color for the [leading] icon. + /// + /// If this property is null, the icon will use the default color provided by + /// the theme or parent widget. + final Color? iconColor; + + /// The primary content of the action item. + /// + /// Typically a [Text] widget. + /// + /// This should not wrap. To enforce the single line limit, use + /// [Text.maxLines]. + final Widget? title; + + /// The color for the text in the [title]. + /// + /// If this property is null, the text will use the default color provided by + /// the theme or parent widget. + final Color? titleTextColor; + + /// The text style for the [title]. + /// + /// If this property is null, the title will use the default text style + /// provided by the theme or parent widget. + final TextStyle? titleTextStyle; + + /// Defines the background color of the action item. + final Color? backgroundColor; +} diff --git a/packages/stream_chat_flutter/lib/src/message_action/message_action_item.dart b/packages/stream_chat_flutter/lib/src/message_action/message_action_item.dart new file mode 100644 index 0000000000..fa4af9328c --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_action/message_action_item.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_action/message_action.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; + +/// {@template streamMessageActionItem} +/// A widget that represents an action item within a message interface. +/// +/// This widget is typically used in action menus or option lists related to +/// messages, providing a consistent appearance for selectable actions with an +/// optional icon and title. +/// {@endtemplate} +class StreamMessageActionItem extends StatelessWidget { + /// {@macro streamMessageActionItem} + const StreamMessageActionItem({ + super.key, + required this.action, + this.onTap, + }); + + /// The underlying action that this item represents. + final StreamMessageAction action; + + /// Called when the user taps this action item. + /// + /// This callback provides the tap handling for the action item, and is + /// typically used to execute the associated action or dismiss menus. + final OnMessageActionTap? onTap; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final textTheme = theme.textTheme; + final colorTheme = theme.colorTheme; + + final iconColor = switch (action.isDestructive) { + true => action.iconColor ?? colorTheme.accentError, + false => action.iconColor ?? colorTheme.textLowEmphasis, + }; + + final titleTextColor = switch (action.isDestructive) { + true => action.titleTextColor ?? colorTheme.accentError, + false => action.titleTextColor ?? colorTheme.textHighEmphasis, + }; + + final titleTextStyle = action.titleTextStyle ?? textTheme.body; + final backgroundColor = action.backgroundColor ?? colorTheme.barsBg; + + return InkWell( + onTap: switch (onTap) { + final onTap? => () => onTap(action.action), + _ => null, + }, + child: Ink( + color: backgroundColor, + child: IconTheme.merge( + data: IconThemeData(color: iconColor), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 9, + horizontal: 16, + ), + child: Row( + spacing: 16, + mainAxisSize: MainAxisSize.min, + children: [ + if (action.leading case final leading?) leading, + if (action.title case final title?) + DefaultTextStyle( + style: titleTextStyle.copyWith( + color: titleTextColor, + ), + child: title, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_action/message_action_type.dart b/packages/stream_chat_flutter/lib/src/message_action/message_action_type.dart new file mode 100644 index 0000000000..5aea4980b1 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_action/message_action_type.dart @@ -0,0 +1,137 @@ +part of 'message_action.dart'; + +/// {@template onMessageActionTap} +/// Signature for a function that is called when a message action is tapped. +/// {@endtemplate} +typedef OnMessageActionTap = void Function(T action); + +/// {@template messageAction} +/// A sealed class that represents different actions that can be performed on a +/// message. +/// {@endtemplate} +sealed class MessageAction { + /// {@macro messageAction} + const MessageAction({required this.message}); + + /// The message this action applies to. + final Message message; +} + +/// Action to show reaction selector for adding reactions to a message +final class SelectReaction extends MessageAction { + /// Create a new select reaction action + const SelectReaction({ + required super.message, + required this.reaction, + this.enforceUnique = false, + }); + + /// The reaction to be added or removed from the message. + final Reaction reaction; + + /// Whether to enforce unique reactions. + final bool enforceUnique; +} + +/// Action to copy message content to clipboard +final class CopyMessage extends MessageAction { + /// Create a new copy message action + const CopyMessage({required super.message}); +} + +/// Action to delete a message from the conversation +final class DeleteMessage extends MessageAction { + /// Create a new delete message action + const DeleteMessage({required super.message}); +} + +/// Action to hard delete a message permanently from the conversation +final class HardDeleteMessage extends MessageAction { + /// Create a new hard delete message action + const HardDeleteMessage({required super.message}); +} + +/// Action to modify content of an existing message +final class EditMessage extends MessageAction { + /// Create a new edit message action + const EditMessage({required super.message}); +} + +/// Action to flag a message for moderator review +final class FlagMessage extends MessageAction { + /// Create a new flag message action + const FlagMessage({required super.message}); +} + +/// Action to mark a message as unread for later viewing +final class MarkUnread extends MessageAction { + /// Create a new mark unread action + const MarkUnread({required super.message}); +} + +/// Action to mute a user to prevent notifications from their messages +final class MuteUser extends MessageAction { + /// Create a new mute user action + const MuteUser({ + required super.message, + required this.user, + }); + + /// The user to be muted. + final User user; +} + +/// Action to unmute a user to receive notifications from their messages +final class UnmuteUser extends MessageAction { + /// Create a new unmute user action + const UnmuteUser({ + required super.message, + required this.user, + }); + + /// The user to be unmuted. + final User user; +} + +/// Action to pin a message to make it prominently visible in the channel +final class PinMessage extends MessageAction { + /// Create a new pin message action + const PinMessage({required super.message}); +} + +/// Action to remove a previously pinned message +final class UnpinMessage extends MessageAction { + /// Create a new unpin message action + const UnpinMessage({required super.message}); +} + +/// Action to attempt to resend a message that failed to send +final class ResendMessage extends MessageAction { + /// Create a new resend message action + const ResendMessage({required super.message}); +} + +/// Action to create a reply with quoted original message content +final class QuotedReply extends MessageAction { + /// Create a new quoted reply action + const QuotedReply({required super.message}); +} + +/// Action to start a threaded conversation from a message +final class ThreadReply extends MessageAction { + /// Create a new thread reply action + const ThreadReply({required super.message}); +} + +/// Custom message action that allows for additional data to be passed +/// along with the message. +class CustomMessageAction extends MessageAction { + /// Create a new custom message action + const CustomMessageAction({ + required super.message, + this.extraData = const {}, + }); + + /// Map of extra data associated with the action. + final Map extraData; +} diff --git a/packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart b/packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart new file mode 100644 index 0000000000..f9dbae07b4 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart @@ -0,0 +1,249 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/message_action/message_action.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// {@template streamMessageActionsBuilder} +/// A utility class that provides a builder for message actions +/// which can be reused across mobile platforms. +/// {@endtemplate} +class StreamMessageActionsBuilder { + /// Private constructor to prevent instantiation + StreamMessageActionsBuilder._(); + + /// Returns a list of message actions for the "bounced with error" state. + /// + /// This method builds a list of [StreamMessageAction]s that are applicable to + /// the given [message] when it is in the "bounced with error" state. + /// + /// The actions include options to retry sending the message, edit or delete + /// the message. + static List buildBouncedErrorActions({ + required BuildContext context, + required Message message, + }) { + // If the message is not bounced with an error, we don't show any actions. + if (!message.isBouncedWithError) return []; + + return [ + StreamMessageAction( + action: ResendMessage(message: message), + iconColor: StreamChatTheme.of(context).colorTheme.accentPrimary, + title: Text(context.translations.sendAnywayLabel), + leading: const StreamSvgIcon(icon: StreamSvgIcons.circleUp), + ), + StreamMessageAction( + action: EditMessage(message: message), + title: Text(context.translations.editMessageLabel), + leading: const StreamSvgIcon(icon: StreamSvgIcons.edit), + ), + StreamMessageAction( + isDestructive: true, + action: HardDeleteMessage(message: message), + title: Text(context.translations.deleteMessageLabel), + leading: const StreamSvgIcon(icon: StreamSvgIcons.delete), + ), + ]; + } + + /// Returns a list of message actions based on the provided message and + /// channel capabilities. + /// + /// This method builds a list of [StreamMessageAction]s that are applicable to + /// the given [message] in the [channel], considering the permissions of the + /// [currentUser] and the current state of the message. + static List buildActions({ + required BuildContext context, + required Message message, + required Channel channel, + OwnUser? currentUser, + Iterable? customActions, + }) { + final messageState = message.state; + + // If the message is deleted, we don't show any actions. + if (messageState.isDeleted) return []; + + if (messageState.isFailed) { + return [ + if (messageState.isSendingFailed || messageState.isUpdatingFailed) ...[ + StreamMessageAction( + action: ResendMessage(message: message), + leading: const StreamSvgIcon(icon: StreamSvgIcons.circleUp), + iconColor: StreamChatTheme.of(context).colorTheme.accentPrimary, + title: Text( + context.translations.toggleResendOrResendEditedMessage( + isUpdateFailed: messageState.isUpdatingFailed, + ), + ), + ), + ], + if (message.state.isDeletingFailed) + StreamMessageAction( + isDestructive: true, + action: ResendMessage(message: message), + leading: const StreamSvgIcon(icon: StreamSvgIcons.delete), + title: Text( + context.translations.toggleDeleteRetryDeleteMessageText( + isDeleteFailed: true, + ), + ), + ), + ]; + } + + final isSentByCurrentUser = message.user?.id == currentUser?.id; + final isThreadMessage = message.parentId != null; + final isParentMessage = (message.replyCount ?? 0) > 0; + final canShowInChannel = message.showInChannel ?? true; + final isPrivateMessage = message.hasRestrictedVisibility; + final canSendReply = channel.canSendReply; + final canPinMessage = channel.canPinMessage; + final canQuoteMessage = channel.canQuoteMessage; + final canReceiveReadEvents = channel.canReceiveReadEvents; + final canUpdateAnyMessage = channel.canUpdateAnyMessage; + final canUpdateOwnMessage = channel.canUpdateOwnMessage; + final canDeleteAnyMessage = channel.canDeleteAnyMessage; + final canDeleteOwnMessage = channel.canDeleteOwnMessage; + final containsPoll = message.poll != null; + final containsGiphy = message.attachments.any( + (attachment) => attachment.type == AttachmentType.giphy, + ); + + final messageActions = []; + + if (canQuoteMessage) { + messageActions.add( + StreamMessageAction( + action: QuotedReply(message: message), + title: Text(context.translations.replyLabel), + leading: const StreamSvgIcon(icon: StreamSvgIcons.reply), + ), + ); + } + + if (canSendReply && !isThreadMessage) { + messageActions.add( + StreamMessageAction( + action: ThreadReply(message: message), + title: Text(context.translations.threadReplyLabel), + leading: const StreamSvgIcon(icon: StreamSvgIcons.threadReply), + ), + ); + } + + if (canReceiveReadEvents) { + StreamMessageAction markUnreadAction() { + return StreamMessageAction( + action: MarkUnread(message: message), + title: Text(context.translations.markAsUnreadLabel), + leading: const StreamSvgIcon(icon: StreamSvgIcons.messageUnread), + ); + } + + // If message is a parent message, it can be marked unread independent of + // other logic. + if (isParentMessage) { + messageActions.add(markUnreadAction()); + } + // If the message is in the channel view, only other user messages can be + // marked unread. + else if (!isSentByCurrentUser && (!isThreadMessage || canShowInChannel)) { + messageActions.add(markUnreadAction()); + } + } + + if (message.text case final text? when text.isNotEmpty) { + messageActions.add( + StreamMessageAction( + action: CopyMessage(message: message), + title: Text(context.translations.copyMessageLabel), + leading: const StreamSvgIcon(icon: StreamSvgIcons.copy), + ), + ); + } + + if (!containsPoll && !containsGiphy) { + if (canUpdateAnyMessage || (canUpdateOwnMessage && isSentByCurrentUser)) { + messageActions.add( + StreamMessageAction( + action: EditMessage(message: message), + title: Text(context.translations.editMessageLabel), + leading: const StreamSvgIcon(icon: StreamSvgIcons.edit), + ), + ); + } + } + + // Pinning a private message is not allowed, simply because pinning a + // message is meant to bring attention to that message, that is not possible + // with a message that is only visible to a subset of users. + if (canPinMessage && !isPrivateMessage) { + final isPinned = message.pinned; + final label = context.translations.togglePinUnpinText; + + final action = switch (isPinned) { + true => UnpinMessage(message: message), + false => PinMessage(message: message) + }; + + messageActions.add( + StreamMessageAction( + action: action, + title: Text(label.call(pinned: isPinned)), + leading: const StreamSvgIcon(icon: StreamSvgIcons.pin), + ), + ); + } + + if (canDeleteAnyMessage || (canDeleteOwnMessage && isSentByCurrentUser)) { + final label = context.translations.toggleDeleteRetryDeleteMessageText; + + messageActions.add( + StreamMessageAction( + isDestructive: true, + action: DeleteMessage(message: message), + leading: const StreamSvgIcon(icon: StreamSvgIcons.delete), + title: Text(label.call(isDeleteFailed: false)), + ), + ); + } + + if (!isSentByCurrentUser) { + messageActions.add( + StreamMessageAction( + action: FlagMessage(message: message), + title: Text(context.translations.flagMessageLabel), + leading: const StreamSvgIcon(icon: StreamSvgIcons.flag), + ), + ); + } + + if (message.user case final messageUser? + when channel.config?.mutes == true && !isSentByCurrentUser) { + final mutedUsers = currentUser?.mutes.map((mute) => mute.target.id); + final isMuted = mutedUsers?.contains(messageUser.id) ?? false; + final label = context.translations.toggleMuteUnmuteUserText; + + final action = switch (isMuted) { + true => UnmuteUser(message: message, user: messageUser), + false => MuteUser(message: message, user: messageUser), + }; + + messageActions.add( + StreamMessageAction( + action: action, + title: Text(label.call(isMuted: isMuted)), + leading: const StreamSvgIcon(icon: StreamSvgIcons.mute), + ), + ); + } + + // Add all the remaining custom actions if provided. + if (customActions case final actions?) messageActions.addAll(actions); + + return messageActions; + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/copy_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/copy_message_button.dart deleted file mode 100644 index cdb7415f21..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/copy_message_button.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template copyMessageButton} -/// Allows a user to copy the text of a message. -/// -/// Used by [MessageActionsModal]. Should not be used by itself. -/// {@endtemplate} -class CopyMessageButton extends StatelessWidget { - /// {@macro copyMessageButton} - const CopyMessageButton({ - super.key, - required this.onTap, - }); - - /// The callback to perform when the button is tapped. - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon( - size: 24, - icon: StreamSvgIcons.copy, - color: streamChatThemeData.primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - context.translations.copyMessageLabel, - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/delete_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/delete_message_button.dart deleted file mode 100644 index 45e0477a5d..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/delete_message_button.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template deleteMessageButton} -/// A button that allows a user to delete the selected message. -/// -/// Used by [MessageActionsModal]. Should not be used by itself. -/// {@endtemplate} -class DeleteMessageButton extends StatelessWidget { - /// {@macro deleteMessageButton} - const DeleteMessageButton({ - super.key, - required this.isDeleteFailed, - required this.onTap, - }); - - /// Indicates whether the deletion has failed or not. - final bool isDeleteFailed; - - /// The action (deleting the message) to be performed on tap. - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); - return InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon( - icon: StreamSvgIcons.delete, - color: theme.colorTheme.accentError, - ), - const SizedBox(width: 16), - Text( - context.translations.toggleDeleteRetryDeleteMessageText( - isDeleteFailed: isDeleteFailed, - ), - style: theme.textTheme.body.copyWith( - color: theme.colorTheme.accentError, - ), - ), - ], - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/edit_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/edit_message_button.dart deleted file mode 100644 index 33165ac8a4..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/edit_message_button.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template editMessageButton} -/// Allows a user to edit a message. -/// -/// Used by [MessageActionsModal]. Should not be used by itself. -/// {@endtemplate} -class EditMessageButton extends StatelessWidget { - /// {@macro editMessageButton} - const EditMessageButton({ - super.key, - required this.onTap, - }); - - /// The callback to perform when the button is tapped. - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon( - icon: StreamSvgIcons.edit, - color: streamChatThemeData.primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - context.translations.editMessageLabel, - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/flag_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/flag_message_button.dart deleted file mode 100644 index 5c57547108..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/flag_message_button.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template flagMessageButton} -/// Allows a user to flag a message. -/// -/// Used by [MessageActionsModal]. Should not be used by itself. -/// {@endtemplate} -class FlagMessageButton extends StatelessWidget { - /// {@macro flagMessageButton} - const FlagMessageButton({ - super.key, - required this.onTap, - }); - - /// The callback to perform when the button is tapped. - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon( - icon: StreamSvgIcons.flag, - color: streamChatThemeData.primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - context.translations.flagMessageLabel, - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/mam_widgets.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/mam_widgets.dart deleted file mode 100644 index e756d682b2..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/mam_widgets.dart +++ /dev/null @@ -1,8 +0,0 @@ -export 'copy_message_button.dart'; -export 'delete_message_button.dart'; -export 'edit_message_button.dart'; -export 'flag_message_button.dart'; -export 'pin_message_button.dart'; -export 'reply_button.dart'; -export 'resend_message_button.dart'; -export 'thread_reply_button.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/mark_unread_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/mark_unread_message_button.dart deleted file mode 100644 index 12c38d0210..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/mark_unread_message_button.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template markUnreadMessageButton} -/// Allows a user to mark message (and all messages onwards) as unread. -/// -/// Used by [MessageActionsModal]. Should not be used by itself. -/// {@endtemplate} -class MarkUnreadMessageButton extends StatelessWidget { - /// {@macro markUnreadMessageButton} - const MarkUnreadMessageButton({ - super.key, - required this.onTap, - }); - - /// The callback to perform when the button is tapped. - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon( - icon: StreamSvgIcons.messageUnread, - color: streamChatThemeData.primaryIconTheme.color, - size: 24, - ), - const SizedBox(width: 16), - Text( - context.translations.markAsUnreadLabel, - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_action.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_action.dart deleted file mode 100644 index f3acaac964..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_action.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/src/utils/typedefs.dart'; - -/// {@template streamMessageAction} -/// Class describing a message action -/// {@endtemplate} -class StreamMessageAction { - /// {@macro streamMessageAction} - StreamMessageAction({ - this.leading, - this.title, - this.onTap, - }); - - /// leading widget - final Widget? leading; - - /// title widget - final Widget? title; - - /// {@macro onMessageTap} - final OnMessageTap? onTap; -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart deleted file mode 100644 index 069c1aea51..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/message_actions_modal.dart +++ /dev/null @@ -1,425 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart' hide ButtonStyle; -import 'package:stream_chat_flutter/src/message_actions_modal/mam_widgets.dart'; -import 'package:stream_chat_flutter/src/message_actions_modal/mark_unread_message_button.dart'; -import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_align.dart'; -import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template messageActionsModal} -/// Constructs a modal with actions for a message -/// {@endtemplate} -class MessageActionsModal extends StatefulWidget { - /// {@macro messageActionsModal} - const MessageActionsModal({ - super.key, - required this.message, - required this.messageWidget, - required this.messageTheme, - this.showReactionPicker = true, - this.showDeleteMessage = true, - this.showEditMessage = true, - this.onReplyTap, - this.onEditMessageTap, - this.onConfirmDeleteTap, - this.onThreadReplyTap, - this.showCopyMessage = true, - this.showReplyMessage = true, - this.showResendMessage = true, - this.showThreadReplyMessage = true, - this.showMarkUnreadMessage = true, - this.showFlagButton = true, - this.showPinButton = true, - this.editMessageInputBuilder, - this.reverse = false, - this.customActions = const [], - this.onCopyTap, - }); - - /// Widget that shows the message - final Widget messageWidget; - - /// Builder for edit message - final EditMessageInputBuilder? editMessageInputBuilder; - - /// The action to perform when "thread reply" is tapped - final OnMessageTap? onThreadReplyTap; - - /// The action to perform when "reply" is tapped - final OnMessageTap? onReplyTap; - - /// The action to perform when "Edit Message" is tapped. - final OnMessageTap? onEditMessageTap; - - /// The action to perform when delete confirmation button is tapped. - final Future Function(Message)? onConfirmDeleteTap; - - /// Message in focus for actions - final Message message; - - /// [StreamMessageThemeData] for message - final StreamMessageThemeData messageTheme; - - /// Flag for showing reaction picker. - final bool showReactionPicker; - - /// Callback when copy is tapped - final OnMessageTap? onCopyTap; - - /// Callback when delete is tapped - final bool showDeleteMessage; - - /// Flag for showing copy action - final bool showCopyMessage; - - /// Flag for showing edit action - final bool showEditMessage; - - /// Flag for showing resend action - final bool showResendMessage; - - /// Flag for showing mark unread action - final bool showMarkUnreadMessage; - - /// Flag for showing reply action - final bool showReplyMessage; - - /// Flag for showing thread reply action - final bool showThreadReplyMessage; - - /// Flag for showing flag action - final bool showFlagButton; - - /// Flag for showing pin action - final bool showPinButton; - - /// Flag for reversing message - final bool reverse; - - /// List of custom actions - final List customActions; - - @override - _MessageActionsModalState createState() => _MessageActionsModalState(); -} - -class _MessageActionsModalState extends State { - bool _showActions = true; - - @override - Widget build(BuildContext context) { - final mediaQueryData = MediaQuery.of(context); - final user = StreamChat.of(context).currentUser; - final orientation = mediaQueryData.orientation; - - final fontSize = widget.messageTheme.messageTextStyle?.fontSize; - final streamChatThemeData = StreamChatTheme.of(context); - - final channel = StreamChannel.of(context).channel; - - final canSendReaction = channel.canSendReaction; - - final child = Center( - child: SingleChildScrollView( - child: SafeArea( - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (widget.showReactionPicker && canSendReaction) - LayoutBuilder( - builder: (context, constraints) { - return Align( - alignment: Alignment( - calculateReactionsHorizontalAlignment( - user, - widget.message, - constraints, - fontSize, - orientation, - ), - 0, - ), - child: StreamReactionPicker( - message: widget.message, - ), - ); - }, - ), - const SizedBox(height: 10), - IgnorePointer( - child: widget.messageWidget, - ), - const SizedBox(height: 8), - Padding( - padding: EdgeInsets.only( - left: widget.reverse ? 0 : 40, - ), - child: SizedBox( - width: mediaQueryData.size.width * 0.75, - child: Material( - color: streamChatThemeData.colorTheme.appBg, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (widget.showReplyMessage && - widget.message.state.isCompleted) - ReplyButton( - onTap: () { - Navigator.of(context).pop(); - if (widget.onReplyTap != null) { - widget.onReplyTap?.call(widget.message); - } - }, - ), - if (widget.showThreadReplyMessage && - (widget.message.state.isCompleted) && - widget.message.parentId == null) - ThreadReplyButton( - message: widget.message, - onThreadReplyTap: widget.onThreadReplyTap, - ), - if (widget.showMarkUnreadMessage) - MarkUnreadMessageButton(onTap: () async { - try { - await channel.markUnread(widget.message.id); - } catch (ex) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.translations.markUnreadError, - ), - ), - ); - } - - Navigator.of(context).pop(); - }), - if (widget.showResendMessage) - ResendMessageButton( - message: widget.message, - channel: channel, - ), - if (widget.showEditMessage) - EditMessageButton( - onTap: switch (widget.onEditMessageTap) { - final onTap? => () => onTap(widget.message), - _ => null, - }, - ), - if (widget.showCopyMessage) - CopyMessageButton( - onTap: () { - widget.onCopyTap?.call(widget.message); - }, - ), - if (widget.showFlagButton) - FlagMessageButton( - onTap: _showFlagDialog, - ), - if (widget.showPinButton) - PinMessageButton( - onTap: _togglePin, - pinned: widget.message.pinned, - ), - if (widget.showDeleteMessage) - DeleteMessageButton( - isDeleteFailed: - widget.message.state.isDeletingFailed, - onTap: _showDeleteBottomSheet, - ), - ...widget.customActions - .map((action) => _buildCustomAction( - context, - action, - )), - ].insertBetween( - Container( - height: 1, - color: streamChatThemeData.colorTheme.borders, - ), - ), - ), - ), - ), - ), - ], - ), - ), - ), - ), - ); - - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => Navigator.of(context).maybePop(), - child: Stack( - children: [ - Positioned.fill( - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 10, - sigmaY: 10, - ), - child: ColoredBox( - color: streamChatThemeData.colorTheme.overlay, - ), - ), - ), - if (_showActions) - TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOutBack, - builder: (context, val, child) => Transform.scale( - scale: val, - child: child, - ), - child: child, - ), - ], - ), - ); - } - - InkWell _buildCustomAction( - BuildContext context, - StreamMessageAction messageAction, - ) { - return InkWell( - onTap: () => messageAction.onTap?.call(widget.message), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - messageAction.leading ?? const Empty(), - const SizedBox(width: 16), - messageAction.title ?? const Empty(), - ], - ), - ), - ); - } - - Future _showFlagDialog() async { - final client = StreamChat.of(context).client; - - final streamChatThemeData = StreamChatTheme.of(context); - final answer = await showConfirmationBottomSheet( - context, - title: context.translations.flagMessageLabel, - icon: StreamSvgIcon( - icon: StreamSvgIcons.flag, - color: streamChatThemeData.colorTheme.accentError, - size: 24, - ), - question: context.translations.flagMessageQuestion, - okText: context.translations.flagLabel, - cancelText: context.translations.cancelLabel, - ); - - final theme = streamChatThemeData; - if (answer == true) { - try { - await client.flagMessage(widget.message.id); - await showInfoBottomSheet( - context, - icon: StreamSvgIcon( - icon: StreamSvgIcons.flag, - color: theme.colorTheme.accentError, - size: 24, - ), - details: context.translations.flagMessageSuccessfulText, - title: context.translations.flagMessageSuccessfulLabel, - okText: context.translations.okLabel, - ); - } catch (err) { - if (err is StreamChatNetworkError && - err.errorCode == ChatErrorCode.inputError) { - await showInfoBottomSheet( - context, - icon: StreamSvgIcon( - icon: StreamSvgIcons.flag, - color: theme.colorTheme.accentError, - size: 24, - ), - details: context.translations.flagMessageSuccessfulText, - title: context.translations.flagMessageSuccessfulLabel, - okText: context.translations.okLabel, - ); - } else { - _showErrorAlertBottomSheet(); - } - } - } - } - - Future _togglePin() async { - final channel = StreamChannel.of(context).channel; - - Navigator.of(context).pop(); - try { - if (!widget.message.pinned) { - await channel.pinMessage(widget.message); - } else { - await channel.unpinMessage(widget.message); - } - } catch (e) { - _showErrorAlertBottomSheet(); - } - } - - /// Shows a "delete message" bottom sheet on mobile platforms. - Future _showDeleteBottomSheet() async { - setState(() => _showActions = false); - final answer = await showConfirmationBottomSheet( - context, - title: context.translations.deleteMessageLabel, - icon: StreamSvgIcon( - icon: StreamSvgIcons.flag, - color: StreamChatTheme.of(context).colorTheme.accentError, - size: 24, - ), - question: context.translations.deleteMessageQuestion, - okText: context.translations.deleteLabel, - cancelText: context.translations.cancelLabel, - ); - - if (answer == true) { - try { - Navigator.of(context).pop(); - final onConfirmDeleteTap = widget.onConfirmDeleteTap; - if (onConfirmDeleteTap != null) { - await onConfirmDeleteTap(widget.message); - } else { - await StreamChannel.of(context).channel.deleteMessage(widget.message); - } - } catch (err) { - _showErrorAlertBottomSheet(); - } - } else { - setState(() => _showActions = true); - } - } - - void _showErrorAlertBottomSheet() { - showInfoBottomSheet( - context, - icon: StreamSvgIcon( - icon: StreamSvgIcons.error, - color: StreamChatTheme.of(context).colorTheme.accentError, - size: 24, - ), - details: context.translations.operationCouldNotBeCompletedText, - title: context.translations.somethingWentWrongError, - okText: context.translations.okLabel, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/moderated_message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/moderated_message_actions_modal.dart deleted file mode 100644 index 6d9669e5d1..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/moderated_message_actions_modal.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/utils/extensions.dart'; - -/// {@template moderatedMessageActionsModal} -/// A modal that is shown when a message is flagged by moderation policies. -/// -/// This modal allows users to: -/// - Send the message anyway, overriding the moderation warning -/// - Edit the message to comply with community guidelines -/// - Delete the message -/// -/// The modal provides clear guidance to users about the moderation issue -/// and options to address it. -/// {@endtemplate} -class ModeratedMessageActionsModal extends StatelessWidget { - /// {@macro moderatedMessageActionsModal} - const ModeratedMessageActionsModal({ - super.key, - this.onSendAnyway, - this.onEditMessage, - this.onDeleteMessage, - }); - - /// Callback function called when the user chooses to send the message - /// despite the moderation warning. - final VoidCallback? onSendAnyway; - - /// Callback function called when the user chooses to edit the message. - final VoidCallback? onEditMessage; - - /// Callback function called when the user chooses to delete the message. - final VoidCallback? onDeleteMessage; - - @override - Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); - final textTheme = theme.textTheme; - final colorTheme = theme.colorTheme; - - final actions = [ - TextButton( - onPressed: onSendAnyway, - style: TextButton.styleFrom( - textStyle: theme.textTheme.body, - foregroundColor: theme.colorTheme.accentPrimary, - disabledForegroundColor: theme.colorTheme.disabled, - ), - child: Text(context.translations.sendAnywayLabel), - ), - TextButton( - onPressed: onEditMessage, - style: TextButton.styleFrom( - textStyle: theme.textTheme.body, - foregroundColor: theme.colorTheme.accentPrimary, - disabledForegroundColor: theme.colorTheme.disabled, - ), - child: Text(context.translations.editMessageLabel), - ), - TextButton( - onPressed: onDeleteMessage, - style: TextButton.styleFrom( - textStyle: theme.textTheme.body, - foregroundColor: theme.colorTheme.accentPrimary, - disabledForegroundColor: theme.colorTheme.disabled, - ), - child: Text(context.translations.deleteMessageLabel), - ), - ]; - - return BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: AlertDialog( - clipBehavior: Clip.antiAlias, - backgroundColor: colorTheme.appBg, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - icon: const StreamSvgIcon(icon: StreamSvgIcons.flag), - iconColor: colorTheme.accentPrimary, - title: Text(context.translations.moderationReviewModalTitle), - titleTextStyle: textTheme.headline.copyWith( - color: colorTheme.textHighEmphasis, - ), - content: Text( - context.translations.moderationReviewModalDescription, - textAlign: TextAlign.center, - ), - contentTextStyle: textTheme.body.copyWith( - color: colorTheme.textLowEmphasis, - ), - actions: actions, - actionsAlignment: MainAxisAlignment.center, - actionsOverflowAlignment: OverflowBarAlignment.center, - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/pin_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/pin_message_button.dart deleted file mode 100644 index 07313065ae..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/pin_message_button.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template pinMessageButton} -/// Allows a user to pin or unpin a message. -/// -/// Used by [MessageActionsModal]. Should not be used by itself. -/// {@endtemplate} -class PinMessageButton extends StatelessWidget { - /// {@macro pinMessageButton} - const PinMessageButton({ - super.key, - required this.onTap, - required this.pinned, - }); - - /// The callback to perform when the button is tapped. - final VoidCallback onTap; - - /// Whether the selected message is currently pinned or not. - final bool pinned; - - @override - Widget build(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon( - icon: StreamSvgIcons.pin, - color: streamChatThemeData.primaryIconTheme.color, - size: 24, - ), - const SizedBox(width: 16), - Text( - context.translations.togglePinUnpinText( - pinned: pinned, - ), - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/reply_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/reply_button.dart deleted file mode 100644 index b4340d8f96..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/reply_button.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template replyButton} -/// Allows a user to reply to a message. -/// -/// Used by [MessageActionsModal]. Should not be used by itself. -/// {@endtemplate} -class ReplyButton extends StatelessWidget { - /// {@macro replyButton} - const ReplyButton({ - super.key, - required this.onTap, - }); - - /// The callback to perform when the button is tapped. - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon( - icon: StreamSvgIcons.reply, - color: streamChatThemeData.primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - context.translations.replyLabel, - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/resend_message_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/resend_message_button.dart deleted file mode 100644 index 8c94c621ad..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/resend_message_button.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template resendMessageButton} -/// Allows a user to resend a message that has failed to be sent. -/// -/// Used by [MessageActionsModal]. Should not be used by itself. -/// {@endtemplate} -class ResendMessageButton extends StatelessWidget { - /// {@macro resendMessageButton} - const ResendMessageButton({ - super.key, - required this.message, - required this.channel, - }); - - /// The message to resend. - final Message message; - - /// The [StreamChannel] above this widget. - final Channel channel; - - @override - Widget build(BuildContext context) { - final isUpdateFailed = message.state.isUpdatingFailed; - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: () { - Navigator.of(context).pop(); - channel.retryMessage(message); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon( - icon: StreamSvgIcons.circleUp, - color: streamChatThemeData.colorTheme.accentPrimary, - ), - const SizedBox(width: 16), - Text( - context.translations.toggleResendOrResendEditedMessage( - isUpdateFailed: isUpdateFailed, - ), - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal/thread_reply_button.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal/thread_reply_button.dart deleted file mode 100644 index f5ffb4a357..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal/thread_reply_button.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template threadReplyButton} -/// Allows a user to start a thread reply to a message. -/// -/// Used by [MessageActionsModal]. Should not be used by itself. -/// {@endtemplate} -class ThreadReplyButton extends StatelessWidget { - /// {@macro threadReplyButton} - const ThreadReplyButton({ - super.key, - required this.message, - this.onThreadReplyTap, - }); - - /// The message to start a thread reply to. - final Message message; - - /// The action to perform when "thread reply" is tapped - final OnMessageTap? onThreadReplyTap; - - @override - Widget build(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - return InkWell( - onTap: () { - Navigator.of(context).pop(); - if (onThreadReplyTap != null) { - onThreadReplyTap?.call(message); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16), - child: Row( - children: [ - StreamSvgIcon( - icon: StreamSvgIcons.threadReply, - color: streamChatThemeData.primaryIconTheme.color, - ), - const SizedBox(width: 16), - Text( - context.translations.threadReplyLabel, - style: streamChatThemeData.textTheme.body, - ), - ], - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_file_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_file_picker.dart index c410532d2d..396af5ace3 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_file_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_file_picker.dart @@ -19,7 +19,7 @@ class StreamFilePicker extends StatelessWidget { this.type = FileType.any, this.allowedExtensions, this.onFileLoading, - this.allowCompression = true, + this.compressionQuality = 0, this.withData = false, this.withReadStream = false, this.lockParentWindow = false, @@ -43,8 +43,8 @@ class StreamFilePicker extends StatelessWidget { /// Callback called when the file picker is loading a file. final Function(FilePickerStatus)? onFileLoading; - /// Whether to allow compression of the file. - final bool allowCompression; + /// The compression quality for the file. + final int compressionQuality; /// Whether to include the file data in the [Attachment]. final bool withData; @@ -73,7 +73,7 @@ class StreamFilePicker extends StatelessWidget { type: type, allowedExtensions: allowedExtensions, onFileLoading: onFileLoading, - allowCompression: allowCompression, + compressionQuality: compressionQuality, withData: withData, withReadStream: withReadStream, lockParentWindow: lockParentWindow, diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart index 905df5f48c..c2914d2007 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_gallery_picker.dart @@ -6,6 +6,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/message_input/attachment_picker/stream_attachment_picker.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_picker/stream_attachment_picker_controller.dart'; import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/photo_gallery/stream_photo_gallery.dart'; import 'package:stream_chat_flutter/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart'; @@ -14,7 +15,7 @@ import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Max image resolution which can be resized by the CDN. -// Taken from https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing +/// Taken from https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing const maxCDNImageResolution = 16800000; /// Widget used to pick media from the device gallery. @@ -23,42 +24,23 @@ class StreamGalleryPicker extends StatefulWidget { const StreamGalleryPicker({ super.key, this.limit = 50, + GalleryPickerConfig? config, required this.selectedMediaItems, required this.onMediaItemSelected, - this.mediaThumbnailSize = const ThumbnailSize(400, 400), - this.mediaThumbnailFormat = ThumbnailFormat.jpeg, - this.mediaThumbnailQuality = 100, - this.mediaThumbnailScale = 1, - }); + }) : config = config ?? const GalleryPickerConfig(); /// Maximum number of media items that can be selected. final int limit; + /// Configuration for the gallery picker. + final GalleryPickerConfig config; + /// List of selected media items. final Iterable selectedMediaItems; /// Callback called when an media item is selected. final ValueSetter onMediaItemSelected; - /// Size of the attachment thumbnails. - /// - /// Defaults to (400, 400). - final ThumbnailSize mediaThumbnailSize; - - /// Format of the attachment thumbnails. - /// - /// Defaults to [ThumbnailFormat.jpeg]. - final ThumbnailFormat mediaThumbnailFormat; - - /// The quality value for the attachment thumbnails. - /// - /// Valid from 1 to 100. - /// Defaults to 100. - final int mediaThumbnailQuality; - - /// The scale to apply on the [attachmentThumbnailSize]. - final double mediaThumbnailScale; - @override State createState() => _StreamGalleryPickerState(); } @@ -159,10 +141,10 @@ class _StreamGalleryPickerState extends State { onMediaTap: widget.onMediaItemSelected, loadMoreTriggerIndex: 10, padding: const EdgeInsets.all(2), - thumbnailSize: widget.mediaThumbnailSize, - thumbnailFormat: widget.mediaThumbnailFormat, - thumbnailQuality: widget.mediaThumbnailQuality, - thumbnailScale: widget.mediaThumbnailScale, + thumbnailSize: widget.config.mediaThumbnailSize, + thumbnailFormat: widget.config.mediaThumbnailFormat, + thumbnailQuality: widget.config.mediaThumbnailQuality, + thumbnailScale: widget.config.mediaThumbnailScale, itemBuilder: (context, mediaItems, index, defaultWidget) { final media = mediaItems[index]; return defaultWidget.copyWith( @@ -178,6 +160,29 @@ class _StreamGalleryPickerState extends State { } } +/// Configuration for the [StreamGalleryPicker]. +class GalleryPickerConfig { + /// Creates a [GalleryPickerConfig] instance. + const GalleryPickerConfig({ + this.mediaThumbnailSize = const ThumbnailSize(400, 400), + this.mediaThumbnailFormat = ThumbnailFormat.jpeg, + this.mediaThumbnailQuality = 100, + this.mediaThumbnailScale = 1, + }); + + /// Size of the attachment thumbnails. + final ThumbnailSize mediaThumbnailSize; + + /// Format of the attachment thumbnails. + final ThumbnailFormat mediaThumbnailFormat; + + /// The quality value for the attachment thumbnails. + final int mediaThumbnailQuality; + + /// The scale to apply on the [mediaThumbnailSize]. + final double mediaThumbnailScale; +} + /// extension StreamImagePickerX on StreamAttachmentPickerController { /// diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart index 97f78ae088..f408b628bc 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker.dart @@ -1,400 +1,64 @@ import 'dart:async'; -import 'package:collection/collection.dart'; +import 'package:file_picker/file_picker.dart' show FileType; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/message_input/attachment_picker/options/options.dart'; import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// The default maximum size for media attachments. -const kDefaultMaxAttachmentSize = 100 * 1024 * 1024; // 100MB in Bytes - -/// The default maximum number of media attachments. -const kDefaultMaxAttachmentCount = 10; - -/// Value class for [AttachmentPickerController]. -/// -/// This class holds the list of [Poll] and [Attachment] objects. -class AttachmentPickerValue { - /// Creates a new instance of [AttachmentPickerValue]. - const AttachmentPickerValue({ - this.poll, - this.attachments = const [], - }); - - /// The poll object. - final Poll? poll; - - /// The list of [Attachment] objects. - final List attachments; - - /// Returns a copy of this object with the provided values. - AttachmentPickerValue copyWith({ - Poll? poll, - List? attachments, - }) { - return AttachmentPickerValue( - poll: poll ?? this.poll, - attachments: attachments ?? this.attachments, - ); - } -} - -/// Controller class for [StreamAttachmentPicker]. -class StreamAttachmentPickerController - extends ValueNotifier { - /// Creates a new instance of [StreamAttachmentPickerController]. - StreamAttachmentPickerController({ - this.initialPoll, - this.initialAttachments, - this.maxAttachmentSize = kDefaultMaxAttachmentSize, - this.maxAttachmentCount = kDefaultMaxAttachmentCount, - }) : assert( - (initialAttachments?.length ?? 0) <= maxAttachmentCount, - '''The initial attachments count must be less than or equal to maxAttachmentCount''', - ), - super( - AttachmentPickerValue( - poll: initialPoll, - attachments: initialAttachments ?? const [], - ), - ); - - /// The max attachment size allowed in bytes. - final int maxAttachmentSize; - - /// The max attachment count allowed. - final int maxAttachmentCount; - - /// The initial poll. - final Poll? initialPoll; - - /// The initial attachments. - final List? initialAttachments; - - @override - set value(AttachmentPickerValue newValue) { - if (newValue.attachments.length > maxAttachmentCount) { - throw ArgumentError( - 'The maximum number of attachments is $maxAttachmentCount.', - ); - } - super.value = newValue; - } - - /// Adds a new [poll] to the message. - set poll(Poll poll) { - value = value.copyWith(poll: poll); - } - - Future _saveToCache(AttachmentFile file) async { - // Cache the attachment in a temporary file. - return StreamAttachmentHandler.instance.saveAttachmentFile( - attachmentFile: file, - ); - } - - Future _removeFromCache(AttachmentFile file) { - // Remove the cached attachment file. - return StreamAttachmentHandler.instance.deleteAttachmentFile( - attachmentFile: file, - ); - } - - /// Adds a new attachment to the message. - Future addAttachment(Attachment attachment) async { - assert(attachment.fileSize != null, ''); - if (attachment.fileSize! > maxAttachmentSize) { - throw ArgumentError( - 'The size of the attachment is ${attachment.fileSize} bytes, ' - 'but the maximum size allowed is $maxAttachmentSize bytes.', - ); - } - - final file = attachment.file; - final uploadState = attachment.uploadState; - - // No need to cache the attachment if it's already uploaded - // or we are on web. - if (file == null || uploadState.isSuccess || isWeb) { - value = value.copyWith(attachments: [...value.attachments, attachment]); - return; - } - - // Cache the attachment in a temporary file. - final tempFilePath = await _saveToCache(file); - - value = value.copyWith(attachments: [ - ...value.attachments, - attachment.copyWith( - file: file.copyWith( - path: tempFilePath, - ), - ), - ]); - } - - /// Removes the specified [attachment] from the message. - Future removeAttachment(Attachment attachment) async { - final file = attachment.file; - final uploadState = attachment.uploadState; - - if (file == null || uploadState.isSuccess || isWeb) { - final updatedAttachments = [...value.attachments]..remove(attachment); - value = value.copyWith(attachments: updatedAttachments); - return; - } - - // Remove the cached attachment file. - await _removeFromCache(file); - - final updatedAttachments = [...value.attachments]..remove(attachment); - value = value.copyWith(attachments: updatedAttachments); - } - - /// Remove the attachment with the given [attachmentId]. - void removeAttachmentById(String attachmentId) { - final attachment = value.attachments.firstWhereOrNull( - (attachment) => attachment.id == attachmentId, - ); - - if (attachment == null) return; - - removeAttachment(attachment); - } - - /// Clears all the attachments. - Future clear() async { - final attachments = [...value.attachments]; - for (final attachment in attachments) { - final file = attachment.file; - final uploadState = attachment.uploadState; - - if (file == null || uploadState.isSuccess || isWeb) continue; - - await _removeFromCache(file); - } - value = const AttachmentPickerValue(); - } - - /// Resets the controller to its initial state. - Future reset() async { - final attachments = [...value.attachments]; - for (final attachment in attachments) { - final file = attachment.file; - final uploadState = attachment.uploadState; - - if (file == null || uploadState.isSuccess || isWeb) continue; - - await _removeFromCache(file); - } - - value = AttachmentPickerValue( - poll: initialPoll, - attachments: initialAttachments ?? const [], - ); - } -} - -/// The possible picker types of the attachment picker. -enum AttachmentPickerType { - /// The attachment picker will only allow to pick images. - images, - - /// The attachment picker will only allow to pick videos. - videos, - - /// The attachment picker will only allow to pick audios. - audios, - - /// The attachment picker will only allow to pick files or documents. - files, - - /// The attachment picker will only allow to create poll. - poll, -} - -/// Function signature for building the attachment picker option view. -typedef AttachmentPickerOptionViewBuilder = Widget Function( - BuildContext context, - StreamAttachmentPickerController controller, -); - -/// Model class for the attachment picker options. -class AttachmentPickerOption { - /// Creates a new instance of [AttachmentPickerOption]. - const AttachmentPickerOption({ - this.key, - required this.supportedTypes, - required this.icon, - this.title, - this.optionViewBuilder, - }); - - /// A key to identify the option. - final String? key; - - /// The icon of the option. - final Widget icon; - - /// The title of the option. - final String? title; - - /// The supported types of the option. - final Iterable supportedTypes; - - /// The option view builder. - final AttachmentPickerOptionViewBuilder? optionViewBuilder; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is AttachmentPickerOption && - runtimeType == other.runtimeType && - key == other.key && - const IterableEquality().equals(supportedTypes, other.supportedTypes); - - @override - int get hashCode => - key.hashCode ^ const IterableEquality().hash(supportedTypes); -} - -/// The attachment picker option for the web or desktop platforms. -class WebOrDesktopAttachmentPickerOption extends AttachmentPickerOption { - /// Creates a new instance of [WebOrDesktopAttachmentPickerOption]. - WebOrDesktopAttachmentPickerOption({ - super.key, - required AttachmentPickerType type, - required super.icon, - required super.title, - }) : super(supportedTypes: [type]); - - /// Creates a new instance of [WebOrDesktopAttachmentPickerOption] from - /// [option]. - factory WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption( - AttachmentPickerOption option, - ) { - return WebOrDesktopAttachmentPickerOption( - key: option.key, - type: option.supportedTypes.first, - icon: option.icon, - title: option.title, - ); - } - - @override - String get title => super.title!; - - /// Type of the option. - AttachmentPickerType get type => supportedTypes.first; -} - -/// Helpful extensions for [StreamAttachmentPickerController]. -extension AttachmentPickerOptionTypeX on StreamAttachmentPickerController { - /// Returns the list of available attachment picker options. - Set get currentAttachmentPickerTypes { - final attach = value.attachments; - final containsImage = attach.any((it) => it.type == AttachmentType.image); - final containsVideo = attach.any((it) => it.type == AttachmentType.video); - final containsAudio = attach.any((it) => it.type == AttachmentType.audio); - final containsFile = attach.any((it) => it.type == AttachmentType.file); - final containsPoll = value.poll != null; - - return { - if (containsImage) AttachmentPickerType.images, - if (containsVideo) AttachmentPickerType.videos, - if (containsAudio) AttachmentPickerType.audios, - if (containsFile) AttachmentPickerType.files, - if (containsPoll) AttachmentPickerType.poll, - }; - } - - /// Returns the list of enabled picker types. - Set filterEnabledTypes({ - required Iterable options, - }) { - final availableTypes = currentAttachmentPickerTypes; - final enabledTypes = {}; - for (final option in options) { - final supportedTypes = option.supportedTypes; - if (availableTypes.any(supportedTypes.contains)) { - enabledTypes.addAll(supportedTypes); - } - } - return enabledTypes; - } - - /// Returns true if the [initialAttachments] are changed. - bool get isValueChanged { - final isPollEqual = value.poll == initialPoll; - final areAttachmentsEqual = UnorderedIterableEquality( - EqualityBy((attachment) => attachment.id), - ).equals(value.attachments, initialAttachments); - - return !isPollEqual || !areAttachmentsEqual; - } -} - -/// Function signature for the callback when the web or desktop attachment -/// picker option gets tapped. -typedef OnWebOrDesktopAttachmentPickerOptionTap = void Function( - BuildContext context, - StreamAttachmentPickerController controller, - WebOrDesktopAttachmentPickerOption option, -); - -/// Bottom sheet widget for the web or desktop version of the attachment picker. -class StreamWebOrDesktopAttachmentPickerBottomSheet extends StatelessWidget { - /// Creates a new instance of [StreamWebOrDesktopAttachmentPickerBottomSheet]. - const StreamWebOrDesktopAttachmentPickerBottomSheet({ +/// Bottom sheet widget for the system attachment picker interface. +/// This is used when the attachment picker uses system integration, +/// typically on web/desktop or when useSystemAttachmentPicker is true. +class StreamSystemAttachmentPickerBottomSheet extends StatelessWidget { + /// Creates a new instance of [StreamSystemAttachmentPickerBottomSheet]. + const StreamSystemAttachmentPickerBottomSheet({ super.key, required this.options, required this.controller, - this.onOptionTap, }); /// The list of options. - final Set options; + final Set options; /// The controller of the attachment picker. final StreamAttachmentPickerController controller; - /// The callback when the option gets tapped. - final OnWebOrDesktopAttachmentPickerOptionTap? onOptionTap; - @override Widget build(BuildContext context) { - final enabledTypes = controller.filterEnabledTypes(options: options); - return ListView( - shrinkWrap: true, - children: [ - ...options.map((option) { - VoidCallback? onOptionTap; - if (this.onOptionTap != null) { - onOptionTap = () { - this.onOptionTap?.call(context, controller, option); - }; - } - - final enabled = enabledTypes.isEmpty || - enabledTypes.any((it) => it == option.type); - - return ListTile( - enabled: enabled, - leading: option.icon, - title: Text(option.title), - onTap: onOptionTap, - ); - }), - ], + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + final enabledTypes = value.filterEnabledTypes(options: options); + + return ListView( + shrinkWrap: true, + children: [ + ...options.map( + (option) { + final supported = option.supportedTypes; + final isEnabled = enabledTypes.any(supported.contains); + + return ListTile( + enabled: isEnabled, + leading: option.icon, + title: Text(option.title), + onTap: () => option.onTap(context, controller), + ); + }, + ), + ], + ); + }, ); } } -/// Bottom sheet widget for the mobile version of the attachment picker. -class StreamMobileAttachmentPickerBottomSheet extends StatefulWidget { - /// Creates a new instance of [StreamMobileAttachmentPickerBottomSheet]. - const StreamMobileAttachmentPickerBottomSheet({ +/// Bottom sheet widget for the tabbed attachment picker interface. +/// This is used when the attachment picker displays a tabbed interface, +/// typically on mobile when useSystemAttachmentPicker is false. +class StreamTabbedAttachmentPickerBottomSheet extends StatefulWidget { + /// Creates a new instance of [StreamTabbedAttachmentPickerBottomSheet]. + const StreamTabbedAttachmentPickerBottomSheet({ super.key, required this.options, required this.controller, @@ -403,54 +67,49 @@ class StreamMobileAttachmentPickerBottomSheet extends StatefulWidget { }); /// The list of options. - final Set options; + final Set options; /// The initial option to be selected. - final AttachmentPickerOption? initialOption; + final TabbedAttachmentPickerOption? initialOption; /// The controller of the attachment picker. final StreamAttachmentPickerController controller; /// The callback when the send button gets tapped. - final ValueSetter? onSendValue; + final ValueSetter? onSendValue; @override - State createState() => - _StreamMobileAttachmentPickerBottomSheetState(); + State createState() => + _StreamTabbedAttachmentPickerBottomSheetState(); } -class _StreamMobileAttachmentPickerBottomSheetState - extends State { - late AttachmentPickerOption _currentOption; +class _StreamTabbedAttachmentPickerBottomSheetState + extends State { + // The current option selected in the tabbed attachment picker. + late var _currentOption = _calculateInitialOption(); + TabbedAttachmentPickerOption _calculateInitialOption() { + if (widget.initialOption case final option?) return option; - @override - void initState() { - super.initState(); - if (widget.initialOption == null) { - final enabledTypes = widget.controller.filterEnabledTypes( - options: widget.options, - ); - if (enabledTypes.isNotEmpty) { - _currentOption = widget.options.firstWhere((it) { - return it.supportedTypes.contains(enabledTypes.first); - }); - } else { - _currentOption = widget.options.first; - } - } else { - _currentOption = widget.initialOption!; - } + final options = widget.options; + final currentValue = widget.controller.value; + final enabledTypes = currentValue.filterEnabledTypes(options: options); + + if (enabledTypes.isEmpty) return options.first; + + return options.firstWhere( + (it) => enabledTypes.any(it.supportedTypes.contains), + ); } @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: widget.controller, - builder: (context, attachments, _) { + builder: (context, value, _) { return Column( mainAxisSize: MainAxisSize.min, children: [ - _AttachmentPickerOptions( + _TabbedAttachmentPickerOptions( controller: widget.controller, options: widget.options, currentOption: _currentOption, @@ -460,9 +119,10 @@ class _StreamMobileAttachmentPickerBottomSheetState }, ), Expanded( - child: _currentOption.optionViewBuilder - ?.call(context, widget.controller) ?? - const Empty(), + child: _currentOption.optionViewBuilder( + context, + widget.controller, + ), ), ], ); @@ -471,8 +131,8 @@ class _StreamMobileAttachmentPickerBottomSheetState } } -class _AttachmentPickerOptions extends StatelessWidget { - const _AttachmentPickerOptions({ +class _TabbedAttachmentPickerOptions extends StatelessWidget { + const _TabbedAttachmentPickerOptions({ required this.options, required this.currentOption, required this.controller, @@ -480,19 +140,20 @@ class _AttachmentPickerOptions extends StatelessWidget { this.onSendValue, }); - final Iterable options; - final AttachmentPickerOption currentOption; + final Iterable options; + final TabbedAttachmentPickerOption currentOption; final StreamAttachmentPickerController controller; - final ValueSetter? onOptionSelected; - final ValueSetter? onSendValue; + final ValueSetter? onOptionSelected; + final ValueSetter? onSendValue; @override Widget build(BuildContext context) { final colorTheme = StreamChatTheme.of(context).colorTheme; return ValueListenableBuilder( valueListenable: controller, - builder: (context, attachments, __) { - final enabledTypes = controller.filterEnabledTypes(options: options); + builder: (context, value, __) { + final enabledTypes = value.filterEnabledTypes(options: options); + return Row( children: [ Expanded( @@ -502,18 +163,19 @@ class _AttachmentPickerOptions extends StatelessWidget { children: [ ...options.map( (option) { - final supportedTypes = option.supportedTypes; - + final supported = option.supportedTypes; + final isEnabled = enabledTypes.any(supported.contains); final isSelected = option == currentOption; - final isEnabled = enabledTypes.isEmpty || - enabledTypes.any(supportedTypes.contains); - final color = isSelected - ? colorTheme.accentPrimary - : colorTheme.textLowEmphasis; + final color = switch (isSelected) { + true => colorTheme.accentPrimary, + _ => colorTheme.textLowEmphasis, + }; - final onPressed = - isEnabled ? () => onOptionSelected!(option) : null; + final onPressed = switch (isEnabled) { + true => () => onOptionSelected?.call(option), + _ => null, + }; return IconButton( color: color, @@ -529,12 +191,18 @@ class _AttachmentPickerOptions extends StatelessWidget { ), Builder( builder: (context) { - final VoidCallback? onPressed; - if (onSendValue != null && controller.isValueChanged) { - onPressed = () => onSendValue!(attachments); - } else { - onPressed = null; - } + final initialValue = controller.initialValue; + final isValueChanged = value != initialValue; + + final onPressed = switch (onSendValue) { + final onSendValue? when isValueChanged => () { + final result = AttachmentsPicked( + attachments: value.attachments, + ); + return onSendValue(result); + }, + _ => null, + }; return IconButton( color: colorTheme.accentPrimary, @@ -744,42 +412,62 @@ class OptionDrawer extends StatelessWidget { } } -/// Returns the mobile version of the attachment picker. -Widget mobileAttachmentPickerBuilder({ +/// Builds a tabbed attachment picker with custom interfaces for different +/// attachment types. +/// +/// Shows horizontal tabs for gallery, files, camera, video, and polls. Each +/// tab displays a specialized interface for selecting that type of +/// attachment. Tabs get enabled or disabled based on what you've already +/// selected. +/// +/// This is the default interface for mobile platforms. Configure with +/// [customOptions], [galleryPickerConfig], [pollConfig], and +/// [allowedTypes]. +Widget tabbedAttachmentPickerBuilder({ required BuildContext context, required StreamAttachmentPickerController controller, - Poll? initialPoll, PollConfig? pollConfig, - Iterable? customOptions, + GalleryPickerConfig? galleryPickerConfig, + Iterable? customOptions, List allowedTypes = AttachmentPickerType.values, - ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400), - ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg, - int attachmentThumbnailQuality = 100, - double attachmentThumbnailScale = 1, - ErrorListener? onError, }) { - return StreamMobileAttachmentPickerBottomSheet( + Future _handleSingePick( + StreamAttachmentPickerController controller, + Attachment? attachment, + ) async { + try { + if (attachment != null) await controller.addAttachment(attachment); + return AttachmentsPicked(attachments: controller.value.attachments); + } catch (error, stk) { + return AttachmentPickerError(error: error, stackTrace: stk); + } + } + + return StreamTabbedAttachmentPickerBottomSheet( controller: controller, onSendValue: Navigator.of(context).pop, options: { ...{ - if (customOptions != null) ...customOptions, - AttachmentPickerOption( + TabbedAttachmentPickerOption( key: 'gallery-picker', icon: const StreamSvgIcon(icon: StreamSvgIcons.pictures), supportedTypes: [ AttachmentPickerType.images, AttachmentPickerType.videos, ], + isEnabled: (value) { + // Enable if nothing has been selected yet. + if (value.isEmpty) return true; + + // Otherwise, enable only if there is at least a image or a video. + return value.attachments.any((it) => it.isImage || it.isVideo); + }, optionViewBuilder: (context, controller) { final attachment = controller.value.attachments; final selectedIds = attachment.map((it) => it.id); return StreamGalleryPicker( + config: galleryPickerConfig, selectedMediaItems: selectedIds, - mediaThumbnailSize: attachmentThumbnailSize, - mediaThumbnailFormat: attachmentThumbnailFormat, - mediaThumbnailQuality: attachmentThumbnailQuality, - mediaThumbnailScale: attachmentThumbnailScale, onMediaItemSelected: (media) async { try { if (selectedIds.contains(media.id)) { @@ -787,178 +475,186 @@ Widget mobileAttachmentPickerBuilder({ } return await controller.addAssetAttachment(media); } catch (e, stk) { - if (onError != null) return onError.call(e, stk); - rethrow; + final err = AttachmentPickerError(error: e, stackTrace: stk); + return Navigator.pop(context, err); } }, ); }, ), - AttachmentPickerOption( + TabbedAttachmentPickerOption( key: 'file-picker', icon: const StreamSvgIcon(icon: StreamSvgIcons.files), supportedTypes: [AttachmentPickerType.files], - optionViewBuilder: (context, controller) { - return StreamFilePicker( - onFilePicked: (file) async { - try { - if (file != null) await controller.addAttachment(file); - return Navigator.pop(context, controller.value); - } catch (e, stk) { - Navigator.pop(context, controller.value); - if (onError != null) return onError.call(e, stk); + isEnabled: (value) { + // Enable if nothing has been selected yet. + if (value.isEmpty) return true; - rethrow; - } - }, - ); + // Otherwise, enable only if there is at least a file. + return value.attachments.any((it) => it.isFile); }, + optionViewBuilder: (context, controller) => StreamFilePicker( + onFilePicked: (file) async { + final result = await _handleSingePick(controller, file); + return Navigator.pop(context, result); + }, + ), ), - AttachmentPickerOption( + TabbedAttachmentPickerOption( key: 'image-picker', icon: const StreamSvgIcon(icon: StreamSvgIcons.camera), supportedTypes: [AttachmentPickerType.images], - optionViewBuilder: (context, controller) { - return StreamImagePicker( - onImagePicked: (image) async { - try { - if (image != null) { - await controller.addAttachment(image); - } - return Navigator.pop(context, controller.value); - } catch (e, stk) { - Navigator.pop(context, controller.value); - if (onError != null) return onError.call(e, stk); + isEnabled: (value) { + // Enable if nothing has been selected yet. + if (value.isEmpty) return true; - rethrow; - } - }, - ); + // Otherwise, enable only if there is at least a image. + return value.attachments.any((it) => it.isImage); }, + optionViewBuilder: (context, controller) => StreamImagePicker( + onImagePicked: (image) async { + final result = await _handleSingePick(controller, image); + return Navigator.pop(context, result); + }, + ), ), - AttachmentPickerOption( + TabbedAttachmentPickerOption( key: 'video-picker', icon: const StreamSvgIcon(icon: StreamSvgIcons.record), supportedTypes: [AttachmentPickerType.videos], - optionViewBuilder: (context, controller) { - return StreamVideoPicker( - onVideoPicked: (video) async { - try { - if (video != null) { - await controller.addAttachment(video); - } - return Navigator.pop(context, controller.value); - } catch (e, stk) { - Navigator.pop(context, controller.value); - if (onError != null) return onError.call(e, stk); + isEnabled: (value) { + // Enable if nothing has been selected yet. + if (value.isEmpty) return true; - rethrow; - } - }, - ); + // Otherwise, enable only if there is at least a video. + return value.attachments.any((it) => it.isVideo); }, + optionViewBuilder: (context, controller) => StreamVideoPicker( + onVideoPicked: (video) async { + final result = await _handleSingePick(controller, video); + return Navigator.pop(context, result); + }, + ), ), - AttachmentPickerOption( + TabbedAttachmentPickerOption( key: 'poll-creator', icon: const StreamSvgIcon(icon: StreamSvgIcons.polls), supportedTypes: [AttachmentPickerType.poll], + isEnabled: (value) { + // Enable if nothing has been selected yet. + if (value.isEmpty) return true; + + // Otherwise, enable only if there is a poll. + return value.poll != null; + }, optionViewBuilder: (context, controller) { final initialPoll = controller.value.poll; return StreamPollCreator( poll: initialPoll, config: pollConfig, onPollCreated: (poll) { - try { - if (poll != null) controller.poll = poll; - return Navigator.pop(context, controller.value); - } catch (e, stk) { - Navigator.pop(context, controller.value); - if (onError != null) return onError.call(e, stk); + if (poll == null) return Navigator.pop(context); + controller.poll = poll; - rethrow; - } + final result = PollCreated(poll: poll); + return Navigator.pop(context, result); }, ); }, ), + ...?customOptions, }.where((option) => option.supportedTypes.every(allowedTypes.contains)), }, ); } -/// Returns the web or desktop version of the attachment picker. -Widget webOrDesktopAttachmentPickerBuilder({ +/// Builds a system attachment picker that opens native platform dialogs. +/// +/// Shows a simple list of options that immediately launch your device's +/// built-in file browser, camera app, or other native tools instead of +/// custom interfaces. +/// +/// This is the default for web and desktop platforms, and can be enabled on +/// mobile with `useSystemAttachmentPicker`. Configure with [customOptions], +/// [pollConfig], and [allowedTypes]. +Widget systemAttachmentPickerBuilder({ required BuildContext context, required StreamAttachmentPickerController controller, - Poll? initialPoll, - PollConfig? pollConfig, - Iterable? customOptions, + PollConfig? pollConfig = const PollConfig(), + GalleryPickerConfig? galleryPickerConfig = const GalleryPickerConfig(), + Iterable? customOptions, List allowedTypes = AttachmentPickerType.values, - ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400), - ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg, - int attachmentThumbnailQuality = 100, - double attachmentThumbnailScale = 1, - ErrorListener? onError, }) { - return StreamWebOrDesktopAttachmentPickerBottomSheet( + Future _pickSystemFile( + StreamAttachmentPickerController controller, + FileType type, + ) async { + try { + final file = await StreamAttachmentHandler.instance.pickFile(type: type); + if (file != null) await controller.addAttachment(file); + + return AttachmentsPicked(attachments: controller.value.attachments); + } catch (error, stk) { + return AttachmentPickerError(error: error, stackTrace: stk); + } + } + + return StreamSystemAttachmentPickerBottomSheet( controller: controller, options: { ...{ - if (customOptions != null) ...customOptions, - WebOrDesktopAttachmentPickerOption( + SystemAttachmentPickerOption( key: 'image-picker', - type: AttachmentPickerType.images, + supportedTypes: [AttachmentPickerType.images], icon: const StreamSvgIcon(icon: StreamSvgIcons.pictures), title: context.translations.uploadAPhotoLabel, + onTap: (context, controller) async { + final result = await _pickSystemFile(controller, FileType.image); + return Navigator.pop(context, result); + }, ), - WebOrDesktopAttachmentPickerOption( + SystemAttachmentPickerOption( key: 'video-picker', - type: AttachmentPickerType.videos, + supportedTypes: [AttachmentPickerType.videos], icon: const StreamSvgIcon(icon: StreamSvgIcons.record), title: context.translations.uploadAVideoLabel, + onTap: (context, controller) async { + final result = await _pickSystemFile(controller, FileType.video); + return Navigator.pop(context, result); + }, ), - WebOrDesktopAttachmentPickerOption( + SystemAttachmentPickerOption( key: 'file-picker', - type: AttachmentPickerType.files, + supportedTypes: [AttachmentPickerType.files], icon: const StreamSvgIcon(icon: StreamSvgIcons.files), title: context.translations.uploadAFileLabel, + onTap: (context, controller) async { + final result = await _pickSystemFile(controller, FileType.any); + return Navigator.pop(context, result); + }, ), - WebOrDesktopAttachmentPickerOption( + SystemAttachmentPickerOption( key: 'poll-creator', - type: AttachmentPickerType.poll, + supportedTypes: [AttachmentPickerType.poll], icon: const StreamSvgIcon(icon: StreamSvgIcons.polls), title: context.translations.createPollLabel(isNew: true), - ), - }.where((option) => option.supportedTypes.every(allowedTypes.contains)), - }, - onOptionTap: (context, controller, option) async { - // Handle the polls type option separately - if (option.type case AttachmentPickerType.poll) { - final poll = await showStreamPollCreatorDialog( - context: context, - poll: initialPoll, - config: pollConfig, - ); - - if (poll != null) controller.poll = poll; - return Navigator.pop(context, controller.value); - } + onTap: (context, controller) async { + final initialPoll = controller.value.poll; + final poll = await showStreamPollCreatorDialog( + context: context, + poll: initialPoll, + config: pollConfig, + ); - // Handle the remaining option types. - try { - final attachment = await StreamAttachmentHandler.instance.pickFile( - type: option.type.fileType, - ); - if (attachment != null) { - await controller.addAttachment(attachment); - } - return Navigator.pop(context, controller.value); - } catch (e, stk) { - Navigator.pop(context, controller.value); - if (onError != null) return onError.call(e, stk); + if (poll == null) return Navigator.pop(context); + controller.poll = poll; - rethrow; - } + final result = PollCreated(poll: poll); + return Navigator.pop(context, result); + }, + ), + ...?customOptions, + }.where((option) => option.supportedTypes.every(allowedTypes.contains)), }, ); } diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart index cbd164120e..5ddb5e7df5 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart @@ -1,76 +1,73 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_picker/options/stream_gallery_picker.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// Shows a modal material design bottom sheet. +/// Shows a modal bottom sheet with the Stream attachment picker. /// -/// A modal bottom sheet is an alternative to a menu or a dialog and prevents -/// the user from interacting with the rest of the app. +/// The picker supports two modes: /// -/// A closely related widget is a persistent bottom sheet, which shows -/// information that supplements the primary content of the app without -/// preventing the use from interacting with the app. Persistent bottom sheets -/// can be created and displayed with the [showBottomSheet] function or the -/// [ScaffoldState.showBottomSheet] method. +/// - **Tabbed interface**: Typically used on mobile platforms. Provide +/// [TabbedAttachmentPickerOption] values in [customOptions]. This mode is +/// active when [useSystemAttachmentPicker] is false (default). /// -/// The `context` argument is used to look up the [Navigator] and [Theme] for -/// the bottom sheet. It is only used when the method is called. Its -/// corresponding widget can be safely removed from the tree before the bottom -/// sheet is closed. +/// - **System integration**: Used on web, desktop, or when +/// [useSystemAttachmentPicker] is true. Provide +/// [SystemAttachmentPickerOption] values in [customOptions]. /// -/// The `isScrollControlled` parameter specifies whether this is a route for -/// a bottom sheet that will utilize [DraggableScrollableSheet]. If you wish -/// to have a bottom sheet that has a scrollable child such as a [ListView] or -/// a [GridView] and have the bottom sheet be draggable, you should set this -/// parameter to true. +/// When using the system picker, all [customOptions] must be +/// [SystemAttachmentPickerOption] instances. If any other type is included, +/// an [ArgumentError] is thrown. /// -/// The `useRootNavigator` parameter ensures that the root navigator is used to -/// display the [BottomSheet] when set to `true`. This is useful in the case -/// that a modal [BottomSheet] needs to be displayed above all other content -/// but the caller is inside another [Navigator]. +/// Example using the tabbed interface: +/// ```dart +/// showStreamAttachmentPickerModalBottomSheet( +/// context: context, +/// customOptions: [ +/// TabbedAttachmentPickerOption( +/// key: 'gallery', +/// icon: Icon(Icons.photo), +/// supportedTypes: [AttachmentPickerType.images], +/// optionViewBuilder: (context, controller) { +/// return CustomGalleryWidget(); +/// }, +/// ), +/// ], +/// ); +/// ``` /// -/// The [isDismissible] parameter specifies whether the bottom sheet will be -/// dismissed when user taps on the scrim. +/// Example using the system picker: +/// ```dart +/// showStreamAttachmentPickerModalBottomSheet( +/// context: context, +/// useSystemAttachmentPicker: true, +/// customOptions: [ +/// SystemAttachmentPickerOption( +/// key: 'upload', +/// type: AttachmentPickerType.files, +/// icon: Icon(Icons.upload_file), +/// title: 'Upload File', +/// onTap: (context, controller) async { +/// // Handle file picker +/// }, +/// ), +/// ], +/// ); +/// ``` /// -/// The [enableDrag] parameter specifies whether the bottom sheet can be -/// dragged up and down and dismissed by swiping downwards. -/// -/// The optional [backgroundColor], [elevation], [shape], [clipBehavior], -/// [constraints] and [transitionAnimationController] -/// parameters can be passed in to customize the appearance and behavior of -/// modal bottom sheets (see the documentation for these on [BottomSheet] -/// for more details). -/// -/// The [transitionAnimationController] controls the bottom sheet's entrance and -/// exit animations if provided. -/// -/// The optional `routeSettings` parameter sets the [RouteSettings] -/// of the modal bottom sheet sheet. -/// This is particularly useful in the case that a user wants to observe -/// [PopupRoute]s within a [NavigatorObserver]. -/// -/// Returns a `Future` that resolves to the value (if any) that was passed to -/// [Navigator.pop] when the modal bottom sheet was closed. -/// -/// See also: -/// -/// * [BottomSheet], which becomes the parent of the widget returned by the -/// function passed as the `builder` argument to [showModalBottomSheet]. -/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing -/// non-modal bottom sheets. -/// * [DraggableScrollableSheet], which allows you to create a bottom sheet -/// that grows and then becomes scrollable once it reaches its maximum size. -/// * +/// Returns a [Future] that completes with the value passed to [Navigator.pop], +/// or `null` if the sheet was dismissed. Future showStreamAttachmentPickerModalBottomSheet({ required BuildContext context, Iterable? customOptions, List allowedTypes = AttachmentPickerType.values, Poll? initialPoll, PollConfig? pollConfig, + GalleryPickerConfig? galleryPickerConfig, List? initialAttachments, + Map? initialExtraData, StreamAttachmentPickerController? controller, - ErrorListener? onError, Color? backgroundColor, double? elevation, BoxConstraints? constraints, @@ -80,21 +77,15 @@ Future showStreamAttachmentPickerModalBottomSheet({ bool isDismissible = true, bool enableDrag = true, bool useSystemAttachmentPicker = false, - @Deprecated("Use 'useSystemAttachmentPicker' instead.") - bool useNativeAttachmentPickerOnMobile = false, RouteSettings? routeSettings, AnimationController? transitionAnimationController, Clip? clipBehavior = Clip.hardEdge, ShapeBorder? shape, - ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400), - ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg, - int attachmentThumbnailQuality = 100, - double attachmentThumbnailScale = 1, }) { final colorTheme = StreamChatTheme.of(context).colorTheme; final color = backgroundColor ?? colorTheme.inputBg; - return showModalBottomSheet( + return showModalBottomSheet( context: context, backgroundColor: color, elevation: elevation, @@ -109,53 +100,73 @@ Future showStreamAttachmentPickerModalBottomSheet({ routeSettings: routeSettings, transitionAnimationController: transitionAnimationController, builder: (BuildContext context) { - return StreamPlatformAttachmentPickerBottomSheetBuilder( + return StreamAttachmentPickerBottomSheetBuilder( controller: controller, initialPoll: initialPoll, initialAttachments: initialAttachments, + initialExtraData: initialExtraData, builder: (context, controller, child) { final isWebOrDesktop = switch (CurrentPlatform.type) { - PlatformType.web || - PlatformType.macOS || - PlatformType.linux || - PlatformType.windows => - true, - _ => false, + PlatformType.android || PlatformType.ios => false, + _ => true, }; - final useSystemPicker = useSystemAttachmentPicker || // - useNativeAttachmentPickerOnMobile; + final useSystemPicker = useSystemAttachmentPicker || isWebOrDesktop; + + if (useSystemPicker) { + final invalidOptions = []; + final customSystemOptions = []; + + for (final option in customOptions ?? []) { + if (option is SystemAttachmentPickerOption) { + customSystemOptions.add(option); + } else { + invalidOptions.add(option); + } + } + + if (invalidOptions.isNotEmpty) { + throw ArgumentError( + 'customOptions must be SystemAttachmentPickerOption when using ' + 'the attachment picker (enabled explicitly or on web/desktop).', + ); + } - if (useSystemPicker || isWebOrDesktop) { - return webOrDesktopAttachmentPickerBuilder.call( + return systemAttachmentPickerBuilder.call( context: context, - onError: onError, controller: controller, allowedTypes: allowedTypes, - customOptions: customOptions?.map( - WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption, - ), - initialPoll: initialPoll, + customOptions: customSystemOptions, pollConfig: pollConfig, - attachmentThumbnailSize: attachmentThumbnailSize, - attachmentThumbnailFormat: attachmentThumbnailFormat, - attachmentThumbnailQuality: attachmentThumbnailQuality, - attachmentThumbnailScale: attachmentThumbnailScale, + galleryPickerConfig: galleryPickerConfig, ); } - return mobileAttachmentPickerBuilder.call( + final invalidOptions = []; + final customTabbedOptions = []; + + for (final option in customOptions ?? []) { + if (option is TabbedAttachmentPickerOption) { + customTabbedOptions.add(option); + } else { + invalidOptions.add(option); + } + } + + if (invalidOptions.isNotEmpty == true) { + throw ArgumentError( + 'customOptions must be TabbedAttachmentPickerOption when using ' + 'the tabbed picker (default on mobile).', + ); + } + + return tabbedAttachmentPickerBuilder.call( context: context, - onError: onError, controller: controller, allowedTypes: allowedTypes, - customOptions: customOptions, - initialPoll: initialPoll, + customOptions: customTabbedOptions, pollConfig: pollConfig, - attachmentThumbnailSize: attachmentThumbnailSize, - attachmentThumbnailFormat: attachmentThumbnailFormat, - attachmentThumbnailQuality: attachmentThumbnailQuality, - attachmentThumbnailScale: attachmentThumbnailScale, + galleryPickerConfig: galleryPickerConfig, ); }, ); @@ -164,13 +175,13 @@ Future showStreamAttachmentPickerModalBottomSheet({ } /// Builds the attachment picker bottom sheet. -class StreamPlatformAttachmentPickerBottomSheetBuilder extends StatefulWidget { +class StreamAttachmentPickerBottomSheetBuilder extends StatefulWidget { /// Creates a new instance of the widget. - const StreamPlatformAttachmentPickerBottomSheetBuilder({ + const StreamAttachmentPickerBottomSheetBuilder({ super.key, - this.customOptions, this.initialPoll, this.initialAttachments, + this.initialExtraData, this.child, this.controller, required this.builder, @@ -186,25 +197,25 @@ class StreamPlatformAttachmentPickerBottomSheetBuilder extends StatefulWidget { Widget? child, ) builder; - /// The custom options to be displayed in the attachment picker. - final List? customOptions; - /// The initial poll. final Poll? initialPoll; /// The initial attachments. final List? initialAttachments; + /// The initial extra data for the attachment picker. + final Map? initialExtraData; + /// The controller. final StreamAttachmentPickerController? controller; @override - State createState() => - _StreamPlatformAttachmentPickerBottomSheetBuilderState(); + State createState() => + _StreamAttachmentPickerBottomSheetBuilderState(); } -class _StreamPlatformAttachmentPickerBottomSheetBuilderState - extends State { +class _StreamAttachmentPickerBottomSheetBuilderState + extends State { late StreamAttachmentPickerController _controller; @override @@ -214,6 +225,7 @@ class _StreamPlatformAttachmentPickerBottomSheetBuilderState StreamAttachmentPickerController( initialPoll: widget.initialPoll, initialAttachments: widget.initialAttachments, + initialExtraData: widget.initialExtraData, ); } @@ -231,6 +243,7 @@ class _StreamPlatformAttachmentPickerBottomSheetBuilderState _controller = StreamAttachmentPickerController( initialPoll: widget.initialPoll, initialAttachments: widget.initialAttachments, + initialExtraData: widget.initialExtraData, ); } else { _controller = current; @@ -239,7 +252,7 @@ class _StreamPlatformAttachmentPickerBottomSheetBuilderState @override void didUpdateWidget( - StreamPlatformAttachmentPickerBottomSheetBuilder oldWidget, + StreamAttachmentPickerBottomSheetBuilder oldWidget, ) { super.didUpdateWidget(oldWidget); _updateAttachmentPickerController( diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_controller.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_controller.dart new file mode 100644 index 0000000000..1353931a42 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_controller.dart @@ -0,0 +1,259 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// The default maximum size for media attachments. +const kDefaultMaxAttachmentSize = 100 * 1024 * 1024; // 100MB in Bytes + +/// The default maximum number of media attachments. +const kDefaultMaxAttachmentCount = 10; + +/// Controller class for [StreamAttachmentPicker]. +class StreamAttachmentPickerController + extends ValueNotifier { + /// Creates a new instance of [StreamAttachmentPickerController]. + factory StreamAttachmentPickerController({ + Poll? initialPoll, + List? initialAttachments, + Map? initialExtraData, + int maxAttachmentSize = kDefaultMaxAttachmentSize, + int maxAttachmentCount = kDefaultMaxAttachmentCount, + }) { + return StreamAttachmentPickerController._fromValue( + AttachmentPickerValue( + poll: initialPoll, + attachments: initialAttachments ?? const [], + extraData: initialExtraData ?? const {}, + ), + maxAttachmentSize: maxAttachmentSize, + maxAttachmentCount: maxAttachmentCount, + ); + } + + StreamAttachmentPickerController._fromValue( + this.initialValue, { + this.maxAttachmentSize = kDefaultMaxAttachmentSize, + this.maxAttachmentCount = kDefaultMaxAttachmentCount, + }) : assert( + (initialValue.attachments.length) <= maxAttachmentCount, + '''The initial attachments count must be less than or equal to maxAttachmentCount''', + ), + super(initialValue); + + /// Initial value for the controller. + final AttachmentPickerValue initialValue; + + /// The max attachment size allowed in bytes. + final int maxAttachmentSize; + + /// The max attachment count allowed. + final int maxAttachmentCount; + + @override + set value(AttachmentPickerValue newValue) { + if (newValue.attachments.length > maxAttachmentCount) { + throw ArgumentError( + 'The maximum number of attachments is $maxAttachmentCount.', + ); + } + super.value = newValue; + } + + /// Adds a new [poll] to the message. + set poll(Poll? poll) => value = value.copyWith(poll: poll); + + /// Sets the extra data value for the controller. + /// + /// This can be used to store custom attachment values in case a custom + /// attachment picker option is used. + set extraData(Map? extraData) { + value = value.copyWith(extraData: extraData); + } + + Future _saveToCache(AttachmentFile file) async { + // Cache the attachment in a temporary file. + return StreamAttachmentHandler.instance.saveAttachmentFile( + attachmentFile: file, + ); + } + + Future _removeFromCache(AttachmentFile file) { + // Remove the cached attachment file. + return StreamAttachmentHandler.instance.deleteAttachmentFile( + attachmentFile: file, + ); + } + + /// Adds a new attachment to the message. + Future addAttachment(Attachment attachment) async { + assert(attachment.fileSize != null, ''); + if (attachment.fileSize! > maxAttachmentSize) { + throw ArgumentError( + 'The size of the attachment is ${attachment.fileSize} bytes, ' + 'but the maximum size allowed is $maxAttachmentSize bytes.', + ); + } + + final file = attachment.file; + final uploadState = attachment.uploadState; + + // No need to cache the attachment if it's already uploaded + // or we are on web. + if (file == null || uploadState.isSuccess || CurrentPlatform.isWeb) { + value = value.copyWith(attachments: [...value.attachments, attachment]); + return; + } + + // Cache the attachment in a temporary file. + final tempFilePath = await _saveToCache(file); + + value = value.copyWith(attachments: [ + ...value.attachments, + attachment.copyWith( + file: file.copyWith( + path: tempFilePath, + ), + ), + ]); + } + + /// Removes the specified [attachment] from the message. + Future removeAttachment(Attachment attachment) async { + final file = attachment.file; + final uploadState = attachment.uploadState; + + if (file == null || uploadState.isSuccess || CurrentPlatform.isWeb) { + final updatedAttachments = [...value.attachments]..remove(attachment); + value = value.copyWith(attachments: updatedAttachments); + return; + } + + // Remove the cached attachment file. + await _removeFromCache(file); + + final updatedAttachments = [...value.attachments]..remove(attachment); + value = value.copyWith(attachments: updatedAttachments); + } + + /// Remove the attachment with the given [attachmentId]. + void removeAttachmentById(String attachmentId) { + final attachment = value.attachments.firstWhereOrNull( + (attachment) => attachment.id == attachmentId, + ); + + if (attachment == null) return; + + removeAttachment(attachment); + } + + /// Clears all the attachments. + Future clear() async { + final attachments = [...value.attachments]; + for (final attachment in attachments) { + final file = attachment.file; + final uploadState = attachment.uploadState; + + if (file == null || uploadState.isSuccess || CurrentPlatform.isWeb) { + continue; + } + + await _removeFromCache(file); + } + value = const AttachmentPickerValue(); + } + + /// Resets the controller to its initial state. + Future reset() async { + final attachments = [...value.attachments]; + for (final attachment in attachments) { + final file = attachment.file; + final uploadState = attachment.uploadState; + + if (file == null || uploadState.isSuccess || CurrentPlatform.isWeb) { + continue; + } + + await _removeFromCache(file); + } + + value = initialValue; + } +} + +/// Value class for [AttachmentPickerController]. +/// +/// This class holds the list of [Poll] and [Attachment] objects. +class AttachmentPickerValue { + /// Creates a new instance of [AttachmentPickerValue]. + const AttachmentPickerValue({ + this.poll, + this.attachments = const [], + this.extraData = const {}, + }); + + /// The poll object. + final Poll? poll; + + /// The list of [Attachment] objects. + final List attachments; + + /// Extra data that can be used to store custom attachment values. + final Map extraData; + + /// Returns true if the value is empty, meaning it has no poll, + /// no attachments and no extra data set. + bool get isEmpty { + if (poll != null) return false; + if (attachments.isNotEmpty) return false; + if (extraData.isNotEmpty) return false; + + return true; + } + + /// Returns a copy of this object with the provided values. + AttachmentPickerValue copyWith({ + Poll? poll, + List? attachments, + Map? extraData, + }) { + return AttachmentPickerValue( + poll: poll ?? this.poll, + attachments: attachments ?? this.attachments, + extraData: extraData ?? this.extraData, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + if (other is! AttachmentPickerValue) return false; + + final isPollEqual = other.poll == poll; + + final areAttachmentsEqual = UnorderedIterableEquality( + EqualityBy((attachment) => attachment.id), + ).equals(other.attachments, attachments); + + final isExtraDataEqual = const DeepCollectionEquality.unordered().equals( + other.extraData, + extraData, + ); + + return isPollEqual && areAttachmentsEqual && isExtraDataEqual; + } + + @override + int get hashCode { + final attachmentsHash = UnorderedIterableEquality( + EqualityBy((attachment) => attachment.id), + ).hash(attachments); + + final extraDataHash = const DeepCollectionEquality.unordered().hash( + extraData, + ); + + return poll.hashCode ^ attachmentsHash ^ extraDataHash; + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_option.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_option.dart new file mode 100644 index 0000000000..1eec1d4638 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_option.dart @@ -0,0 +1,198 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_picker/stream_attachment_picker_controller.dart'; + +/// Function signature for building the attachment picker option view. +typedef AttachmentPickerOptionViewBuilder = Widget Function( + BuildContext context, + StreamAttachmentPickerController controller, +); + +/// Function signature for system attachment picker option callback. +typedef OnSystemAttachmentPickerOptionTap = Future Function( + BuildContext context, + StreamAttachmentPickerController controller, +); + +/// Base class for attachment picker options. +abstract class AttachmentPickerOption { + /// Creates a new instance of [AttachmentPickerOption]. + const AttachmentPickerOption({ + this.key, + required this.supportedTypes, + required this.icon, + this.title, + this.isEnabled = _defaultIsEnabled, + }); + + /// A key to identify the option. + final String? key; + + /// The icon of the option. + final Widget icon; + + /// The title of the option. + final String? title; + + /// The supported types of the option. + final Iterable supportedTypes; + + /// Determines if this option is enabled based on the current value. + /// + /// If not provided, defaults to always returning true. + final bool Function(AttachmentPickerValue value) isEnabled; + static bool _defaultIsEnabled(AttachmentPickerValue value) => true; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! AttachmentPickerOption) return false; + if (runtimeType != other.runtimeType) return false; + + final areSupportedTypesEqual = const UnorderedIterableEquality().equals( + supportedTypes, + other.supportedTypes, + ); + + return key == other.key && areSupportedTypesEqual; + } + + @override + int get hashCode { + final supportedTypesHash = const UnorderedIterableEquality().hash( + supportedTypes, + ); + + return key.hashCode ^ supportedTypesHash; + } +} + +/// Attachment picker option that shows custom UI inside the attachment picker's +/// tabbed interface. Use this when you want to display your own custom +/// interface for selecting attachments. +/// +/// This option is used when the attachment picker displays a tabbed interface +/// (typically on mobile when useSystemAttachmentPicker is false). +class TabbedAttachmentPickerOption extends AttachmentPickerOption { + /// Creates a new instance of [TabbedAttachmentPickerOption]. + const TabbedAttachmentPickerOption({ + super.key, + required super.supportedTypes, + required super.icon, + required this.optionViewBuilder, + super.title, + super.isEnabled, + }); + + /// The option view builder for custom UI. + final AttachmentPickerOptionViewBuilder optionViewBuilder; +} + +/// Attachment picker option that uses system integration +/// (file dialogs, camera, etc.). +/// +/// Use this when you want to open system pickers or perform system actions. +class SystemAttachmentPickerOption extends AttachmentPickerOption { + /// Creates a new instance of [SystemAttachmentPickerOption]. + const SystemAttachmentPickerOption({ + super.key, + required super.supportedTypes, + required super.icon, + required this.title, + required this.onTap, + super.isEnabled, + }); + + @override + final String title; + + /// The callback that is called when the option is tapped. + final OnSystemAttachmentPickerOptionTap onTap; +} + +/// Helpful extensions for [StreamAttachmentPickerController]. +extension AttachmentPickerOptionTypeX on AttachmentPickerValue { + /// Returns the list of enabled picker types. + Set filterEnabledTypes({ + required Iterable options, + }) { + final enabledTypes = {}; + for (final option in options) { + if (option.isEnabled.call(this)) { + enabledTypes.addAll(option.supportedTypes); + } + } + return enabledTypes; + } +} + +/// {@template streamAttachmentPickerType} +/// A sealed class that represents different types of attachment which a picker +/// option can support. +/// {@endtemplate} +sealed class AttachmentPickerType { + const AttachmentPickerType(); + + /// The option will allow to pick images. + static const images = ImagesPickerType(); + + /// The option will allow to pick videos. + static const videos = VideosPickerType(); + + /// The option will allow to pick audios. + static const audios = AudiosPickerType(); + + /// The option will allow to pick files or documents. + static const files = FilesPickerType(); + + /// The option will allow to create a poll. + static const poll = PollPickerType(); + + /// A list of all predefined attachment picker types. + static const values = [images, videos, audios, files, poll]; +} + +/// A predefined attachment picker type that allows picking images. +final class ImagesPickerType extends AttachmentPickerType { + /// Creates a new images picker type. + const ImagesPickerType(); +} + +/// A predefined attachment picker type that allows picking videos. +final class VideosPickerType extends AttachmentPickerType { + /// Creates a new videos picker type. + const VideosPickerType(); +} + +/// A predefined attachment picker type that allows picking audios. +final class AudiosPickerType extends AttachmentPickerType { + /// Creates a new audios picker type. + const AudiosPickerType(); +} + +/// A predefined attachment picker type that allows picking files or documents. +final class FilesPickerType extends AttachmentPickerType { + /// Creates a new files picker type. + const FilesPickerType(); +} + +/// A predefined attachment picker type that allows creating polls. +final class PollPickerType extends AttachmentPickerType { + /// Creates a new poll picker type. + const PollPickerType(); +} + +/// A custom picker type that can be extended to support custom types of +/// attachments. This allows developers to create their own attachment picker +/// options for specialized content types. +/// +/// Example: +/// ```dart +/// class DocumentPickerType extends CustomAttachmentPickerType { +/// const DocumentPickerType(); +/// } +/// ``` +abstract class CustomAttachmentPickerType extends AttachmentPickerType { + /// Creates a new custom picker type. + const CustomAttachmentPickerType(); +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_result.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_result.dart new file mode 100644 index 0000000000..770fc4422b --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_result.dart @@ -0,0 +1,58 @@ +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Signature for a function that is called when a custom attachment picker +/// result is received. +typedef OnCustomAttachmentPickerResult + = OnAttachmentPickerResult; + +/// Signature for a function that is called when a attachment picker result +/// is received. +typedef OnAttachmentPickerResult = void + Function(T result); + +/// {@template streamAttachmentPickerAction} +/// A sealed class that represents different results that can be returned +/// from the attachment picker. +/// {@endtemplate} +sealed class StreamAttachmentPickerResult { + const StreamAttachmentPickerResult(); +} + +/// A result indicating that the attachment picker was met with an error. +final class AttachmentPickerError extends StreamAttachmentPickerResult { + /// Create a new attachment picker error result + const AttachmentPickerError({required this.error, this.stackTrace}); + + /// The error that occurred in the attachment picker. + final Object error; + + /// The stack trace associated with the error, if available. + final StackTrace? stackTrace; +} + +/// A result indicating that some attachments were picked using the media +/// related options in the attachment picker (e.g., camera, gallery). +final class AttachmentsPicked extends StreamAttachmentPickerResult { + /// Create a new attachments picked result + const AttachmentsPicked({required this.attachments}); + + /// The list of attachments that were picked. + final List attachments; +} + +/// A result indicating that a poll was created using the create poll option +/// in the attachment picker. +final class PollCreated extends StreamAttachmentPickerResult { + /// Create a new poll created result + const PollCreated({required this.poll}); + + /// The poll that was created. + final Poll poll; +} + +/// A custom attachment picker result that can be extended to support +/// custom type of results from the attachment picker. +class CustomAttachmentPickerResult extends StreamAttachmentPickerResult { + /// Create a new custom attachment picker result + const CustomAttachmentPickerResult(); +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/dm_checkbox.dart b/packages/stream_chat_flutter/lib/src/message_input/dm_checkbox.dart deleted file mode 100644 index 0a69b8d5fe..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_input/dm_checkbox.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template dmCheckbox} -/// Prompts the user to send a reply to a message thread as a DM. -/// {@endtemplate} -@Deprecated("Use 'DmCheckboxListTile' instead.") -class DmCheckbox extends StatelessWidget { - /// {@macro dmCheckbox} - const DmCheckbox({ - super.key, - required this.foregroundDecoration, - required this.color, - required this.onTap, - required this.crossFadeState, - }); - - /// The decoration to use for the button's foreground. - final BoxDecoration foregroundDecoration; - - /// The color to use for the button. - final Color color; - - /// The action to perform when the button is tapped or clicked. - final VoidCallback onTap; - - /// The [CrossFadeState] of the animation. - final CrossFadeState crossFadeState; - - @override - Widget build(BuildContext context) { - final _streamChatTheme = StreamChatTheme.of(context); - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 16, - width: 16, - foregroundDecoration: foregroundDecoration, - child: Center( - child: Material( - borderRadius: BorderRadius.circular(3), - color: color, - child: InkWell( - onTap: onTap, - child: AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - reverseDuration: const Duration(milliseconds: 300), - crossFadeState: crossFadeState, - firstChild: StreamSvgIcon( - size: 16, - icon: StreamSvgIcons.check, - color: _streamChatTheme.colorTheme.barsBg, - ), - secondChild: const SizedBox( - height: 16, - width: 16, - ), - ), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - context.translations.alsoSendAsDirectMessageLabel, - style: _streamChatTheme.textTheme.footnote.copyWith( - color: - // ignore: deprecated_member_use - _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), - ), - ), - ), - ], - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart index 24611a0a13..931ea553bc 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/quoted_message_widget.dart @@ -151,9 +151,17 @@ class _QuotedMessage extends StatelessWidget { Flexible( child: Text( '๐Ÿ“Š ${message.poll?.name}', - style: messageTheme.messageTextStyle?.copyWith( - fontSize: 12, - ), + style: messageTheme.messageTextStyle?.copyWith(fontSize: 12), + ), + ), + ]; + } else if (message.sharedLocation case final location?) { + // Show shared location message + children = [ + Flexible( + child: Text( + context.translations.locationLabel(isLive: location.isLive), + style: messageTheme.messageTextStyle?.copyWith(fontSize: 12), ), ), ]; diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart index 1355150f13..de88469d30 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart @@ -124,10 +124,8 @@ class StreamMessageInput extends StatefulWidget { this.hideSendAsDm = false, this.enableVoiceRecording = false, this.sendVoiceRecordingAutomatically = false, - Widget? idleSendIcon, - @Deprecated("Use 'idleSendIcon' instead") Widget? idleSendButton, - Widget? activeSendIcon, - @Deprecated("Use 'activeSendIcon' instead") Widget? activeSendButton, + this.idleSendIcon, + this.activeSendIcon, this.showCommandsButton = true, this.userMentionsTileBuilder, this.maxAttachmentSize = kDefaultMaxAttachmentSize, @@ -158,27 +156,13 @@ class StreamMessageInput extends StatefulWidget { this.ogPreviewFilter = _defaultOgPreviewFilter, this.hintGetter = _defaultHintGetter, this.contentInsertionConfiguration, - bool useSystemAttachmentPicker = false, - @Deprecated( - 'Use useSystemAttachmentPicker instead. ' - 'This feature was deprecated after v9.4.0', - ) - bool useNativeAttachmentPickerOnMobile = false, + this.useSystemAttachmentPicker = false, this.pollConfig, + this.customAttachmentPickerOptions = const [], + this.onCustomAttachmentPickerResult, this.padding = const EdgeInsets.all(8), this.textInputMargin, - }) : assert( - idleSendIcon == null || idleSendButton == null, - 'idleSendIcon and idleSendButton cannot be used together', - ), - idleSendIcon = idleSendIcon ?? idleSendButton, - assert( - activeSendIcon == null || activeSendButton == null, - 'activeSendIcon and activeSendButton cannot be used together', - ), - activeSendIcon = activeSendIcon ?? activeSendButton, - useSystemAttachmentPicker = useSystemAttachmentPicker || // - useNativeAttachmentPickerOnMobile; + }); /// The predicate used to send a message on desktop/web final KeyEventPredicate sendMessageKeyPredicate; @@ -307,17 +291,9 @@ class StreamMessageInput extends StatefulWidget { /// Send button widget in an idle state final Widget? idleSendIcon; - /// Send button widget in an idle state - @Deprecated("Use 'idleSendIcon' instead") - Widget? get idleSendButton => idleSendIcon; - /// Send button widget in an active state final Widget? activeSendIcon; - /// Send button widget in an active state - @Deprecated("Use 'activeSendIcon' instead") - Widget? get activeSendButton => activeSendIcon; - /// Customize the tile for the mentions overlay. final UserMentionTileBuilder? userMentionsTileBuilder; @@ -413,19 +389,21 @@ class StreamMessageInput extends StatefulWidget { /// functionality of the system media picker. final bool useSystemAttachmentPicker; - /// Forces use of native attachment picker on mobile instead of the custom - /// Stream attachment picker. - @Deprecated( - 'Use useSystemAttachmentPicker instead. ' - 'This feature was deprecated after v9.4.0', - ) - bool get useNativeAttachmentPickerOnMobile => useSystemAttachmentPicker; - /// The configuration to use while creating a poll. /// /// If not provided, the default configuration is used. final PollConfig? pollConfig; + /// A list of custom attachment picker options that can be used to extend the + /// attachment picker functionality. + final List customAttachmentPickerOptions; + + /// Callback that is called when the custom attachment picker result is + /// received. + /// + /// This is used to handle the result of the custom attachment picker + final OnCustomAttachmentPickerResult? onCustomAttachmentPickerResult; + /// Padding for the message input. /// /// Defaults to `EdgeInsets.all(8)`. @@ -960,36 +938,30 @@ class StreamMessageInputState extends State defaultButton; } - Future _sendPoll(Poll poll, Channel channel) { - return channel.sendPoll(poll); - } - - Future _updatePoll(Poll poll, Channel channel) { - return channel.updatePoll(poll); - } - - Future _deletePoll(Poll poll, Channel channel) { - return channel.deletePoll(poll); - } - - Future _createOrUpdatePoll( - Poll? old, - Poll? current, - ) async { + Future _onPollCreated(Poll poll) async { final channel = StreamChannel.maybeOf(context)?.channel; if (channel == null) return; - // If both are null or the same, return - if ((old == null && current == null) || old == current) return; + return channel.sendPoll(poll).ignore(); + } + + // Returns the list of allowed attachment picker types based on the + // current channel configuration and context. + List _getAllowedAttachmentPickerTypes() { + final allowedTypes = widget.allowedAttachmentPickerTypes.where((type) { + if (type != AttachmentPickerType.poll) return true; - // If old is null, i.e., there was no poll before, create the poll. - if (old == null) return _sendPoll(current!, channel); + // We don't allow editing polls. + if (_isEditing) return false; + // We don't allow creating polls in threads. + if (_effectiveController.message.parentId != null) return false; - // If current is null, i.e., the poll is removed, delete the poll. - if (current == null) return _deletePoll(old, channel); + // Otherwise, check if the user has the permission to send polls. + final channel = StreamChannel.of(context).channel; + return channel.config?.polls == true && channel.canSendPoll; + }); - // Otherwise, update the poll. - return _updatePoll(current, channel); + return allowedTypes.toList(growable: false); } /// Handle the platform-specific logic for selecting files. @@ -1000,43 +972,46 @@ class StreamMessageInputState extends State Future _onAttachmentButtonPressed() async { final initialPoll = _effectiveController.poll; final initialAttachments = _effectiveController.attachments; - - // Remove AttachmentPickerType.poll if the user doesn't have the permission - // to send a poll or if this is a thread message. - final allowedTypes = [...widget.allowedAttachmentPickerTypes] - ..removeWhere((it) { - if (it != AttachmentPickerType.poll) return false; - if (_effectiveController.message.parentId != null) return true; - - final channel = StreamChannel.maybeOf(context)?.channel; - if (channel == null) return true; - - if (channel.config?.polls == true && channel.canSendPoll) return false; - - return true; - }); + final allowedTypes = _getAllowedAttachmentPickerTypes(); final messageInputTheme = StreamMessageInputTheme.of(context); final useSystemPicker = widget.useSystemAttachmentPicker || (messageInputTheme.useSystemAttachmentPicker ?? false); - final value = await showStreamAttachmentPickerModalBottomSheet( + final result = await showStreamAttachmentPickerModalBottomSheet( context: context, - onError: widget.onError, allowedTypes: allowedTypes, - pollConfig: widget.pollConfig, initialPoll: initialPoll, + pollConfig: widget.pollConfig, initialAttachments: initialAttachments, useSystemAttachmentPicker: useSystemPicker, + customOptions: widget.customAttachmentPickerOptions, ); - if (value == null || value is! AttachmentPickerValue) return; + if (result == null || result is! StreamAttachmentPickerResult) return; + + void _onAttachmentsPicked(List attachments) { + _effectiveController.attachments = attachments; + } + + void _onAttachmentPickerError(AttachmentPickerError error) { + return widget.onError?.call(error.error, error.stackTrace); + } - // Add the attachments to the controller. - _effectiveController.attachments = value.attachments; + void _onCustomAttachmentPickerResult(CustomAttachmentPickerResult result) { + return widget.onCustomAttachmentPickerResult?.call(result); + } - // Create or update the poll. - await _createOrUpdatePoll(initialPoll, value.poll); + return switch (result) { + // Add the attachments to the controller. + AttachmentsPicked() => _onAttachmentsPicked(result.attachments), + // Send the created poll in the channel. + PollCreated() => _onPollCreated(result.poll), + // Handle custom attachment picker results. + CustomAttachmentPickerResult() => _onCustomAttachmentPickerResult(result), + // Handle/Notify returned errors. + AttachmentPickerError() => _onAttachmentPickerError(result), + }; } Widget _buildTextInput(BuildContext context) { diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart index ac38e761b6..742846c694 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart @@ -11,25 +11,10 @@ class StreamMessageSendButton extends StatelessWidget { super.key, this.timeOut = 0, this.isIdle = true, - @Deprecated('Will be removed in the next major version') - this.isCommandEnabled = false, - @Deprecated('Will be removed in the next major version') - this.isEditEnabled = false, - Widget? idleSendIcon, - @Deprecated("Use 'idleSendIcon' instead") Widget? idleSendButton, - Widget? activeSendIcon, - @Deprecated("Use 'activeSendIcon' instead") Widget? activeSendButton, + this.idleSendIcon, + this.activeSendIcon, required this.onSendMessage, - }) : assert( - idleSendIcon == null || idleSendButton == null, - 'idleSendIcon and idleSendButton cannot be used together', - ), - idleSendIcon = idleSendIcon ?? idleSendButton, - assert( - activeSendIcon == null || activeSendButton == null, - 'activeSendIcon and activeSendButton cannot be used together', - ), - activeSendIcon = activeSendIcon ?? activeSendButton; + }); /// Time out related to slow mode. final int timeOut; @@ -37,28 +22,12 @@ class StreamMessageSendButton extends StatelessWidget { /// If true the button will be disabled. final bool isIdle; - /// True if a command is being sent. - @Deprecated('It will be removed in the next major version') - final bool isCommandEnabled; - - /// True if in editing mode. - @Deprecated('It will be removed in the next major version') - final bool isEditEnabled; - /// The icon to display when the button is idle. final Widget? idleSendIcon; - /// The widget to display when the button is disabled. - @Deprecated("Use 'idleSendIcon' instead") - Widget? get idleSendButton => idleSendIcon; - /// The icon to display when the button is active. final Widget? activeSendIcon; - /// The widget to display when the button is enabled. - @Deprecated("Use 'activeSendIcon' instead") - Widget? get activeSendButton => activeSendIcon; - /// The callback to call when the button is pressed. final VoidCallback onSendMessage; diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart b/packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart index 9802da666a..6b9b2b381e 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/floating_date_divider.dart @@ -16,15 +16,9 @@ class FloatingDateDivider extends StatelessWidget { required this.reverse, required this.messages, required this.itemCount, - @Deprecated('No longer used, Will be removed in future versions.') - this.isThreadConversation = false, this.dateDividerBuilder, }); - /// true if this is a thread conversation - @Deprecated('No longer used, Will be removed in future versions.') - final bool isThreadConversation; - /// A [ValueListenable] that provides the positions of items in the list view. final ValueListenable> itemPositionListener; diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart index 70eae83e9a..ac5d542f9f 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart @@ -997,10 +997,6 @@ class _StreamMessageListViewState extends State { final isMyMessage = message.user!.id == StreamChat.of(context).currentUser!.id; final isOnlyEmoji = message.text?.isOnlyEmoji ?? false; - final currentUser = StreamChat.of(context).currentUser; - final members = StreamChannel.of(context).channel.state?.members ?? []; - final currentUserMember = - members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); final hasFileAttachment = message.attachments.any((it) => it.type == AttachmentType.file); @@ -1017,6 +1013,11 @@ class _StreamMessageListViewState extends State { final borderSide = isOnlyEmoji ? BorderSide.none : null; final defaultMessageWidget = StreamMessageWidget( + message: message, + reverse: isMyMessage, + showUsername: !isMyMessage, + showReactions: !message.isDeleted && !message.state.isDeletingFailed, + showReactionPicker: !message.isDeleted && !message.state.isDeletingFailed, showReplyMessage: false, showResendMessage: false, showThreadReplyMessage: false, @@ -1024,9 +1025,6 @@ class _StreamMessageListViewState extends State { showDeleteMessage: false, showEditMessage: false, showMarkUnreadMessage: false, - message: message, - reverse: isMyMessage, - showUsername: !isMyMessage, padding: const EdgeInsets.all(8), showSendingIndicator: false, attachmentPadding: EdgeInsets.all( @@ -1069,13 +1067,6 @@ class _StreamMessageListViewState extends State { : _streamTheme.otherMessageTheme, onMessageTap: widget.onMessageTap, onMessageLongPress: widget.onMessageLongPress, - showPinButton: currentUserMember != null && - streamChannel?.channel.canPinMessage == true && - // Pinning a restricted visibility message is not allowed, simply - // because pinning a message is meant to bring attention to that - // message, that is not possible with a message that is only visible - // to a subset of users. - !message.hasRestrictedVisibility, ); if (widget.parentMessageBuilder != null) { @@ -1278,15 +1269,11 @@ class _StreamMessageListViewState extends State { final borderSide = isOnlyEmoji ? BorderSide.none : null; - final currentUser = StreamChat.of(context).currentUser; - final members = StreamChannel.of(context).channel.state?.members ?? []; - final currentUserMember = - members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); - Widget messageWidget = StreamMessageWidget( message: message, reverse: isMyMessage, - showReactions: !message.isDeleted, + showReactions: !message.isDeleted && !message.state.isDeletingFailed, + showReactionPicker: !message.isDeleted && !message.state.isDeletingFailed, padding: const EdgeInsets.symmetric(horizontal: 8), showInChannelIndicator: showInChannelIndicator, showThreadReplyIndicator: showThreadReplyIndicator, @@ -1388,13 +1375,6 @@ class _StreamMessageListViewState extends State { : _streamTheme.otherMessageTheme, onMessageTap: widget.onMessageTap, onMessageLongPress: widget.onMessageLongPress, - showPinButton: currentUserMember != null && - streamChannel?.channel.canPinMessage == true && - // Pinning a restricted visibility message is not allowed, simply - // because pinning a message is meant to bring attention to that - // message, that is not possible with a message that is only visible - // to a subset of users. - !message.hasRestrictedVisibility, ); if (widget.messageBuilder != null) { @@ -1492,31 +1472,41 @@ class _StreamMessageListViewState extends State { } void _getOnThreadTap() { - if (widget.onThreadTap != null) { - _onThreadTap = (Message message) { - final threadBuilder = widget.threadBuilder; - widget.onThreadTap!( - message, - threadBuilder != null ? threadBuilder(context, message) : null, - ); - }; - } else if (widget.threadBuilder != null) { - _onThreadTap = (Message message) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => BetterStreamBuilder( - stream: streamChannel!.channel.state!.messagesStream.map( - (messages) => messages.firstWhere((m) => m.id == message.id), - ), - initialData: message, - builder: (_, data) => StreamChannel( - channel: streamChannel!.channel, - child: widget.threadBuilder!(context, data), + _onThreadTap = switch ((widget.onThreadTap, widget.threadBuilder)) { + // Case 1: widget.onThreadTap is provided. + // The created callback will use widget.onThreadTap, passing the result + // of widget.threadBuilder (if provided) as the second argument. + (final onThreadTap?, final threadBuilder) => (Message message) { + onThreadTap( + message, + threadBuilder?.call(context, message), + ); + }, + // Case 2: widget.onThreadTap is null, but widget.threadBuilder is provided. + // The created callback will perform the default navigation action, + // using widget.threadBuilder to build the thread page. + (null, final threadBuilder?) => (Message message) { + final threadPage = StreamChatConfiguration( + // This is needed to provide the nearest reaction icons to the + // StreamMessageReactionsModal. + data: StreamChatConfiguration.of(context), + child: StreamChannel( + channel: streamChannel!.channel, + child: BetterStreamBuilder( + initialData: message, + stream: streamChannel!.channel.state?.messagesStream.map( + (it) => it.firstWhere((m) => m.id == message.id), + ), + builder: (_, data) => threadBuilder(context, data), ), ), - ), - ); - }; - } + ); + + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => threadPage), + ); + }, + _ => null, + }; } } diff --git a/packages/stream_chat_flutter/lib/src/message_modal/message_action_confirmation_modal.dart b/packages/stream_chat_flutter/lib/src/message_modal/message_action_confirmation_modal.dart new file mode 100644 index 0000000000..ec79afc166 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_modal/message_action_confirmation_modal.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/misc/adaptive_dialog_action.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; + +/// {@template streamMessageActionConfirmationModal} +/// A confirmation modal dialog for message actions in Stream Chat. +/// +/// This widget creates a platform-adaptive confirmation dialog that can be used +/// when a user attempts to perform an action on a message that requires +/// confirmation (like delete, flag, etc). +/// +/// The dialog presents two options: cancel and confirm, with customizable text +/// for both actions. The confirm action can be styled as destructive for +/// actions like deletion. +/// +/// Example usage: +/// +/// ```dart +/// showDialog( +/// context: context, +/// builder: (context) => StreamMessageActionConfirmationModal( +/// title: Text('Delete Message'), +/// content: Text('Are you sure you want to delete this message?'), +/// confirmActionTitle: Text('Delete'), +/// isDestructiveAction: true, +/// ), +/// ).then((confirmed) { +/// if (confirmed == true) { +/// // Perform the action +/// } +/// }); +/// ``` +/// {@endtemplate} +class StreamMessageActionConfirmationModal extends StatelessWidget { + /// Creates a message action confirmation modal. + /// + /// The [cancelActionTitle] defaults to a Text widget with 'Cancel'. + /// The [confirmActionTitle] defaults to a Text widget with 'Confirm'. + /// Set [isDestructiveAction] to true for actions like deletion that should + /// be highlighted as destructive. + const StreamMessageActionConfirmationModal({ + super.key, + this.title, + this.content, + this.cancelActionTitle = const Text('Cancel'), + this.confirmActionTitle = const Text('Confirm'), + this.isDestructiveAction = false, + }); + + /// The title of the dialog. + /// + /// Typically a [Text] widget. + final Widget? title; + + /// The content of the dialog, displayed below the title. + /// + /// Typically a [Text] widget that provides more details about the action. + final Widget? content; + + /// The widget to display as the cancel action button. + /// + /// Defaults to a [Text] widget with 'Cancel'. + /// When pressed, this action dismisses the dialog and returns false. + final Widget cancelActionTitle; + + /// The widget to display as the confirm action button. + /// + /// Defaults to a [Text] widget with 'Confirm'. + /// When pressed, this action dismisses the dialog and returns true. + final Widget confirmActionTitle; + + /// Whether the confirm action is destructive (like deletion). + /// + /// When true, the confirm action will be styled accordingly + /// (e.g., in red on iOS/macOS). + final bool isDestructiveAction; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final colorTheme = theme.colorTheme; + final textTheme = theme.textTheme; + + final actions = [ + AdaptiveDialogAction( + onPressed: () => Navigator.of(context).maybePop(false), + child: cancelActionTitle, + ), + AdaptiveDialogAction( + onPressed: () => Navigator.of(context).maybePop(true), + isDefaultAction: true, + isDestructiveAction: isDestructiveAction, + child: confirmActionTitle, + ), + ]; + + return AlertDialog.adaptive( + clipBehavior: Clip.antiAlias, + backgroundColor: colorTheme.barsBg, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + title: title, + titleTextStyle: textTheme.headline.copyWith( + color: colorTheme.textHighEmphasis, + ), + content: content, + contentTextStyle: textTheme.body.copyWith( + color: colorTheme.textLowEmphasis, + ), + actions: actions, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_modal/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_modal/message_actions_modal.dart new file mode 100644 index 0000000000..02e929b26f --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_modal/message_actions_modal.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/reactions/picker/reaction_picker_bubble_overlay.dart'; + +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template streamMessageActionsModal} +/// A modal that displays a list of actions that can be performed on a message. +/// +/// This widget presents a customizable menu of actions for a message, such as +/// reply, edit, delete, etc., along with an optional reaction picker. +/// +/// Typically used when a user long-presses on a message to see available +/// actions. +/// {@endtemplate} +class StreamMessageActionsModal extends StatelessWidget { + /// {@macro streamMessageActionsModal} + const StreamMessageActionsModal({ + super.key, + required this.message, + required this.messageActions, + required this.messageWidget, + this.reverse = false, + this.showReactionPicker = false, + this.reactionPickerBuilder = StreamReactionPicker.builder, + this.onActionTap, + }); + + /// The message object that actions will be performed on. + /// + /// This is the message the user selected to see available actions. + final Message message; + + /// List of custom actions that will be displayed in the modal. + /// + /// Each action is represented by a [StreamMessageAction] object which defines + /// the action's appearance and behavior. + final List messageActions; + + /// The widget representing the message being acted upon. + /// + /// This is typically displayed at the top of the modal as a reference for the + /// user. + final Widget messageWidget; + + /// Whether the message should be displayed in reverse direction. + /// + /// This affects how the modal and reactions are displayed and aligned. + /// Set to `true` for right-aligned messages (typically the current user's). + /// Set to `false` for left-aligned messages (typically other users'). + /// + /// Defaults to `false`. + final bool reverse; + + /// Controls whether to show the reaction picker at the top of the modal. + /// + /// When `true`, users can add reactions directly from the modal. + /// When `false`, the reaction picker is hidden. + /// + /// Defaults to `false`. + final bool showReactionPicker; + + /// {@macro reactionPickerBuilder} + final ReactionPickerBuilder reactionPickerBuilder; + + /// Callback triggered when a message action is tapped. + /// + /// Provides the tapped [MessageAction] object to the callback. + final OnMessageActionTap? onActionTap; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + + final alignment = switch (reverse) { + true => AlignmentDirectional.centerEnd, + false => AlignmentDirectional.centerStart, + }; + + final onReactionPicked = switch (onActionTap) { + null => null, + final onActionTap => (reaction) => onActionTap( + SelectReaction(message: message, reaction: reaction), + ), + }; + + return StreamMessageDialog( + spacing: 4, + alignment: alignment, + headerBuilder: (context) { + final safeArea = MediaQuery.paddingOf(context); + + return Padding( + padding: EdgeInsets.only(top: safeArea.top), + child: ReactionPickerBubbleOverlay( + message: message, + reverse: reverse, + visible: showReactionPicker, + anchorOffset: const Offset(0, -8), + onReactionPicked: onReactionPicked, + reactionPickerBuilder: reactionPickerBuilder, + child: IgnorePointer(child: messageWidget), + ), + ); + }, + contentBuilder: (context) { + final actions = Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: { + ...messageActions.map( + (action) => StreamMessageActionItem( + action: action, + onTap: onActionTap, + ), + ), + }.insertBetween(Divider(height: 1, color: theme.colorTheme.borders)), + ); + + return FractionallySizedBox( + widthFactor: 0.78, + child: Material( + type: MaterialType.transparency, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: actions, + ), + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_modal/message_modal.dart b/packages/stream_chat_flutter/lib/src/message_modal/message_modal.dart new file mode 100644 index 0000000000..ec80700364 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_modal/message_modal.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; + +/// {@template streamMessageDialog} +/// A customizable modal dialog for displaying message-related content. +/// +/// This widget provides a consistent container for message actions and other +/// message-related dialog content. It handles layout, animation, and keyboard +/// adjustments automatically. +/// +/// The dialog can contain a header (optional) and content section (required), +/// and will adjust its position when the keyboard appears. +/// {@endtemplate} +class StreamMessageDialog extends StatelessWidget { + /// Creates a Stream message dialog. + /// + /// The [contentBuilder] parameter is required to build the main content + /// of the dialog. The [headerBuilder] is optional and can be used to add + /// a header above the main content. + const StreamMessageDialog({ + super.key, + this.spacing = 8.0, + this.headerBuilder, + required this.contentBuilder, + this.useSafeArea = true, + this.insetAnimationDuration = const Duration(milliseconds: 100), + this.insetAnimationCurve = Curves.decelerate, + this.insetPadding = const EdgeInsets.all(8), + this.alignment = Alignment.center, + }); + + /// Vertical spacing between header and content sections. + final double spacing; + + /// Optional builder for the header section of the dialog. + final WidgetBuilder? headerBuilder; + + /// Required builder for the main content of the dialog. + final WidgetBuilder contentBuilder; + + /// Whether to use a [SafeArea] to avoid system UI intrusions. + /// + /// Defaults to `true`. + final bool useSafeArea; + + /// The duration of the animation to show when the system keyboard intrudes + /// into the space that the dialog is placed in. + /// + /// Defaults to 100 milliseconds. + final Duration insetAnimationDuration; + + /// The curve to use for the animation shown when the system keyboard intrudes + /// into the space that the dialog is placed in. + /// + /// Defaults to [Curves.decelerate]. + final Curve insetAnimationCurve; + + /// The amount of padding added to [MediaQueryData.viewInsets] on the outside + /// of the dialog. This defines the minimum space between the screen's edges + /// and the dialog. + /// + /// Defaults to `EdgeInsets.zero`. + final EdgeInsets insetPadding; + + /// How to align the [StreamMessageDialog]. + /// + /// Defaults to [Alignment.center]. + final AlignmentGeometry alignment; + + @override + Widget build(BuildContext context) { + final effectivePadding = MediaQuery.viewInsetsOf(context) + insetPadding; + + final dialogChild = Align( + alignment: alignment, + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 280), + child: Material( + type: MaterialType.transparency, + child: Column( + spacing: spacing, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: alignment.toColumnCrossAxisAlignment(), + children: [ + if (headerBuilder case final builder?) builder(context), + Flexible(child: contentBuilder(context)), + ], + ), + ), + ), + ); + + Widget dialog = AnimatedPadding( + padding: effectivePadding, + duration: insetAnimationDuration, + curve: insetAnimationCurve, + child: MediaQuery.removeViewInsets( + removeLeft: true, + removeTop: true, + removeRight: true, + removeBottom: true, + context: context, + child: dialogChild, + ), + ); + + if (useSafeArea) { + dialog = Align( + alignment: alignment, + child: SingleChildScrollView( + hitTestBehavior: HitTestBehavior.translucent, + child: SafeArea(child: dialog), + ), + ); + } + + return dialog; + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_modal/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_modal/message_reactions_modal.dart new file mode 100644 index 0000000000..96920b3e9c --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_modal/message_reactions_modal.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; +import 'package:stream_chat_flutter/src/reactions/picker/reaction_picker_bubble_overlay.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template streamMessageReactionsModal} +/// A modal that displays message reactions and allows users to add reactions. +/// +/// This modal contains: +/// 1. A reaction picker (optional) that appears at the top +/// 2. The original message widget +/// 3. A display of all current reactions with user avatars +/// +/// The modal uses [StreamMessageDialog] as its base layout and customizes +/// both the header and content sections to display reaction-specific +/// information. +/// {@endtemplate} +class StreamMessageReactionsModal extends StatelessWidget { + /// {@macro streamMessageReactionsModal} + const StreamMessageReactionsModal({ + super.key, + required this.message, + required this.messageWidget, + this.reverse = false, + this.showReactionPicker = true, + this.reactionPickerBuilder = StreamReactionPicker.builder, + this.onReactionPicked, + this.onUserAvatarTap, + }); + + /// The message for which to display and manage reactions. + final Message message; + + /// The original message widget that will be displayed in the modal. + final Widget messageWidget; + + /// Whether the message should be displayed in reverse direction. + /// + /// This affects how the modal and reactions are displayed and aligned. + /// Set to `true` for right-aligned messages (typically the current user's). + /// Set to `false` for left-aligned messages (typically other users'). + /// + /// Defaults to `false`. + final bool reverse; + + /// Controls whether to show the reaction picker at the top of the modal. + /// + /// When `true`, users can add reactions directly from the modal. + /// When `false`, the reaction picker is hidden. + final bool showReactionPicker; + + /// {@macro reactionPickerBuilder} + final ReactionPickerBuilder reactionPickerBuilder; + + /// Callback triggered when a user adds or toggles a reaction. + /// + /// Provides the selected [Reaction] object to the callback. + final OnMessageActionTap? onReactionPicked; + + /// Callback triggered when a user avatar is tapped in the reactions list. + /// + /// Provides the [User] object associated with the tapped avatar. + final void Function(User)? onUserAvatarTap; + + @override + Widget build(BuildContext context) { + final alignment = switch (reverse) { + true => AlignmentDirectional.centerEnd, + false => AlignmentDirectional.centerStart, + }; + + final onReactionPicked = switch (this.onReactionPicked) { + null => null, + final onPicked => (reaction) { + return onPicked.call( + SelectReaction(message: message, reaction: reaction), + ); + }, + }; + + return StreamMessageDialog( + spacing: 4, + alignment: alignment, + headerBuilder: (context) { + final safeArea = MediaQuery.paddingOf(context); + + return Padding( + padding: EdgeInsets.only(top: safeArea.top), + child: ReactionPickerBubbleOverlay( + message: message, + reverse: reverse, + visible: showReactionPicker, + anchorOffset: const Offset(0, -8), + onReactionPicked: onReactionPicked, + reactionPickerBuilder: reactionPickerBuilder, + child: IgnorePointer(child: messageWidget), + ), + ); + }, + contentBuilder: (context) { + final reactions = message.latestReactions; + final hasReactions = reactions != null && reactions.isNotEmpty; + if (!hasReactions) return const Empty(); + + return FractionallySizedBox( + widthFactor: 0.78, + child: StreamUserReactions( + message: message, + onUserAvatarTap: onUserAvatarTap, + ), + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_modal/moderated_message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_modal/moderated_message_actions_modal.dart new file mode 100644 index 0000000000..da655979a7 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_modal/moderated_message_actions_modal.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/message_action/message_action.dart'; +import 'package:stream_chat_flutter/src/misc/adaptive_dialog_action.dart'; +import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// {@template moderatedMessageActionsModal} +/// A modal that is shown when a message is flagged by moderation policies. +/// +/// This modal allows users to: +/// - Send the message anyway, overriding the moderation warning +/// - Edit the message to comply with community guidelines +/// - Delete the message +/// +/// The modal provides clear guidance to users about the moderation issue +/// and options to address it. +/// {@endtemplate} +class ModeratedMessageActionsModal extends StatelessWidget { + /// {@macro moderatedMessageActionsModal} + const ModeratedMessageActionsModal({ + super.key, + required this.message, + required this.messageActions, + this.onActionTap, + }); + + /// The message object that actions will be performed on. + /// + /// This is the message the user selected to see available actions. + final Message message; + + /// List of custom actions that will be displayed in the modal. + /// + /// Each action is represented by a [StreamMessageAction] object which defines + /// the action's appearance and behavior. + final List messageActions; + + /// Callback triggered when a moderated message action is tapped. + /// + /// Provides the tapped [MessageAction] object to the callback. + final OnMessageActionTap? onActionTap; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final textTheme = theme.textTheme; + final colorTheme = theme.colorTheme; + + final actions = [ + ...messageActions.map( + (action) => AdaptiveDialogAction( + onPressed: switch (onActionTap) { + final onTap? => () => onTap.call(action.action), + _ => null, + }, + isDestructiveAction: action.isDestructive, + child: action.title ?? const Empty(), + ), + ), + ]; + + return AlertDialog.adaptive( + clipBehavior: Clip.antiAlias, + backgroundColor: colorTheme.barsBg, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + icon: const StreamSvgIcon(icon: StreamSvgIcons.flag), + iconColor: colorTheme.accentPrimary, + title: Text(context.translations.moderationReviewModalTitle), + titleTextStyle: textTheme.headline.copyWith( + color: colorTheme.textHighEmphasis, + ), + content: Text( + context.translations.moderationReviewModalDescription, + textAlign: TextAlign.center, + ), + contentTextStyle: textTheme.body.copyWith( + color: colorTheme.textLowEmphasis, + ), + actions: actions, + actionsAlignment: MainAxisAlignment.center, + actionsOverflowAlignment: OverflowBarAlignment.center, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/ephemeral_message.dart b/packages/stream_chat_flutter/lib/src/message_widget/ephemeral_message.dart index 076cb4117d..4f27668bb3 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/ephemeral_message.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/ephemeral_message.dart @@ -39,10 +39,9 @@ class StreamEphemeralMessage extends StatelessWidget { child: GiphyEphemeralMessage( message: message, onActionPressed: (name, value) { - streamChannel.channel.sendAction( - message, - {name: value}, - ); + return streamChannel.channel.sendAction(message, { + name: value, + }).ignore(); }, ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart index 62ebaada6d..6a0e214faa 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart @@ -17,7 +17,6 @@ class MessageCard extends StatefulWidget { required this.hasQuotedMessage, required this.hasUrlAttachments, required this.hasNonUrlAttachments, - required this.hasPoll, required this.isOnlyEmoji, required this.isGiphy, required this.attachmentBuilders, @@ -66,9 +65,6 @@ class MessageCard extends StatefulWidget { /// {@macro hasNonUrlAttachments} final bool hasNonUrlAttachments; - /// {@macro hasPoll} - final bool hasPoll; - /// {@macro isOnlyEmoji} final bool isOnlyEmoji; @@ -128,10 +124,6 @@ class _MessageCardState extends State { final attachmentsKey = GlobalKey(); double? widthLimit; - bool get hasAttachments { - return widget.hasUrlAttachments || widget.hasNonUrlAttachments; - } - void _updateWidthLimit() { final attachmentContext = attachmentsKey.currentContext; final renderBox = attachmentContext?.findRenderObject() as RenderBox?; @@ -150,11 +142,9 @@ class _MessageCardState extends State { // If there is an attachment, we need to wait for the attachment to be // rendered to get the width of the attachment and set it as the width // limit of the message card. - if (hasAttachments) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _updateWidthLimit(); - }); - } + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateWidthLimit(); + }); } @override @@ -164,9 +154,9 @@ class _MessageCardState extends State { return Container( constraints: const BoxConstraints().copyWith(maxWidth: widthLimit), - margin: EdgeInsets.symmetric( - horizontal: (widget.isFailedState ? 12.0 : 0.0) + - (widget.showUserAvatar == DisplayWidget.gone ? 0 : 4.0), + margin: EdgeInsetsDirectional.only( + end: widget.reverse && widget.isFailedState ? 12.0 : 0.0, + start: !widget.reverse && widget.isFailedState ? 12.0 : 0.0, ), clipBehavior: Clip.hardEdge, decoration: ShapeDecoration( @@ -201,23 +191,17 @@ class _MessageCardState extends State { hasNonUrlAttachments: widget.hasNonUrlAttachments, ), ), - if (hasAttachments) - ParseAttachments( - key: attachmentsKey, - message: widget.message, - attachmentBuilders: widget.attachmentBuilders, - attachmentPadding: widget.attachmentPadding, - attachmentShape: widget.attachmentShape, - onAttachmentTap: widget.onAttachmentTap, - onShowMessage: widget.onShowMessage, - onReplyTap: widget.onReplyTap, - attachmentActionsModalBuilder: - widget.attachmentActionsModalBuilder, - ), - if (widget.hasPoll) - PollMessage( - message: widget.message, - ), + ParseAttachments( + key: attachmentsKey, + message: widget.message, + attachmentBuilders: widget.attachmentBuilders, + attachmentPadding: widget.attachmentPadding, + attachmentShape: widget.attachmentShape, + onAttachmentTap: widget.onAttachmentTap, + onShowMessage: widget.onShowMessage, + onReplyTap: widget.onReplyTap, + attachmentActionsModalBuilder: widget.attachmentActionsModalBuilder, + ), TextBubble( messageTheme: widget.messageTheme, message: widget.message, diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index ebf18428fc..111d479f2c 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -1,16 +1,11 @@ -import 'package:flutter/material.dart' hide ButtonStyle; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_portal/flutter_portal.dart'; -import 'package:stream_chat_flutter/conditional_parent_builder/conditional_parent_builder.dart'; import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart'; import 'package:stream_chat_flutter/src/context_menu/context_menu.dart'; import 'package:stream_chat_flutter/src/context_menu/context_menu_region.dart'; -import 'package:stream_chat_flutter/src/context_menu_items/context_menu_reaction_picker.dart'; -import 'package:stream_chat_flutter/src/context_menu_items/stream_chat_context_menu_item.dart'; -import 'package:stream_chat_flutter/src/dialogs/dialogs.dart'; -import 'package:stream_chat_flutter/src/message_actions_modal/message_actions_modal.dart'; -import 'package:stream_chat_flutter/src/message_actions_modal/moderated_message_actions_modal.dart'; import 'package:stream_chat_flutter/src/message_widget/message_widget_content.dart'; +import 'package:stream_chat_flutter/src/misc/flexible_fractionally_sized_box.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// The display behaviour of a widget @@ -56,7 +51,6 @@ class StreamMessageWidget extends StatefulWidget { this.onReactionsTap, this.onReactionsHover, this.showReactionPicker = true, - this.showReactionTail, this.showUserAvatar = DisplayWidget.show, this.showSendingIndicator = true, this.showThreadReplyIndicator = false, @@ -98,11 +92,13 @@ class StreamMessageWidget extends StatefulWidget { this.widthFactor = 0.78, this.onQuotedMessageTap, this.customActions = const [], + this.onCustomActionTap, this.onAttachmentTap, this.imageAttachmentThumbnailSize = const Size(400, 400), this.imageAttachmentThumbnailResizeType = 'clip', this.imageAttachmentThumbnailCropType = 'center', this.attachmentActionsModalBuilder, + this.reactionPickerBuilder = StreamReactionPicker.builder, }); /// {@template onMentionTap} @@ -257,16 +253,10 @@ class StreamMessageWidget extends StatefulWidget { /// {@template showReactionPicker} /// Whether or not to show the reaction picker. - /// Used in [StreamMessageReactionsModal] and [MessageActionsModal]. + /// Used in [StreamMessageReactionsModal] and [StreamMessageActionsModal]. /// {@endtemplate} final bool showReactionPicker; - /// {@template showReactionPickerTail} - /// Whether or not to show the reaction picker tail. - /// This is calculated internally in most cases and does not need to be set. - /// {@endtemplate} - final bool? showReactionTail; - /// {@template onShowMessage} /// Callback when show message is tapped /// {@endtemplate} @@ -376,12 +366,20 @@ class StreamMessageWidget extends StatefulWidget { /// {@endtemplate} final List customActions; + /// Callback for when a custom message action is tapped. + /// + /// {@macro onMessageActionTap} + final OnMessageActionTap? onCustomActionTap; + /// {@macro onMessageWidgetAttachmentTap} final StreamAttachmentWidgetTapCallback? onAttachmentTap; /// {@macro attachmentActionsBuilder} final AttachmentActionsBuilder? attachmentActionsModalBuilder; + /// {@macro reactionPickerBuilder} + final ReactionPickerBuilder reactionPickerBuilder; + /// Size of the image attachment thumbnail. final Size imageAttachmentThumbnailSize; @@ -433,7 +431,6 @@ class StreamMessageWidget extends StatefulWidget { void Function(String)? onLinkTap, bool? showReactionBrowser, bool? showReactionPicker, - bool? showReactionTail, List? readList, ShowMessageCallback? onShowMessage, bool? showUsername, @@ -457,12 +454,14 @@ class StreamMessageWidget extends StatefulWidget { OnReactionsTap? onReactionsTap, OnReactionsHover? onReactionsHover, List? customActions, + OnMessageActionTap? onCustomActionTap, void Function(Message message, Attachment attachment)? onAttachmentTap, Widget Function(BuildContext, User)? userAvatarBuilder, Size? imageAttachmentThumbnailSize, String? imageAttachmentThumbnailResizeType, String? imageAttachmentThumbnailCropType, AttachmentActionsBuilder? attachmentActionsModalBuilder, + ReactionPickerBuilder? reactionPickerBuilder, }) { return StreamMessageWidget( key: key ?? this.key, @@ -501,7 +500,6 @@ class StreamMessageWidget extends StatefulWidget { onUserAvatarTap: onUserAvatarTap ?? this.onUserAvatarTap, onLinkTap: onLinkTap ?? this.onLinkTap, showReactionPicker: showReactionPicker ?? this.showReactionPicker, - showReactionTail: showReactionTail ?? this.showReactionTail, onShowMessage: onShowMessage ?? this.onShowMessage, showUsername: showUsername ?? this.showUsername, showTimestamp: showTimestamp ?? this.showTimestamp, @@ -525,6 +523,7 @@ class StreamMessageWidget extends StatefulWidget { onReactionsTap: onReactionsTap ?? this.onReactionsTap, onReactionsHover: onReactionsHover ?? this.onReactionsHover, customActions: customActions ?? this.customActions, + onCustomActionTap: onCustomActionTap ?? this.onCustomActionTap, onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap, userAvatarBuilder: userAvatarBuilder ?? this.userAvatarBuilder, imageAttachmentThumbnailSize: @@ -535,6 +534,8 @@ class StreamMessageWidget extends StatefulWidget { this.imageAttachmentThumbnailCropType, attachmentActionsModalBuilder: attachmentActionsModalBuilder ?? this.attachmentActionsModalBuilder, + reactionPickerBuilder: + reactionPickerBuilder ?? this.reactionPickerBuilder, ); } @@ -598,11 +599,6 @@ class _StreamMessageWidgetState extends State bool get hasNonUrlAttachments => widget.message.attachments .any((it) => it.type != AttachmentType.urlPreview); - /// {@template hasPoll} - /// `true` if the [message] contains a poll. - /// {@endtemplate} - bool get hasPoll => widget.message.poll != null; - /// {@template hasUrlAttachments} /// `true` if any of the [message]'s attachments are a giphy with a /// [Attachment.titleLink]. @@ -641,175 +637,129 @@ class _StreamMessageWidgetState extends State (widget.message.latestReactions?.isNotEmpty == true) && !widget.message.isDeleted; - bool get shouldShowReplyAction => - widget.showReplyMessage && !isFailedState && widget.onReplyTap != null; - - bool get shouldShowEditAction => - widget.showEditMessage && - !isDeleteFailed && - !hasPoll && - !widget.message.attachments - .any((element) => element.type == AttachmentType.giphy); - - bool get shouldShowResendAction => - widget.showResendMessage && (isSendFailed || isUpdateFailed); - - bool get shouldShowCopyAction => - widget.showCopyMessage && - !isFailedState && - widget.message.text?.trim().isNotEmpty == true; - - bool get shouldShowThreadReplyAction => - widget.showThreadReplyMessage && - !isFailedState && - widget.onThreadTap != null; - - bool get shouldShowDeleteAction => widget.showDeleteMessage || isDeleteFailed; - @override bool get wantKeepAlive => widget.message.attachments.isNotEmpty; - late StreamChatThemeData _streamChatTheme; - late StreamChatState _streamChat; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _streamChatTheme = StreamChatTheme.of(context); - _streamChat = StreamChat.of(context); - } - @override Widget build(BuildContext context) { super.build(context); + final theme = StreamChatTheme.of(context); + final streamChat = StreamChat.of(context); + final avatarWidth = widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40; final bottomRowPadding = widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; - final showReactions = shouldShowReactions; - - return ConditionalParentBuilder( - builder: (context, child) { - final message = widget.message; + return Portal( + child: Material( + color: switch (isPinned && widget.showPinHighlight) { + true => theme.colorTheme.highlight, + false => Colors.transparent, + }, + child: PlatformWidgetBuilder( + mobile: (context, child) { + final message = widget.message; + return InkWell( + onTap: switch (widget.onMessageTap) { + final onTap? => () => onTap(message), + _ => null, + }, + onLongPress: switch (widget.onMessageLongPress) { + final onLongPress? => () => onLongPress(message), + // If the message is not yet sent or deleted, we don't want + // to handle long press events by default. + _ when message.state.isDeleted => null, + _ when message.state.isOutgoing => null, + _ => () => _onMessageLongPressed(context, message), + }, + child: child, + ); + }, + desktopOrWeb: (context, child) { + final message = widget.message; + final messageState = message.state; - // If the message is deleted or not yet sent, we don't want to show any - // context menu actions. - if (message.state.isDeleted || message.state.isOutgoing) return child; + // If the message is deleted or not yet sent, we don't want to + // show any context menu actions. + if (messageState.isDeleted || messageState.isOutgoing) return child; - final menuItems = _buildDesktopOrWebActions(context, message); - if (menuItems.isEmpty) return child; + final menuItems = _buildDesktopOrWebActions(context, message); + if (menuItems.isEmpty) return MouseRegion(child: child); - return ContextMenuRegion( - contextMenuBuilder: (_, anchor) => ContextMenu( - anchor: anchor, - menuItems: menuItems, - ), - child: child, - ); - }, - child: Material( - type: MaterialType.transparency, - child: AnimatedContainer( - duration: const Duration(seconds: 1), - color: isPinned && widget.showPinHighlight - ? _streamChatTheme.colorTheme.highlight - // ignore: deprecated_member_use - : _streamChatTheme.colorTheme.barsBg.withOpacity(0), - child: Portal( - child: PlatformWidgetBuilder( - mobile: (context, child) { - final message = widget.message; - return InkWell( - onTap: switch (widget.onMessageTap) { - final onTap? => () => onTap(message), - _ => null, - }, - onLongPress: switch (widget.onMessageLongPress) { - final onLongPress? => () => onLongPress(message), - // If the message is not yet sent or deleted, we don't want - // to handle long press events by default. - _ when message.state.isDeleted => null, - _ when message.state.isOutgoing => null, - _ => () => _onMessageLongPressed(context, message), - }, - child: child, - ); - }, - desktop: (_, child) => MouseRegion(child: child), - web: (_, child) => MouseRegion(child: child), - child: Padding( - padding: widget.padding ?? const EdgeInsets.all(8), - child: FractionallySizedBox( - alignment: widget.reverse - ? Alignment.centerRight - : Alignment.centerLeft, - widthFactor: widget.widthFactor, - child: Builder(builder: (context) { - return MessageWidgetContent( - streamChatTheme: _streamChatTheme, - showUsername: showUsername, - showTimeStamp: showTimeStamp, - showEditedLabel: showEditedLabel, - showThreadReplyIndicator: showThreadReplyIndicator, - showSendingIndicator: showSendingIndicator, - showInChannel: showInChannel, - isGiphy: isGiphy, - isOnlyEmoji: isOnlyEmoji, - hasUrlAttachments: hasUrlAttachments, - messageTheme: widget.messageTheme, - reverse: widget.reverse, - message: widget.message, - hasNonUrlAttachments: hasNonUrlAttachments, - hasPoll: hasPoll, - hasQuotedMessage: hasQuotedMessage, - textPadding: widget.textPadding, - attachmentBuilders: widget.attachmentBuilders, - attachmentPadding: widget.attachmentPadding, - attachmentShape: widget.attachmentShape, - onAttachmentTap: widget.onAttachmentTap, - onReplyTap: widget.onReplyTap, - onThreadTap: widget.onThreadTap, - onShowMessage: widget.onShowMessage, - attachmentActionsModalBuilder: - widget.attachmentActionsModalBuilder, - avatarWidth: avatarWidth, - bottomRowPadding: bottomRowPadding, - isFailedState: isFailedState, - isPinned: isPinned, - messageWidget: widget, - showBottomRow: showBottomRow, - showPinHighlight: widget.showPinHighlight, - showReactionPickerTail: calculateReactionTailEnabled( - ReactionTailType.list, - ), - showReactions: showReactions, - onReactionsTap: () { - final message = widget.message; - return switch (widget.onReactionsTap) { - final onReactionsTap? => onReactionsTap(message), - _ => _showMessageReactionsModal(context, message), - }; - }, - onReactionsHover: widget.onReactionsHover, - showUserAvatar: widget.showUserAvatar, - streamChat: _streamChat, - translateUserAvatar: widget.translateUserAvatar, - shape: widget.shape, - borderSide: widget.borderSide, - borderRadiusGeometry: widget.borderRadiusGeometry, - textBuilder: widget.textBuilder, - quotedMessageBuilder: widget.quotedMessageBuilder, - onLinkTap: widget.onLinkTap, - onMentionTap: widget.onMentionTap, - onQuotedMessageTap: widget.onQuotedMessageTap, - bottomRowBuilderWithDefaultWidget: - widget.bottomRowBuilderWithDefaultWidget, - onUserAvatarTap: widget.onUserAvatarTap, - userAvatarBuilder: widget.userAvatarBuilder, - ); - }), - ), + return ContextMenuRegion( + contextMenuBuilder: (_, anchor) => ContextMenu( + anchor: anchor, + menuItems: menuItems, + ), + child: MouseRegion(child: child), + ); + }, + child: FlexibleFractionallySizedBox( + widthFactor: widget.widthFactor, + alignment: switch (widget.reverse) { + true => AlignmentDirectional.centerEnd, + false => AlignmentDirectional.centerStart, + }, + child: Padding( + padding: widget.padding ?? const EdgeInsets.all(8), + child: MessageWidgetContent( + streamChatTheme: theme, + showUsername: showUsername, + showTimeStamp: showTimeStamp, + showEditedLabel: showEditedLabel, + showThreadReplyIndicator: showThreadReplyIndicator, + showSendingIndicator: showSendingIndicator, + showInChannel: showInChannel, + isGiphy: isGiphy, + isOnlyEmoji: isOnlyEmoji, + hasUrlAttachments: hasUrlAttachments, + messageTheme: widget.messageTheme, + reverse: widget.reverse, + message: widget.message, + hasNonUrlAttachments: hasNonUrlAttachments, + hasQuotedMessage: hasQuotedMessage, + textPadding: widget.textPadding, + attachmentBuilders: widget.attachmentBuilders, + attachmentPadding: widget.attachmentPadding, + attachmentShape: widget.attachmentShape, + onAttachmentTap: widget.onAttachmentTap, + onReplyTap: widget.onReplyTap, + onThreadTap: widget.onThreadTap, + onShowMessage: widget.onShowMessage, + attachmentActionsModalBuilder: + widget.attachmentActionsModalBuilder, + avatarWidth: avatarWidth, + bottomRowPadding: bottomRowPadding, + isFailedState: isFailedState, + isPinned: isPinned, + messageWidget: widget, + showBottomRow: showBottomRow, + showPinHighlight: widget.showPinHighlight, + showReactions: shouldShowReactions, + onReactionsTap: () { + final message = widget.message; + return switch (widget.onReactionsTap) { + final onReactionsTap? => onReactionsTap(message), + _ => _showMessageReactionsModal(context, message), + }; + }, + onReactionsHover: widget.onReactionsHover, + showUserAvatar: widget.showUserAvatar, + streamChat: streamChat, + translateUserAvatar: widget.translateUserAvatar, + shape: widget.shape, + borderSide: widget.borderSide, + borderRadiusGeometry: widget.borderRadiusGeometry, + textBuilder: widget.textBuilder, + quotedMessageBuilder: widget.quotedMessageBuilder, + onLinkTap: widget.onLinkTap, + onMentionTap: widget.onMentionTap, + onQuotedMessageTap: widget.onQuotedMessageTap, + bottomRowBuilderWithDefaultWidget: + widget.bottomRowBuilderWithDefaultWidget, + onUserAvatarTap: widget.onUserAvatarTap, + userAvatarBuilder: widget.userAvatarBuilder, ), ), ), @@ -818,6 +768,49 @@ class _StreamMessageWidgetState extends State ); } + List _buildBouncedErrorMessageActions({ + required BuildContext context, + required Message message, + }) { + final actions = StreamMessageActionsBuilder.buildBouncedErrorActions( + context: context, + message: message, + ); + + return actions; + } + + List _buildMessageActions({ + required BuildContext context, + required Message message, + required Channel channel, + OwnUser? currentUser, + List? customActions, + }) { + final actions = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channel, + currentUser: currentUser, + customActions: customActions, + )..retainWhere( + (it) => switch (it.action) { + QuotedReply() => widget.showReplyMessage, + ThreadReply() => widget.showThreadReplyMessage, + MarkUnread() => widget.showMarkUnreadMessage, + ResendMessage() => widget.showResendMessage, + EditMessage() => widget.showEditMessage, + CopyMessage() => widget.showCopyMessage, + FlagMessage() => widget.showFlagButton, + PinMessage() => widget.showPinButton, + DeleteMessage() => widget.showDeleteMessage, + _ => true, // Retain all the remaining actions. + }, + ); + + return actions; + } + List _buildDesktopOrWebActions( BuildContext context, Message message, @@ -833,48 +826,23 @@ class _StreamMessageWidgetState extends State BuildContext context, Message message, ) { - final theme = StreamChatTheme.of(context); final channel = StreamChannel.of(context).channel; + final actions = _buildBouncedErrorMessageActions( + context: context, + message: message, + ); + return [ - StreamChatContextMenuItem( - leading: StreamSvgIcon( - icon: StreamSvgIcons.circleUp, - color: theme.colorTheme.accentPrimary, - ), - title: Text(context.translations.sendAnywayLabel), - onClick: () { - Navigator.of(context, rootNavigator: true).pop(); - channel.sendMessage(message).ignore(); - }, - ), - StreamChatContextMenuItem( - leading: const StreamSvgIcon(icon: StreamSvgIcons.edit), - title: Text(context.translations.editMessageLabel), - onClick: () { - Navigator.of(context, rootNavigator: true).pop(); - showEditMessageSheet( - context: context, - channel: channel, - message: message, - editMessageInputBuilder: widget.editMessageInputBuilder, - ); - }, - ), - StreamChatContextMenuItem( - leading: StreamSvgIcon( - icon: StreamSvgIcons.delete, - color: theme.colorTheme.accentError, - ), - title: Text( - context.translations.deleteMessageLabel, - style: TextStyle(color: theme.colorTheme.accentError), - ), - onClick: () { - Navigator.of(context, rootNavigator: true).pop(); - channel.deleteMessage(message, hard: true).ignore(); - }, - ), + ...actions.map((action) { + return StreamMessageActionItem( + action: action, + onTap: (action) async { + final popped = await Navigator.of(context).maybePop(); + if (popped) return _onActionTap(context, channel, action).ignore(); + }, + ); + }), ]; } @@ -882,203 +850,91 @@ class _StreamMessageWidgetState extends State BuildContext context, Message message, ) { - final theme = StreamChatTheme.of(context); final channel = StreamChannel.of(context).channel; + final currentUser = channel.client.state.currentUser; + final showPicker = widget.showReactionPicker && channel.canSendReaction; + + final actions = _buildMessageActions( + context: context, + message: message, + channel: channel, + currentUser: currentUser, + customActions: widget.customActions, + ); + + void onActionTap(MessageAction action) async { + final popped = await Navigator.of(context).maybePop(); + if (popped) return _onActionTap(context, channel, action).ignore(); + } return [ - if (widget.showReactionPicker) - StreamChatContextMenuItem( - child: StreamChannel( - channel: channel, - child: ContextMenuReactionPicker(message: message), + if (showPicker) + widget.reactionPickerBuilder( + context, + message, + (reaction) => onActionTap( + SelectReaction(message: message, reaction: reaction), ), ), - if (shouldShowReplyAction) ...[ - StreamChatContextMenuItem( - leading: const StreamSvgIcon(icon: StreamSvgIcons.reply), - title: Text(context.translations.replyLabel), - onClick: () { - Navigator.of(context, rootNavigator: true).pop(); - widget.onReplyTap?.call(message); - }, - ), - ], - if (widget.showMarkUnreadMessage) - StreamChatContextMenuItem( - leading: const StreamSvgIcon(icon: StreamSvgIcons.messageUnread), - title: Text(context.translations.markAsUnreadLabel), - onClick: () async { - Navigator.of(context, rootNavigator: true).pop(); - try { - await channel.markUnread(message.id); - } catch (ex) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.translations.markUnreadError, - ), - ), - ); - } - }, - ), - if (shouldShowThreadReplyAction) - StreamChatContextMenuItem( - leading: const StreamSvgIcon(icon: StreamSvgIcons.threadReply), - title: Text(context.translations.threadReplyLabel), - onClick: () { - Navigator.of(context, rootNavigator: true).pop(); - widget.onThreadTap?.call(message); - }, - ), - if (shouldShowCopyAction) - StreamChatContextMenuItem( - leading: const StreamSvgIcon(icon: StreamSvgIcons.copy), - title: Text(context.translations.copyMessageLabel), - onClick: () { - Navigator.of(context, rootNavigator: true).pop(); - final copiedMessage = message.replaceMentions(linkify: false); - if (copiedMessage.text case final text?) { - Clipboard.setData(ClipboardData(text: text)); - } - }, - ), - if (shouldShowEditAction) ...[ - StreamChatContextMenuItem( - leading: const StreamSvgIcon(icon: StreamSvgIcons.edit), - title: Text(context.translations.editMessageLabel), - onClick: () { - Navigator.of(context, rootNavigator: true).pop(); - showEditMessageSheet( - context: context, - channel: channel, - message: message, - editMessageInputBuilder: widget.editMessageInputBuilder, - ); - }, - ), - ], - if (widget.showPinButton) - StreamChatContextMenuItem( - leading: const StreamSvgIcon(icon: StreamSvgIcons.pin), - title: Text( - context.translations.togglePinUnpinText(pinned: isPinned), - ), - onClick: () async { - Navigator.of(context, rootNavigator: true).pop(); - try { - await switch (isPinned) { - true => channel.unpinMessage(message), - false => channel.pinMessage(message), - }; - } catch (e) { - throw Exception(e); - } - }, - ), - if (shouldShowResendAction) - StreamChatContextMenuItem( - leading: const StreamSvgIcon(icon: StreamSvgIcons.sendMessage), - title: Text( - context.translations.toggleResendOrResendEditedMessage( - isUpdateFailed: message.state.isUpdatingFailed, - ), - ), - onClick: () async { - Navigator.of(context, rootNavigator: true).pop(); - await channel.retryMessage(message); - }, - ), - if (shouldShowDeleteAction) - StreamChatContextMenuItem( - leading: StreamSvgIcon( - color: theme.colorTheme.accentError, - icon: StreamSvgIcons.delete, - ), - title: Text( - context.translations.deleteMessageLabel, - style: TextStyle(color: theme.colorTheme.accentError), - ), - onClick: () async { - Navigator.of(context, rootNavigator: true).pop(); - final deleted = await showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const DeleteMessageDialog(), - ); - if (deleted == true) { - try { - await switch (widget.onConfirmDeleteTap) { - final onConfirmDeleteTap? => onConfirmDeleteTap(message), - _ => channel.deleteMessage(message), - }; - } catch (e) { - showDialog( - context: context, - builder: (_) => const MessageDialog(), - ); - } - } - }, - ), - ...widget.customActions.map( - (e) => StreamChatContextMenuItem( - leading: e.leading, - title: e.title, - onClick: () => e.onTap?.call(message), - ), - ), + ...actions.map((action) { + return StreamMessageActionItem( + action: action, + onTap: onActionTap, + ); + }), ]; } - void _showMessageReactionsModal( + Future _showMessageReactionsModal( BuildContext context, Message message, ) { final channel = StreamChannel.of(context).channel; + final showPicker = widget.showReactionPicker && channel.canSendReaction; - showDialog( - useRootNavigator: false, + void onReactionPicked(SelectReaction action) async { + final popped = await Navigator.of(context).maybePop(); + if (popped) return _onActionTap(context, channel, action).ignore(); + } + + return showStreamDialog( context: context, - useSafeArea: false, - barrierColor: _streamChatTheme.colorTheme.overlay, - builder: (context) => StreamChannel( - channel: channel, + useRootNavigator: false, + builder: (_) => StreamChatConfiguration( + // This is needed to provide the nearest reaction icons to the + // StreamMessageReactionsModal. + data: StreamChatConfiguration.of(context), child: StreamMessageReactionsModal( message: message, - showReactionPicker: widget.showReactionPicker, - messageWidget: widget.copyWith( - key: const Key('MessageWidget'), - message: message.copyWith( - text: (message.text?.length ?? 0) > 200 - ? '${message.text!.substring(0, 200)}...' - : message.text, - ), - showReactions: false, - showReactionTail: calculateReactionTailEnabled( - ReactionTailType.reactions, + reverse: widget.reverse, + onUserAvatarTap: widget.onUserAvatarTap, + showReactionPicker: showPicker, + reactionPickerBuilder: widget.reactionPickerBuilder, + onReactionPicked: onReactionPicked, + messageWidget: StreamChannel( + channel: channel, + child: widget.copyWith( + key: const Key('MessageWidget'), + message: message.trimmed, + showReactions: false, + showUsername: false, + showTimestamp: false, + translateUserAvatar: false, + showSendingIndicator: false, + padding: EdgeInsets.zero, + showPinHighlight: false, + showUserAvatar: switch (widget.reverse) { + true => DisplayWidget.gone, + false => DisplayWidget.show, + }, ), - showUsername: false, - showTimestamp: false, - translateUserAvatar: false, - showSendingIndicator: false, - padding: EdgeInsets.zero, - showReactionPicker: widget.showReactionPicker, - showPinHighlight: false, - showUserAvatar: - message.user!.id == channel.client.state.currentUser!.id - ? DisplayWidget.gone - : DisplayWidget.show, ), - onUserAvatarTap: widget.onUserAvatarTap, - messageTheme: widget.messageTheme, - reverse: widget.reverse, ), ), ); } - void _onMessageLongPressed( + Future _onMessageLongPressed( BuildContext context, Message message, ) { @@ -1089,10 +945,10 @@ class _StreamMessageWidgetState extends State return _onMessageActions(context, message); } - void _onBouncedErrorMessageActions( + Future _onBouncedErrorMessageActions( BuildContext context, Message message, - ) { + ) async { if (widget.onBouncedErrorMessageActions case final onActions?) { return onActions(context, message); } @@ -1100,42 +956,37 @@ class _StreamMessageWidgetState extends State return _showBouncedErrorMessageActionsDialog(context, message); } - void _showBouncedErrorMessageActionsDialog( + Future _showBouncedErrorMessageActionsDialog( BuildContext context, Message message, - ) { + ) async { final channel = StreamChannel.of(context).channel; - showDialog( + final actions = _buildBouncedErrorMessageActions( context: context, - builder: (context) { - return ModeratedMessageActionsModal( - onSendAnyway: () { - Navigator.of(context).pop(); - channel.sendMessage(widget.message).ignore(); - }, - onEditMessage: () { - Navigator.of(context).pop(); - showEditMessageSheet( - context: context, - channel: channel, - message: widget.message, - editMessageInputBuilder: widget.editMessageInputBuilder, - ); - }, - onDeleteMessage: () { - Navigator.of(context).pop(); - channel.deleteMessage(message, hard: true).ignore(); - }, - ); - }, + message: message, + ); + + void onActionTap(MessageAction action) async { + final popped = await Navigator.of(context).maybePop(); + if (popped) return _onActionTap(context, channel, action).ignore(); + } + + return showStreamDialog( + context: context, + useRootNavigator: false, + builder: (_) => ModeratedMessageActionsModal( + message: message, + messageActions: actions, + onActionTap: onActionTap, + ), ); } - void _onMessageActions( + Future _onMessageActions( BuildContext context, Message message, - ) { + ) async { if (widget.onMessageActions case final onActions?) { return onActions(context, message); } @@ -1143,107 +994,214 @@ class _StreamMessageWidgetState extends State return _showMessageActionModalDialog(context, message); } - void _showMessageActionModalDialog( + Future _showMessageActionModalDialog( BuildContext context, Message message, ) { final channel = StreamChannel.of(context).channel; + final currentUser = channel.client.state.currentUser; + final showPicker = widget.showReactionPicker && channel.canSendReaction; - showDialog( - useRootNavigator: false, + final actions = _buildMessageActions( + context: context, + message: message, + channel: channel, + currentUser: currentUser, + customActions: widget.customActions, + ); + + void onActionTap(MessageAction action) async { + final popped = await Navigator.of(context).maybePop(); + if (popped) return _onActionTap(context, channel, action).ignore(); + } + + return showStreamDialog( context: context, - useSafeArea: false, - barrierColor: _streamChatTheme.colorTheme.overlay, - builder: (context) { - return StreamChannel( - channel: channel, - child: MessageActionsModal( - message: message, - messageWidget: widget.copyWith( + useRootNavigator: false, + builder: (_) => StreamChatConfiguration( + // This is needed to provide the nearest reaction icons to the + // StreamMessageActionsModal. + data: StreamChatConfiguration.of(context), + child: StreamMessageActionsModal( + message: message, + reverse: widget.reverse, + messageActions: actions, + showReactionPicker: showPicker, + reactionPickerBuilder: widget.reactionPickerBuilder, + onActionTap: onActionTap, + messageWidget: StreamChannel( + channel: channel, + child: widget.copyWith( key: const Key('MessageWidget'), - message: message.copyWith( - text: (message.text?.length ?? 0) > 200 - ? '${message.text!.substring(0, 200)}...' - : message.text, - ), + message: message.trimmed, showReactions: false, - showReactionTail: calculateReactionTailEnabled( - ReactionTailType.messageActions, - ), showUsername: false, showTimestamp: false, translateUserAvatar: false, showSendingIndicator: false, padding: EdgeInsets.zero, showPinHighlight: false, - showUserAvatar: - message.user!.id == channel.client.state.currentUser!.id - ? DisplayWidget.gone - : DisplayWidget.show, + showUserAvatar: switch (widget.reverse) { + true => DisplayWidget.gone, + false => DisplayWidget.show, + }, ), - onEditMessageTap: (message) { - Navigator.of(context).pop(); - showEditMessageSheet( - context: context, - channel: channel, - message: message, - editMessageInputBuilder: widget.editMessageInputBuilder, - ); - }, - onCopyTap: (message) { - Navigator.of(context).pop(); - final copiedMessage = message.replaceMentions(linkify: false); - if (copiedMessage.text case final text?) { - Clipboard.setData(ClipboardData(text: text)); - } - }, - messageTheme: widget.messageTheme, - reverse: widget.reverse, - showDeleteMessage: shouldShowDeleteAction, - onConfirmDeleteTap: widget.onConfirmDeleteTap, - editMessageInputBuilder: widget.editMessageInputBuilder, - onReplyTap: widget.onReplyTap, - onThreadReplyTap: widget.onThreadTap, - showResendMessage: shouldShowResendAction, - showCopyMessage: shouldShowCopyAction, - showEditMessage: shouldShowEditAction, - showReactionPicker: widget.showReactionPicker, - showReplyMessage: shouldShowReplyAction, - showThreadReplyMessage: shouldShowThreadReplyAction, - showFlagButton: widget.showFlagButton, - showPinButton: widget.showPinButton, - showMarkUnreadMessage: widget.showMarkUnreadMessage, - customActions: widget.customActions, ), - ); - }, + ), + ), ); } - /// Calculates if the reaction picker tail should be enabled. - bool calculateReactionTailEnabled(ReactionTailType type) { - if (widget.showReactionTail != null) return widget.showReactionTail!; - - switch (type) { - case ReactionTailType.list: - return false; - case ReactionTailType.messageActions: - return widget.showReactionPicker; - case ReactionTailType.reactions: - return widget.showReactionPicker; + Future _onActionTap( + BuildContext context, + Channel channel, + MessageAction action, + ) async { + return switch (action) { + SelectReaction() => + _selectReaction(context, action.message, channel, action.reaction), + CopyMessage() => _copyMessage(action.message, channel), + DeleteMessage() => _maybeDeleteMessage(context, action.message, channel), + HardDeleteMessage() => channel.deleteMessage(action.message, hard: true), + EditMessage() => _editMessage(context, action.message, channel), + FlagMessage() => _maybeFlagMessage(context, action.message, channel), + MarkUnread() => channel.markUnread(action.message.id), + MuteUser() => channel.client.muteUser(action.user.id), + UnmuteUser() => channel.client.unmuteUser(action.user.id), + PinMessage() => channel.pinMessage(action.message), + UnpinMessage() => channel.unpinMessage(action.message), + ResendMessage() => channel.retryMessage(action.message), + QuotedReply() => widget.onReplyTap?.call(action.message), + ThreadReply() => widget.onThreadTap?.call(action.message), + CustomMessageAction() => widget.onCustomActionTap?.call(action), + }; + } + + Future _copyMessage( + Message message, + Channel channel, + ) async { + final presentableMessage = message.replaceMentions(linkify: false); + + final messageText = presentableMessage.text; + if (messageText == null || messageText.isEmpty) return; + + return Clipboard.setData(ClipboardData(text: messageText)); + } + + Future _maybeDeleteMessage( + BuildContext context, + Message message, + Channel channel, + ) async { + final confirmDelete = await showStreamDialog( + context: context, + builder: (context) => StreamMessageActionConfirmationModal( + title: Text(context.translations.deleteMessageLabel), + content: Text(context.translations.deleteMessageQuestion), + cancelActionTitle: Text(context.translations.cancelLabel.sentenceCase), + confirmActionTitle: Text(context.translations.deleteLabel.sentenceCase), + isDestructiveAction: true, + ), + ); + + if (confirmDelete == false) return null; + + return channel.deleteMessage(message); + } + + Future _editMessage( + BuildContext context, + Message message, + Channel channel, + ) { + return showEditMessageSheet( + context: context, + channel: channel, + message: message, + editMessageInputBuilder: widget.editMessageInputBuilder, + ); + } + + Future _maybeFlagMessage( + BuildContext context, + Message message, + Channel channel, + ) async { + final confirmFlag = await showStreamDialog( + context: context, + builder: (context) => StreamMessageActionConfirmationModal( + title: Text(context.translations.flagMessageLabel), + content: Text(context.translations.flagMessageQuestion), + cancelActionTitle: Text(context.translations.cancelLabel.sentenceCase), + confirmActionTitle: Text(context.translations.flagLabel.sentenceCase), + isDestructiveAction: true, + ), + ); + + if (confirmFlag == false) return null; + + final messageId = message.id; + return channel.client.flagMessage(messageId); + } + + Future _selectReaction( + BuildContext context, + Message message, + Channel channel, + Reaction reaction, + ) { + final ownReactions = [...?message.ownReactions]; + final shouldDelete = ownReactions.any((it) => it.type == reaction.type); + + if (shouldDelete) { + return channel.deleteReaction(message, reaction); } + + final configurations = StreamChatConfiguration.of(context); + final enforceUnique = configurations.enforceUniqueReactions; + + return channel.sendReaction( + message, + reaction, + enforceUnique: enforceUnique, + ); + } +} + +extension on Message { + Message get trimmed { + final trimmedText = switch (text) { + final text? when text.length > 100 => '${text.substring(0, 100)}...', + _ => text, + }; + + return copyWith( + text: trimmedText, + poll: poll?.trimmed, + quotedMessage: quotedMessage?.trimmed, + ); + } +} + +extension on Poll { + Poll get trimmed { + final trimmedName = switch (name) { + final name when name.length > 100 => '${name.substring(0, 100)}...', + _ => name, + }; + + return copyWith(name: trimmedName); } } -/// Enum for declaring the location of the message for which the reaction picker -/// is to be enabled. -enum ReactionTailType { - /// Message is in the [StreamMessageListView] - list, +extension on String { + String get sentenceCase { + if (isEmpty) return this; - /// Message is in the [MessageActionsModal] - messageActions, + final firstChar = this[0].toUpperCase(); + final restOfString = substring(1).toLowerCase(); - /// Message is in the message reactions modal - reactions, + return '$firstChar$restOfString'; + } } diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart index a6ae651476..f56e636cdc 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_portal/flutter_portal.dart'; import 'package:meta/meta.dart'; -import 'package:stream_chat_flutter/src/message_widget/reactions/desktop_reactions_builder.dart'; +import 'package:stream_chat_flutter/src/reactions/desktop_reactions_builder.dart'; +import 'package:stream_chat_flutter/src/reactions/indicator/reaction_indicator_bubble_overlay.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Signature for the builder function that will be called when the message @@ -42,7 +42,6 @@ class MessageWidgetContent extends StatelessWidget { required this.hasQuotedMessage, required this.hasUrlAttachments, required this.hasNonUrlAttachments, - required this.hasPoll, required this.isOnlyEmoji, required this.isGiphy, required this.attachmentBuilders, @@ -53,7 +52,6 @@ class MessageWidgetContent extends StatelessWidget { required this.onReplyTap, required this.attachmentActionsModalBuilder, required this.textPadding, - required this.showReactionPickerTail, required this.translateUserAvatar, required this.bottomRowPadding, required this.showInChannel, @@ -138,9 +136,6 @@ class MessageWidgetContent extends StatelessWidget { /// {@macro hasNonUrlAttachments} final bool hasNonUrlAttachments; - /// {@macro hasPoll} - final bool hasPoll; - /// {@macro isOnlyEmoji} final bool isOnlyEmoji; @@ -189,9 +184,6 @@ class MessageWidgetContent extends StatelessWidget { /// {@macro quotedMessageBuilder} final Widget Function(BuildContext, Message)? quotedMessageBuilder; - /// {@macro showReactionPickerTail} - final bool showReactionPickerTail; - /// {@macro translateUserAvatar} final bool translateUserAvatar; @@ -265,146 +257,58 @@ class MessageWidgetContent extends StatelessWidget { currentUser: streamChat.currentUser!, ), Row( - crossAxisAlignment: CrossAxisAlignment.end, + spacing: 8, mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (!reverse && - showUserAvatar == DisplayWidget.show && - message.user != null) ...[ - UserAvatarTransform( - onUserAvatarTap: onUserAvatarTap, - userAvatarBuilder: userAvatarBuilder, - translateUserAvatar: translateUserAvatar, - messageTheme: messageTheme, - message: message, - ), - const SizedBox(width: 4), - ], - if (showUserAvatar == DisplayWidget.hide) - SizedBox(width: avatarWidth + 4), - Flexible( - child: PortalTarget( - visible: isMobileDevice && showReactions, - portalFollower: isMobileDevice && showReactions - ? ReactionIndicator( - message: message, - messageTheme: messageTheme, - ownId: streamChat.currentUser!.id, - reverse: reverse, - onTap: onReactionsTap, - ) - : null, - anchor: Aligned( - follower: Alignment( - reverse ? 1 : -1, - -1, - ), - target: Alignment( - reverse ? -1 : 1, - -1, + ...[ + Flexible( + child: ReactionIndicatorBubbleOverlay( + reverse: reverse, + message: message, + onTap: onReactionsTap, + visible: isMobileDevice && showReactions, + anchorOffset: const Offset(0, 36), + childSizeDelta: switch (showUserAvatar) { + DisplayWidget.gone => Offset.zero, + // Size adjustment for the user avatar + _ => const Offset(40, 0), + }, + child: Padding( + padding: switch (showReactions) { + true => const EdgeInsets.only(top: 28), + false => EdgeInsets.zero, + }, + child: _buildMessageCard(context), ), ), - child: Stack( - clipBehavior: Clip.none, - children: [ - Padding( - padding: showReactions - ? const EdgeInsets.only(top: 18) - : EdgeInsets.zero, - child: (message.isDeleted && !isFailedState) - ? Container( - margin: EdgeInsets.symmetric( - horizontal: showUserAvatar == - DisplayWidget.gone - ? 0 - : 4.0, - ), - child: StreamDeletedMessage( - borderRadiusGeometry: - borderRadiusGeometry, - borderSide: borderSide, - shape: shape, - messageTheme: messageTheme, - ), - ) - : MessageCard( - message: message, - isFailedState: isFailedState, - showUserAvatar: showUserAvatar, - messageTheme: messageTheme, - hasQuotedMessage: hasQuotedMessage, - hasUrlAttachments: hasUrlAttachments, - hasNonUrlAttachments: - hasNonUrlAttachments, - hasPoll: hasPoll, - isOnlyEmoji: isOnlyEmoji, - isGiphy: isGiphy, - attachmentBuilders: attachmentBuilders, - attachmentPadding: attachmentPadding, - attachmentShape: attachmentShape, - onAttachmentTap: onAttachmentTap, - onReplyTap: onReplyTap, - onShowMessage: onShowMessage, - attachmentActionsModalBuilder: - attachmentActionsModalBuilder, - textPadding: textPadding, - reverse: reverse, - onQuotedMessageTap: onQuotedMessageTap, - onMentionTap: onMentionTap, - onLinkTap: onLinkTap, - textBuilder: textBuilder, - quotedMessageBuilder: - quotedMessageBuilder, - borderRadiusGeometry: - borderRadiusGeometry, - borderSide: borderSide, - shape: shape, - ), - ), - // TODO: Make tail part of the Reaction Picker. - if (showReactionPickerTail) - Positioned( - right: reverse ? null : 4, - left: reverse ? 4 : null, - top: -8, - child: CustomPaint( - painter: ReactionBubblePainter( - streamChatTheme.colorTheme.barsBg, - Colors.transparent, - Colors.transparent, - tailCirclesSpace: 1, - flipTail: !reverse, - ), - ), - ), - ], - ), ), + ].addConditionally( + reverse: reverse, + condition: (_) => message.user != null, + switch (showUserAvatar) { + DisplayWidget.gone => null, + DisplayWidget.hide => SizedBox(width: avatarWidth), + DisplayWidget.show => UserAvatarTransform( + onUserAvatarTap: onUserAvatarTap, + userAvatarBuilder: userAvatarBuilder, + translateUserAvatar: translateUserAvatar, + messageTheme: messageTheme, + message: message, + ), + }, ), - if (reverse && - showUserAvatar == DisplayWidget.show && - message.user != null) ...[ - UserAvatarTransform( - onUserAvatarTap: onUserAvatarTap, - userAvatarBuilder: userAvatarBuilder, - translateUserAvatar: translateUserAvatar, - messageTheme: messageTheme, - message: message, - ), - const SizedBox(width: 4), - ], - if (showUserAvatar == DisplayWidget.hide) - SizedBox(width: avatarWidth + 4), ], ), if (isDesktopDeviceOrWeb && showReactions) ...[ Padding( - padding: showUserAvatar != DisplayWidget.gone - ? EdgeInsets.only( - left: avatarWidth + 4, - right: avatarWidth + 4, - ) - : EdgeInsets.zero, + padding: switch (showUserAvatar) { + DisplayWidget.gone => EdgeInsets.zero, + _ => EdgeInsets.only( + left: avatarWidth + 4, + right: avatarWidth + 4, + ) + }, child: DesktopReactionsBuilder( message: message, messageTheme: messageTheme, @@ -437,6 +341,52 @@ class MessageWidgetContent extends StatelessWidget { ); } + Widget _buildMessageCard(BuildContext context) { + if (message.isDeleted) { + return Container( + margin: EdgeInsetsDirectional.only( + end: reverse && isFailedState ? 12.0 : 0.0, + start: !reverse && isFailedState ? 12.0 : 0.0, + ), + child: StreamDeletedMessage( + borderRadiusGeometry: borderRadiusGeometry, + borderSide: borderSide, + shape: shape, + messageTheme: messageTheme, + ), + ); + } + + return MessageCard( + message: message, + isFailedState: isFailedState, + showUserAvatar: showUserAvatar, + messageTheme: messageTheme, + hasQuotedMessage: hasQuotedMessage, + hasUrlAttachments: hasUrlAttachments, + hasNonUrlAttachments: hasNonUrlAttachments, + isOnlyEmoji: isOnlyEmoji, + isGiphy: isGiphy, + attachmentBuilders: attachmentBuilders, + attachmentPadding: attachmentPadding, + attachmentShape: attachmentShape, + onAttachmentTap: onAttachmentTap, + onReplyTap: onReplyTap, + onShowMessage: onShowMessage, + attachmentActionsModalBuilder: attachmentActionsModalBuilder, + textPadding: textPadding, + reverse: reverse, + onQuotedMessageTap: onQuotedMessageTap, + onMentionTap: onMentionTap, + onLinkTap: onLinkTap, + textBuilder: textBuilder, + quotedMessageBuilder: quotedMessageBuilder, + borderRadiusGeometry: borderRadiusGeometry, + borderSide: borderSide, + shape: shape, + ); + } + Widget _buildBottomRow(BuildContext context) { final defaultWidget = BottomRow( onThreadTap: onThreadTap, @@ -469,3 +419,17 @@ class MessageWidgetContent extends StatelessWidget { return defaultWidget; } } + +extension on Iterable { + Iterable addConditionally( + T? item, { + required bool condition(T element), + bool reverse = false, + }) sync* { + for (final element in this) { + if (item != null && !reverse && condition(element)) yield item; + yield element; + if (item != null && reverse && condition(element)) yield item; + } + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content_components.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content_components.dart index ccbd8a0e8a..a3a6782630 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content_components.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content_components.dart @@ -3,7 +3,4 @@ export 'message_card.dart'; export 'parse_attachments.dart'; export 'pinned_message.dart'; export 'quoted_message.dart'; -export 'reactions/message_reactions_modal.dart'; -export 'reactions/reaction_bubble.dart'; -export 'reactions/reaction_indicator.dart'; export 'user_avatar_transform.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart deleted file mode 100644 index a3eed77874..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/message_reactions_modal.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_align.dart'; -import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_card.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template streamMessageReactionsModal} -/// Modal widget for displaying message reactions -/// {@endtemplate} -class StreamMessageReactionsModal extends StatelessWidget { - /// {@macro streamMessageReactionsModal} - const StreamMessageReactionsModal({ - super.key, - required this.message, - required this.messageWidget, - required this.messageTheme, - this.showReactionPicker = true, - this.reverse = false, - this.onUserAvatarTap, - }); - - /// Widget that shows the message - final Widget messageWidget; - - /// Message to display reactions of - final Message message; - - /// [StreamMessageThemeData] to apply to [message] - final StreamMessageThemeData messageTheme; - - /// {@macro reverse} - final bool reverse; - - /// Flag for showing reaction picker. - final bool showReactionPicker; - - /// {@macro onUserAvatarTap} - final void Function(User)? onUserAvatarTap; - - @override - Widget build(BuildContext context) { - final user = StreamChat.of(context).currentUser; - final channel = StreamChannel.of(context).channel; - final orientation = MediaQuery.of(context).orientation; - final canSendReaction = channel.canSendReaction; - final fontSize = messageTheme.messageTextStyle?.fontSize; - - final child = Center( - child: SingleChildScrollView( - child: SafeArea( - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (showReactionPicker && canSendReaction) - LayoutBuilder( - builder: (context, constraints) { - return Align( - alignment: Alignment( - calculateReactionsHorizontalAlignment( - user, - message, - constraints, - fontSize, - orientation, - ), - 0, - ), - child: StreamReactionPicker( - message: message, - ), - ); - }, - ), - const SizedBox(height: 10), - IgnorePointer( - child: messageWidget, - ), - if (message.latestReactions?.isNotEmpty == true) ...[ - const SizedBox(height: 8), - ReactionsCard( - currentUser: user!, - message: message, - messageTheme: messageTheme, - ), - ], - ], - ), - ), - ), - ), - ); - - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => Navigator.of(context).maybePop(), - child: Stack( - children: [ - Positioned.fill( - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 10, - sigmaY: 10, - ), - child: DecoratedBox( - decoration: BoxDecoration( - color: StreamChatTheme.of(context).colorTheme.overlay, - ), - ), - ), - ), - TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOutBack, - builder: (context, val, widget) => Transform.scale( - scale: val, - child: widget, - ), - child: child, - ), - ], - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_indicator.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_indicator.dart deleted file mode 100644 index 873d60f7a4..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_indicator.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template reactionIndicator} -/// Indicates the reaction a [StreamMessageWidget] has. -/// -/// Used in [MessageWidgetContent]. -/// {@endtemplate} -class ReactionIndicator extends StatelessWidget { - /// {@macro reactionIndicator} - const ReactionIndicator({ - super.key, - required this.ownId, - required this.message, - required this.onTap, - required this.reverse, - required this.messageTheme, - }); - - /// The id of the current user. - final String ownId; - - /// {@macro message} - final Message message; - - /// The callback to perform when the widget is tapped or clicked. - final VoidCallback onTap; - - /// {@macro reverse} - final bool reverse; - - /// {@macro messageTheme} - final StreamMessageThemeData messageTheme; - - @override - Widget build(BuildContext context) { - final reactionsMap = {}; - message.latestReactions?.forEach((element) { - if (!reactionsMap.containsKey(element.type) || - element.user!.id == ownId) { - reactionsMap[element.type] = element; - } - }); - final reactionsList = reactionsMap.values.toList() - ..sort((a, b) => a.user!.id == ownId ? 1 : -1); - - return Transform( - transform: Matrix4.translationValues(reverse ? 12 : -12, 0, 0), - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 22 * 6.0, - ), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: GestureDetector( - onTap: onTap, - child: StreamReactionBubble( - key: ValueKey('${message.id}.reactions'), - reverse: reverse, - flipTail: reverse, - backgroundColor: - messageTheme.reactionsBackgroundColor ?? Colors.transparent, - borderColor: - messageTheme.reactionsBorderColor ?? Colors.transparent, - maskColor: messageTheme.reactionsMaskColor ?? Colors.transparent, - reactions: reactionsList, - ), - ), - ), - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_picker.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_picker.dart deleted file mode 100644 index 5f61d1dfcc..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_picker.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'package:ezanimation/ezanimation.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template streamReactionPicker} -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker_paint.png) -/// -/// Allows the user to select reactions to a message on mobile. -/// -/// It is not recommended to use this widget directly as it's one of the -/// default widgets used by [StreamMessageWidget.onMessageActions]. -/// {@endtemplate} -class StreamReactionPicker extends StatefulWidget { - /// {@macro streamReactionPicker} - const StreamReactionPicker({ - super.key, - required this.message, - }); - - /// Message to attach the reaction to - final Message message; - - @override - _StreamReactionPickerState createState() => _StreamReactionPickerState(); -} - -class _StreamReactionPickerState extends State - with TickerProviderStateMixin { - List animations = []; - - @override - Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); - final reactionIcons = StreamChatConfiguration.of(context).reactionIcons; - - if (animations.isEmpty && reactionIcons.isNotEmpty) { - reactionIcons.forEach((element) { - animations.add( - EzAnimation.tween( - Tween(begin: 0.0, end: 1.0), - const Duration(milliseconds: 500), - curve: Curves.easeInOutBack, - ), - ); - }); - - triggerAnimations(); - } - - final child = Material( - borderRadius: BorderRadius.circular(24), - color: chatThemeData.colorTheme.barsBg, - clipBehavior: Clip.hardEdge, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - child: Row( - spacing: 16, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: reactionIcons.map((reactionIcon) { - final ownReactionIndex = widget.message.ownReactions?.indexWhere( - (reaction) => reaction.type == reactionIcon.type, - ) ?? - -1; - final index = reactionIcons.indexOf(reactionIcon); - - final child = reactionIcon.builder( - context, - ownReactionIndex != -1, - 24, - ); - - return ConstrainedBox( - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - child: RawMaterialButton( - elevation: 0, - shape: ContinuousRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction( - context, - widget.message.ownReactions![ownReactionIndex], - ); - } else { - sendReaction( - context, - reactionIcon.type, - ); - } - }, - child: AnimatedBuilder( - animation: animations[index], - builder: (context, child) => Transform.scale( - scale: animations[index].value, - child: child, - ), - child: child, - ), - ), - ); - }).toList(), - ), - ), - ); - - return TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - curve: Curves.easeInOutBack, - duration: const Duration(milliseconds: 500), - builder: (context, val, widget) => Transform.scale( - scale: val, - child: widget, - ), - child: child, - ); - } - - Future triggerAnimations() async { - for (final a in animations) { - a.start(); - await Future.delayed(const Duration(milliseconds: 100)); - } - } - - Future pop() async { - for (final a in animations) { - a.stop(); - } - Navigator.of(context).pop(); - } - - /// Add a reaction to the message - void sendReaction(BuildContext context, String reactionType) { - StreamChannel.of(context).channel.sendReaction( - widget.message, - reactionType, - enforceUnique: - StreamChatConfiguration.of(context).enforceUniqueReactions, - ); - pop(); - } - - /// Remove a reaction from the message - void removeReaction(BuildContext context, Reaction reaction) { - StreamChannel.of(context).channel.deleteReaction(widget.message, reaction); - pop(); - } - - @override - void dispose() { - for (final a in animations) { - a.dispose(); - } - super.dispose(); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_align.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_align.dart deleted file mode 100644 index a7baad7eec..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_align.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// This method calculates the align that the modal of reactions should have. -/// This is an approximation based on the size of the message and the -/// available space in the screen. -double calculateReactionsHorizontalAlignment( - User? user, - Message message, - BoxConstraints constraints, - double? fontSize, - Orientation orientation, -) { - final maxWidth = constraints.maxWidth; - - final roughSentenceSize = message.roughMessageSize(fontSize); - final hasAttachments = message.attachments.isNotEmpty; - final isReply = message.quotedMessageId != null; - final isAttachment = hasAttachments && !isReply; - - // divFactor is the percentage of the available space that the message takes. - // When the divFactor is bigger than 0.5 that means that the messages is - // bigger than 50% of the available space and the modal should have an offset - // in the direction that the message grows. When the divFactor is smaller - // than 0.5 then the offset should be to he side opposite of the message - // growth. - // In resume, when divFactor > 0.5 then result > 0, when divFactor < 0.5 - // then result < 0. - var divFactor = 0.5; - - // When in portrait, attachments normally take 75% of the screen, when in - // landscape, attachments normally take 50% of the screen. - if (isAttachment) { - if (orientation == Orientation.portrait) { - divFactor = 0.75; - } else { - divFactor = 0.5; - } - } else { - divFactor = roughSentenceSize == 0 ? 0.5 : (roughSentenceSize / maxWidth); - } - - final signal = user?.id == message.user?.id ? 1 : -1; - final result = signal * (1 - divFactor * 2.0); - - // Ensure reactions don't get pushed past the edge of the screen. - // - // This happens if divFactor is really big. When this happens, we can simply - // move the model all the way to the end of screen. - return result.clamp(-1, 1); -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_card.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_card.dart deleted file mode 100644 index 727b44affc..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reactions_card.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/avatars/user_avatar.dart'; -import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart'; -import 'package:stream_chat_flutter/src/theme/message_theme.dart'; -import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/utils/extensions.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; - -/// {@template reactionsCard} -/// A card that displays the reactions to a message. -/// -/// Used in [StreamMessageReactionsModal] and [DesktopReactionsBuilder]. -/// {@endtemplate} -class ReactionsCard extends StatelessWidget { - /// {@macro reactionsCard} - const ReactionsCard({ - super.key, - required this.currentUser, - required this.message, - required this.messageTheme, - this.onUserAvatarTap, - }); - - /// Current logged in user. - final User currentUser; - - /// Message to display reactions of. - final Message message; - - /// [StreamMessageThemeData] to apply to [message]. - final StreamMessageThemeData messageTheme; - - /// {@macro onUserAvatarTap} - final void Function(User)? onUserAvatarTap; - - @override - Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); - return Card( - color: chatThemeData.colorTheme.barsBg, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.translations.messageReactionsLabel, - style: chatThemeData.textTheme.headlineBold, - ), - const SizedBox(height: 16), - Flexible( - child: SingleChildScrollView( - child: Wrap( - spacing: 16, - runSpacing: 16, - children: message.latestReactions! - .map((e) => _buildReaction( - e, - currentUser, - context, - )) - .toList(), - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildReaction( - Reaction reaction, - User currentUser, - BuildContext context, - ) { - final isCurrentUser = reaction.user?.id == currentUser.id; - final chatThemeData = StreamChatTheme.of(context); - final reverse = !isCurrentUser; - return ConstrainedBox( - constraints: BoxConstraints.loose( - const Size(64, 100), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - clipBehavior: Clip.none, - children: [ - StreamUserAvatar( - onTap: onUserAvatarTap, - user: reaction.user!, - constraints: const BoxConstraints.tightFor( - height: 64, - width: 64, - ), - onlineIndicatorConstraints: const BoxConstraints.tightFor( - height: 12, - width: 12, - ), - borderRadius: BorderRadius.circular(32), - ), - Positioned( - bottom: 6, - left: !reverse ? -3 : null, - right: reverse ? -3 : null, - child: Align( - alignment: - reverse ? Alignment.centerRight : Alignment.centerLeft, - child: StreamReactionBubble( - reactions: [reaction], - reverse: !reverse, - flipTail: !reverse, - borderColor: - messageTheme.reactionsBorderColor ?? Colors.transparent, - backgroundColor: messageTheme.reactionsBackgroundColor ?? - Colors.transparent, - maskColor: chatThemeData.colorTheme.barsBg, - tailCirclesSpacing: 1, - ), - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - reaction.user!.name.split(' ')[0], - style: chatThemeData.textTheme.footnoteBold, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ], - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/misc/adaptive_dialog_action.dart b/packages/stream_chat_flutter/lib/src/misc/adaptive_dialog_action.dart new file mode 100644 index 0000000000..159c55b557 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/misc/adaptive_dialog_action.dart @@ -0,0 +1,71 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; + +/// A platform-adaptive dialog action button that renders appropriately based on +/// the platform. +/// +/// This widget uses [CupertinoDialogAction] on iOS and macOS platforms, +/// and [TextButton] on all other platforms, maintaining the appropriate +/// platform design language. +/// +/// The styling is influenced by the [StreamChatTheme] to ensure consistent +/// appearance with other Stream Chat components. +class AdaptiveDialogAction extends StatelessWidget { + /// Creates an adaptive dialog action. + const AdaptiveDialogAction({ + super.key, + this.onPressed, + this.isDefaultAction = false, + this.isDestructiveAction = false, + required this.child, + }); + + /// The callback that is called when the action is tapped. + final VoidCallback? onPressed; + + /// Whether this action is the default choice in the dialog. + /// + /// Default actions use emphasized styling (bold text) on iOS/macOS. + /// This has no effect on other platforms. + final bool isDefaultAction; + + /// Whether this action performs a destructive action like deletion. + /// + /// Destructive actions are displayed with red text on iOS/macOS. + /// This has no effect on other platforms. + final bool isDestructiveAction; + + /// The widget to display as the content of the action. + /// + /// Typically a [Text] widget. + final Widget child; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + + return switch (Theme.of(context).platform) { + TargetPlatform.iOS || TargetPlatform.macOS => CupertinoTheme( + data: CupertinoTheme.of(context).copyWith( + primaryColor: theme.colorTheme.accentPrimary, + ), + child: CupertinoDialogAction( + onPressed: onPressed, + isDefaultAction: isDefaultAction, + isDestructiveAction: isDestructiveAction, + child: child, + ), + ), + _ => TextButton( + onPressed: onPressed, + style: TextButton.styleFrom( + textStyle: theme.textTheme.body, + foregroundColor: theme.colorTheme.accentPrimary, + disabledForegroundColor: theme.colorTheme.disabled, + ), + child: child, + ), + }; + } +} diff --git a/packages/stream_chat_flutter/lib/src/misc/back_button.dart b/packages/stream_chat_flutter/lib/src/misc/back_button.dart index a589a5921d..d2c11d003c 100644 --- a/packages/stream_chat_flutter/lib/src/misc/back_button.dart +++ b/packages/stream_chat_flutter/lib/src/misc/back_button.dart @@ -42,7 +42,7 @@ class StreamBackButton extends StatelessWidget { start: 12, child: switch (channelId) { final cid? => StreamUnreadIndicator.channels(cid: cid), - _ => StreamUnreadIndicator(), + _ => const StreamUnreadIndicator(), }, ), ], diff --git a/packages/stream_chat_flutter/lib/src/misc/flexible_fractionally_sized_box.dart b/packages/stream_chat_flutter/lib/src/misc/flexible_fractionally_sized_box.dart new file mode 100644 index 0000000000..86da240000 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/misc/flexible_fractionally_sized_box.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +/// A widget that sizes its child to a fraction of the total available space. +class FlexibleFractionallySizedBox extends StatelessWidget { + /// Creates a widget that sizes its child to a fraction of the total available + /// space. + /// + /// If non-null, the [widthFactor] and [heightFactor] arguments must be + /// non-negative. + const FlexibleFractionallySizedBox({ + super.key, + this.alignment = Alignment.center, + this.widthFactor, + this.heightFactor, + this.child, + }) : assert(widthFactor == null || widthFactor >= 0.0, ''), + assert(heightFactor == null || heightFactor >= 0.0, ''); + + /// The widget below this widget in the tree. + /// + /// {@macro flutter.widgets.ProxyWidget.child} + final Widget? child; + + /// If non-null, the fraction of the incoming width given to the child. + /// + /// If non-null, the child is given a tight width constraint that is the max + /// incoming width constraint multiplied by this factor. + /// + /// If null, the incoming width constraints are passed to the child + /// unmodified. + final double? widthFactor; + + /// If non-null, the fraction of the incoming height given to the child. + /// + /// If non-null, the child is given a tight height constraint that is the max + /// incoming height constraint multiplied by this factor. + /// + /// If null, the incoming height constraints are passed to the child + /// unmodified. + final double? heightFactor; + + /// How to align the child. + /// + /// The x and y values of the alignment control the horizontal and vertical + /// alignment, respectively. An x value of -1.0 means that the left edge of + /// the child is aligned with the left edge of the parent whereas an x value + /// of 1.0 means that the right edge of the child is aligned with the right + /// edge of the parent. Other values interpolate (and extrapolate) linearly. + /// For example, a value of 0.0 means that the center of the child is aligned + /// with the center of the parent. + /// + /// Defaults to [Alignment.center]. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final AlignmentGeometry alignment; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + var maxWidth = constraints.maxWidth; + if (widthFactor case final widthFactor?) { + final width = maxWidth * widthFactor; + maxWidth = width; + } + + var maxHeight = constraints.maxHeight; + if (heightFactor case final heightFactor?) { + final height = maxHeight * heightFactor; + maxHeight = height; + } + + return UnconstrainedBox( + alignment: alignment, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: maxHeight, + ), + child: child, + ), + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart b/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart index 60ac8fe9c2..fb5ccde92e 100644 --- a/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart +++ b/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart @@ -1,11 +1,15 @@ +// ignore_for_file: avoid_positional_boolean_parameters + import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// {@template reactionIconBuilder} /// Signature for a function that builds a reaction icon. /// {@endtemplate} typedef ReactionIconBuilder = Widget Function( BuildContext context, - // ignore: avoid_positional_boolean_parameters bool isHighlighted, double iconSize, ); @@ -17,12 +21,154 @@ class StreamReactionIcon { /// {@macro streamReactionIcon} const StreamReactionIcon({ required this.type, + this.emojiCode, required this.builder, }); + /// Creates a reaction icon with a default unknown icon. + const StreamReactionIcon.unknown() + : type = 'unknown', + emojiCode = null, + builder = _unknownBuilder; + + /// Converts this [StreamReactionIcon] to a [Reaction] object. + Reaction toReaction() => Reaction(type: type, emojiCode: emojiCode); + /// Type of reaction final String type; + /// Optional emoji code for the reaction. + /// + /// Used to display a custom emoji in the notification. + final String? emojiCode; + /// {@macro reactionIconBuilder} final ReactionIconBuilder builder; + + /// The default list of reaction icons provided by Stream Chat. + /// + /// This includes five reactions: + /// - love: Represented by a heart icon + /// - like: Represented by a thumbs up icon + /// - sad: Represented by a thumbs down icon + /// - haha: Represented by a laughing face icon + /// - wow: Represented by a surprised face icon + /// + /// These default reactions can be used directly or as a starting point for + /// custom reaction configurations. + static const List defaultReactions = [ + StreamReactionIcon(type: 'love', emojiCode: 'โค๏ธ', builder: _loveBuilder), + StreamReactionIcon(type: 'like', emojiCode: '๐Ÿ‘', builder: _likeBuilder), + StreamReactionIcon(type: 'sad', emojiCode: '๐Ÿ‘Ž', builder: _sadBuilder), + StreamReactionIcon(type: 'haha', emojiCode: '๐Ÿ˜‚', builder: _hahaBuilder), + StreamReactionIcon(type: 'wow', emojiCode: '๐Ÿ˜ฎ', builder: _wowBuilder), + ]; + + static Widget _loveBuilder( + BuildContext context, + bool highlighted, + double size, + ) { + final theme = StreamChatTheme.of(context); + final iconColor = switch (highlighted) { + true => theme.colorTheme.accentPrimary, + false => theme.primaryIconTheme.color, + }; + + return StreamSvgIcon( + icon: StreamSvgIcons.loveReaction, + color: iconColor, + size: size, + ); + } + + static Widget _likeBuilder( + BuildContext context, + bool highlighted, + double size, + ) { + final theme = StreamChatTheme.of(context); + final iconColor = switch (highlighted) { + true => theme.colorTheme.accentPrimary, + false => theme.primaryIconTheme.color, + }; + + return StreamSvgIcon( + icon: StreamSvgIcons.thumbsUpReaction, + color: iconColor, + size: size, + ); + } + + static Widget _sadBuilder( + BuildContext context, + bool highlighted, + double size, + ) { + final theme = StreamChatTheme.of(context); + final iconColor = switch (highlighted) { + true => theme.colorTheme.accentPrimary, + false => theme.primaryIconTheme.color, + }; + + return StreamSvgIcon( + icon: StreamSvgIcons.thumbsDownReaction, + color: iconColor, + size: size, + ); + } + + static Widget _hahaBuilder( + BuildContext context, + bool highlighted, + double size, + ) { + final theme = StreamChatTheme.of(context); + final iconColor = switch (highlighted) { + true => theme.colorTheme.accentPrimary, + false => theme.primaryIconTheme.color, + }; + + return StreamSvgIcon( + icon: StreamSvgIcons.lolReaction, + color: iconColor, + size: size, + ); + } + + static Widget _wowBuilder( + BuildContext context, + bool highlighted, + double size, + ) { + final theme = StreamChatTheme.of(context); + final iconColor = switch (highlighted) { + true => theme.colorTheme.accentPrimary, + false => theme.primaryIconTheme.color, + }; + + return StreamSvgIcon( + icon: StreamSvgIcons.wutReaction, + color: iconColor, + size: size, + ); + } + + static Widget _unknownBuilder( + BuildContext context, + bool highlighted, + double size, + ) { + final theme = StreamChatTheme.of(context); + final iconColor = switch (highlighted) { + true => theme.colorTheme.accentPrimary, + false => theme.primaryIconTheme.color, + }; + + return Icon( + Icons.help_outline_rounded, + color: iconColor, + size: size, + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/misc/simple_safe_area.dart b/packages/stream_chat_flutter/lib/src/misc/simple_safe_area.dart index a691b568ee..7aeeb8615c 100644 --- a/packages/stream_chat_flutter/lib/src/misc/simple_safe_area.dart +++ b/packages/stream_chat_flutter/lib/src/misc/simple_safe_area.dart @@ -1,27 +1,90 @@ import 'package:flutter/material.dart'; -/// A [SafeArea] with an enabled toggle +/// A simple wrapper around Flutter's [SafeArea] widget. +/// +/// [SimpleSafeArea] provides a convenient way to avoid system intrusions +/// (such as notches, status, and navigation bars) on all or specific sides. +/// +/// By default, all sides are enabled. Use [SimpleSafeArea.only] to specify +/// specific sides to avoid. +/// +/// See also: +/// - [SafeArea], which this widget wraps. +/// class SimpleSafeArea extends StatelessWidget { - /// Constructor for [SimpleSafeArea] + /// Creates a [SimpleSafeArea] that avoids system intrusions either on all + /// sides or none. const SimpleSafeArea({ super.key, - this.enabled = true, + bool? enabled, + this.minimum = EdgeInsets.zero, + this.maintainBottomViewPadding = false, + required this.child, + }) : left = enabled ?? true, + top = enabled ?? true, + right = enabled ?? true, + bottom = enabled ?? true; + + /// Creates a [SimpleSafeArea] that avoids system intrusions only on the + /// specified sides. + const SimpleSafeArea.only({ + super.key, + this.left = false, + this.top = false, + this.right = false, + this.bottom = false, + this.minimum = EdgeInsets.zero, + this.maintainBottomViewPadding = false, required this.child, }); - /// Wrap [child] with [SafeArea] - final bool? enabled; + /// Whether to avoid system intrusions on the left. + final bool left; + + /// Whether to avoid system intrusions at the top of the screen, typically the + /// system status bar. + final bool top; + + /// Whether to avoid system intrusions on the right. + final bool right; + + /// Whether to avoid system intrusions on the bottom side of the screen. + final bool bottom; + + /// This minimum padding to apply. + /// + /// The greater of the minimum insets and the media padding will be applied. + final EdgeInsets minimum; + + /// Specifies whether the [SafeArea] should maintain the bottom + /// [MediaQueryData.viewPadding] instead of the bottom + /// [MediaQueryData.padding], defaults to false. + /// + /// For example, if there is an onscreen keyboard displayed above the + /// SafeArea, the padding can be maintained below the obstruction rather than + /// being consumed. This can be helpful in cases where your layout contains + /// flexible widgets, which could visibly move when opening a software + /// keyboard due to the change in the padding value. Setting this to true will + /// avoid the UI shift. + final bool maintainBottomViewPadding; - /// Child widget to wrap + /// The widget below this widget in the tree. + /// + /// The padding on the [MediaQuery] for the [child] will be suitably adjusted + /// to zero out any sides that were avoided by this widget. + /// + /// {@macro flutter.widgets.ProxyWidget.child} final Widget child; @override Widget build(BuildContext context) { return SafeArea( - left: enabled ?? true, - top: enabled ?? true, - right: enabled ?? true, - bottom: enabled ?? true, + left: left, + top: top, + right: right, + bottom: bottom, + minimum: minimum, + maintainBottomViewPadding: maintainBottomViewPadding, child: child, ); } diff --git a/packages/stream_chat_flutter/lib/src/misc/size_change_listener.dart b/packages/stream_chat_flutter/lib/src/misc/size_change_listener.dart new file mode 100644 index 0000000000..1e6a2adfcd --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/misc/size_change_listener.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +/// A widget that calls the callback when the layout dimensions of +/// its child change. +class SizeChangeListener extends SingleChildRenderObjectWidget { + /// Creates a new instance of [SizeChangeListener]. + const SizeChangeListener({ + super.key, + required this.onSizeChanged, + super.child, + }); + + /// The action to perform when the size of child widget changes. + final ValueChanged onSizeChanged; + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderSizeChangedWithCallback(onSizeChanged: onSizeChanged); + } +} + +class _RenderSizeChangedWithCallback extends RenderProxyBox { + _RenderSizeChangedWithCallback({ + required this.onSizeChanged, + }); + + final ValueChanged onSizeChanged; + Size? _oldSize; + + @override + void performLayout() { + super.performLayout(); + if (size != _oldSize) { + _oldSize = size; + WidgetsBinding.instance.addPostFrameCallback((_) { + // Call the callback with the new size + onSizeChanged.call(size); + }); + } + } +} diff --git a/packages/stream_chat_flutter/lib/src/misc/stream_modal.dart b/packages/stream_chat_flutter/lib/src/misc/stream_modal.dart new file mode 100644 index 0000000000..19269ebd9c --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/misc/stream_modal.dart @@ -0,0 +1,61 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; + +/// Shows a modal dialog with customized transitions and backdrop effects. +/// +/// This function is a wrapper around [showGeneralDialog] that provides +/// a consistent look and feel for modals in Stream Chat. +/// +/// Returns a [Future] that resolves to the value passed to [Navigator.pop] +/// when the dialog is closed. +Future showStreamDialog({ + required BuildContext context, + required WidgetBuilder builder, + bool barrierDismissible = true, + String? barrierLabel, + Color? barrierColor, + Duration transitionDuration = const Duration(milliseconds: 335), + RouteTransitionsBuilder? transitionBuilder, + bool useRootNavigator = true, + RouteSettings? routeSettings, + Offset? anchorPoint, +}) { + assert(debugCheckHasMaterialLocalizations(context), ''); + final localizations = MaterialLocalizations.of(context); + + final theme = StreamChatTheme.of(context); + final colorTheme = theme.colorTheme; + + final capturedThemes = InheritedTheme.capture( + from: context, + to: Navigator.of(context, rootNavigator: useRootNavigator).context, + ); + + return showGeneralDialog( + context: context, + useRootNavigator: useRootNavigator, + anchorPoint: anchorPoint, + routeSettings: routeSettings, + transitionDuration: transitionDuration, + barrierDismissible: barrierDismissible, + barrierColor: barrierColor ?? colorTheme.overlay, + barrierLabel: barrierLabel ?? localizations.modalBarrierDismissLabel, + transitionBuilder: (context, animation, secondaryAnimation, child) { + final sigma = 10 * animation.value; + final scaleAnimation = Tween(begin: 0, end: 1).animate( + CurvedAnimation(parent: animation, curve: Curves.easeOutBack), + ); + + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: sigma, sigmaY: sigma), + child: ScaleTransition(scale: scaleAnimation, child: child), + ); + }, + pageBuilder: (context, animation, secondaryAnimation) { + final pageChild = Builder(builder: builder); + return capturedThemes.wrap(pageChild); + }, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart b/packages/stream_chat_flutter/lib/src/reactions/desktop_reactions_builder.dart similarity index 95% rename from packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart rename to packages/stream_chat_flutter/lib/src/reactions/desktop_reactions_builder.dart index 753341083b..f228833564 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/desktop_reactions_builder.dart +++ b/packages/stream_chat_flutter/lib/src/reactions/desktop_reactions_builder.dart @@ -4,7 +4,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; -import 'package:stream_chat_flutter/src/message_widget/reactions/reactions_card.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@template desktopReactionsBuilder} @@ -101,11 +100,7 @@ class _DesktopReactionsBuilderState extends State { maxWidth: 336, maxHeight: 342, ), - child: ReactionsCard( - currentUser: currentUser, - message: widget.message, - messageTheme: widget.messageTheme, - ), + child: StreamUserReactions(message: widget.message), ), ), child: MouseRegion( @@ -188,8 +183,7 @@ class _BottomReaction extends StatelessWidget { } else if (reactionIcon != null) { StreamChannel.of(context).channel.sendReaction( message, - reactionIcon!.type, - score: reaction.score + 1, + reactionIcon!.toReaction(), enforceUnique: StreamChatConfiguration.of(context).enforceUniqueReactions, ); diff --git a/packages/stream_chat_flutter/lib/src/reactions/indicator/reaction_indicator.dart b/packages/stream_chat_flutter/lib/src/reactions/indicator/reaction_indicator.dart new file mode 100644 index 0000000000..8f57179b2b --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/reactions/indicator/reaction_indicator.dart @@ -0,0 +1,107 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/reactions/indicator/reaction_indicator_icon_list.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template streamReactionIndicator} +/// A widget that displays a horizontal list of reaction icons that users have +/// reacted with on a message. +/// +/// This widget is typically used to show the reactions on a message in a +/// compact way, allowing users to see which reactions have been added +/// to a message without opening a full user reactions view. +/// {@endtemplate} +class StreamReactionIndicator extends StatelessWidget { + /// {@macro streamReactionIndicator} + const StreamReactionIndicator({ + super.key, + this.onTap, + required this.message, + this.backgroundColor, + this.padding = const EdgeInsets.all(8), + this.scrollable = true, + this.borderRadius = const BorderRadius.all(Radius.circular(26)), + this.reactionSorting = ReactionSorting.byFirstReactionAt, + }); + + /// Message to attach the reaction to. + final Message message; + + /// Callback triggered when the reaction indicator is tapped. + final VoidCallback? onTap; + + /// Background color for the reaction indicator. + final Color? backgroundColor; + + /// Padding around the reaction picker. + /// + /// Defaults to `EdgeInsets.all(8)`. + final EdgeInsets padding; + + /// Whether the reaction picker should be scrollable. + /// + /// Defaults to `true`. + final bool scrollable; + + /// Border radius for the reaction picker. + /// + /// Defaults to a circular border with a radius of 26. + final BorderRadius? borderRadius; + + /// Sorting strategy for the reaction. + /// + /// Defaults to sorting by the first reaction at. + final Comparator reactionSorting; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final config = StreamChatConfiguration.of(context); + final reactionIcons = config.reactionIcons; + + final ownReactions = {...?message.ownReactions?.map((it) => it.type)}; + final indicatorIcons = message.reactionGroups?.entries + .sortedByCompare((it) => it.value, reactionSorting) + .map((group) { + final reactionIcon = reactionIcons.firstWhere( + (it) => it.type == group.key, + orElse: () => const StreamReactionIcon.unknown(), + ); + + return ReactionIndicatorIcon( + type: reactionIcon.type, + builder: reactionIcon.builder, + isSelected: ownReactions.contains(reactionIcon.type), + ); + }); + + final isSingleIndicatorIcon = indicatorIcons?.length == 1; + final extraPadding = switch (isSingleIndicatorIcon) { + true => EdgeInsets.zero, + false => const EdgeInsets.symmetric(horizontal: 4), + }; + + final indicator = ReactionIndicatorIconList( + indicatorIcons: [...?indicatorIcons], + ); + + return Material( + borderRadius: borderRadius, + clipBehavior: Clip.antiAlias, + color: backgroundColor ?? theme.colorTheme.barsBg, + child: InkWell( + onTap: onTap, + child: Padding( + padding: padding.add(extraPadding), + child: switch (scrollable) { + true => SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: indicator, + ), + false => indicator, + }, + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/reactions/indicator/reaction_indicator_bubble_overlay.dart b/packages/stream_chat_flutter/lib/src/reactions/indicator/reaction_indicator_bubble_overlay.dart new file mode 100644 index 0000000000..cb4ce0d465 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/reactions/indicator/reaction_indicator_bubble_overlay.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/reactions/indicator/reaction_indicator.dart'; +import 'package:stream_chat_flutter/src/reactions/reaction_bubble_overlay.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// {@template reactionIndicatorBubbleOverlay} +/// A widget that displays a reaction indicator bubble overlay attached to a +/// [child] widget. Typically used to show the reactions for a [Message]. +/// +/// It positions the reaction indicator relative to the provided [child] widget, +/// using the given [anchorOffset] and [childSizeDelta] for fine-tuned placement +/// {@endtemplate} +class ReactionIndicatorBubbleOverlay extends StatelessWidget { + /// {@macro reactionIndicatorBubbleOverlay} + const ReactionIndicatorBubbleOverlay({ + super.key, + this.onTap, + required this.message, + required this.child, + this.visible = true, + this.reverse = false, + this.anchorOffset = Offset.zero, + this.childSizeDelta = Offset.zero, + }); + + /// Whether the overlay should be visible. + final bool visible; + + /// Whether to reverse the alignment of the overlay. + final bool reverse; + + /// The widget to which the overlay is anchored. + final Widget child; + + /// The message to display reactions for. + final Message message; + + /// Callback triggered when the reaction indicator is tapped. + final VoidCallback? onTap; + + /// The offset to apply to the anchor position. + final Offset anchorOffset; + + /// The additional size delta to apply to the child widget for positioning. + final Offset childSizeDelta; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final messageTheme = theme.getMessageTheme(reverse: reverse); + + return ReactionBubbleOverlay( + visible: visible, + childSizeDelta: childSizeDelta, + config: ReactionBubbleConfig( + fillColor: messageTheme.reactionsBackgroundColor, + borderColor: messageTheme.reactionsBorderColor, + maskColor: messageTheme.reactionsMaskColor, + ), + anchor: ReactionBubbleAnchor( + offset: anchorOffset, + follower: AlignmentDirectional.bottomCenter, + target: AlignmentDirectional(reverse ? -1 : 1, -1), + ), + reaction: StreamReactionIndicator( + onTap: onTap, + message: message, + backgroundColor: messageTheme.reactionsBackgroundColor, + ), + child: child, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/reactions/indicator/reaction_indicator_icon_list.dart b/packages/stream_chat_flutter/lib/src/reactions/indicator/reaction_indicator_icon_list.dart new file mode 100644 index 0000000000..c01222fc7a --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/reactions/indicator/reaction_indicator_icon_list.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/misc/reaction_icon.dart'; + +/// {@template reactionIndicatorIconBuilder} +/// Function signature for building a custom reaction icon widget. +/// +/// This is used to customize how each reaction icon is displayed in the +/// [ReactionIndicatorIconList]. +/// +/// Parameters: +/// - [context]: The build context. +/// - [icon]: The reaction icon data containing type and selection state. +/// {@endtemplate} +typedef ReactionIndicatorIconBuilder = Widget Function( + BuildContext context, + ReactionIndicatorIcon icon, +); + +/// {@template reactionIndicatorIconList} +/// A widget that displays a list of reactionIcons that users have reacted with +/// on a message. +/// +/// also see: +/// - [StreamReactionIndicator], which is a higher-level widget that uses this +/// widget to display a reaction indicator in a modal or inline. +/// {@endtemplate} +class ReactionIndicatorIconList extends StatelessWidget { + /// {@macro reactionIndicatorIconList} + const ReactionIndicatorIconList({ + super.key, + required this.indicatorIcons, + this.iconBuilder = _defaultIconBuilder, + }); + + /// The list of available reaction indicator icons. + final List indicatorIcons; + + /// The builder used to create the reaction indicator icons. + final ReactionIndicatorIconBuilder iconBuilder; + + static Widget _defaultIconBuilder( + BuildContext context, + ReactionIndicatorIcon icon, + ) { + return icon.build(context); + } + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 4, + runAlignment: WrapAlignment.center, + alignment: WrapAlignment.spaceAround, + crossAxisAlignment: WrapCrossAlignment.center, + children: [...indicatorIcons.map((icon) => iconBuilder(context, icon))], + ); + } +} + +/// {@template reactionIndicatorIcon} +/// A data class that represents a reaction icon within the reaction indicator. +/// +/// This class holds information about a specific reaction, such as its type, +/// whether it's currently selected by the user, and a builder function +/// to construct its visual representation. +/// {@endtemplate} +class ReactionIndicatorIcon { + /// {@macro reactionIndicatorIcon} + const ReactionIndicatorIcon({ + required this.type, + this.isSelected = false, + this.iconSize = 16, + required ReactionIconBuilder builder, + }) : _builder = builder; + + /// The unique identifier for the reaction type (e.g., "like", "love"). + final String type; + + /// A boolean indicating whether this reaction is currently selected by the + /// user. + final bool isSelected; + + /// The size of the reaction icon. + final double iconSize; + + /// Builds the actual widget for this reaction icon using the provided + /// [context], selection state, and icon size. + Widget build(BuildContext context) => _builder(context, isSelected, iconSize); + final ReactionIconBuilder _builder; +} diff --git a/packages/stream_chat_flutter/lib/src/reactions/picker/reaction_picker.dart b/packages/stream_chat_flutter/lib/src/reactions/picker/reaction_picker.dart new file mode 100644 index 0000000000..06456e85cf --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/reactions/picker/reaction_picker.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// {@template reactionPickerBuilder} +/// Function signature for building a custom reaction picker widget. +/// +/// Use this to provide a custom reaction picker in [StreamMessageActionsModal] +/// or [StreamMessageReactionsModal]. +/// +/// Parameters: +/// - [context]: The build context. +/// - [message]: The message to show reactions for. +/// - [onReactionPicked]: Callback when a reaction is picked. +/// {@endtemplate} +typedef ReactionPickerBuilder = Widget Function( + BuildContext context, + Message message, + OnReactionPicked? onReactionPicked, +); + +/// {@template streamReactionPicker} +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker_paint.png) +/// +/// A widget that displays a horizontal list of reaction icons that users can +/// select to react to a message. +/// +/// The reaction picker can be configured with custom reaction icons, padding, +/// border radius, and can be made scrollable or static depending on the +/// specific needs. +/// {@endtemplate} +class StreamReactionPicker extends StatelessWidget { + /// {@macro streamReactionPicker} + const StreamReactionPicker({ + super.key, + required this.message, + required this.reactionIcons, + this.onReactionPicked, + this.padding = const EdgeInsets.all(4), + this.scrollable = true, + this.borderRadius = const BorderRadius.all(Radius.circular(24)), + }); + + /// Creates a [StreamReactionPicker] using the default reaction icons + /// provided by the [StreamChatConfiguration]. + /// + /// This is the recommended way to create a reaction picker + /// as it ensures that the icons are consistent with the rest of the app. + /// + /// The [onReactionPicked] callback is optional and can be used to handle + /// the reaction selection. + factory StreamReactionPicker.builder( + BuildContext context, + Message message, + OnReactionPicked? onReactionPicked, + ) { + final config = StreamChatConfiguration.of(context); + final reactionIcons = config.reactionIcons; + + final platform = Theme.of(context).platform; + return switch (platform) { + TargetPlatform.iOS || TargetPlatform.android => StreamReactionPicker( + message: message, + reactionIcons: reactionIcons, + onReactionPicked: onReactionPicked, + ), + _ => StreamReactionPicker( + message: message, + scrollable: false, + borderRadius: BorderRadius.zero, + reactionIcons: reactionIcons, + onReactionPicked: onReactionPicked, + ), + }; + } + + /// Message to attach the reaction to. + final Message message; + + /// List of reaction icons to display. + final List reactionIcons; + + /// {@macro onReactionPressed} + final OnReactionPicked? onReactionPicked; + + /// Padding around the reaction picker. + /// + /// Defaults to `EdgeInsets.all(4)`. + final EdgeInsets padding; + + /// Whether the reaction picker should be scrollable. + /// + /// Defaults to `true`. + final bool scrollable; + + /// Border radius for the reaction picker. + /// + /// Defaults to a circular border with a radius of 24. + final BorderRadius? borderRadius; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + + final reactionPicker = ReactionPickerIconList( + message: message, + reactionIcons: reactionIcons, + onReactionPicked: onReactionPicked, + ); + + final isSinglePickerIcon = reactionIcons.length == 1; + final extraPadding = switch (isSinglePickerIcon) { + true => EdgeInsets.zero, + false => const EdgeInsets.symmetric(horizontal: 4), + }; + + return Material( + borderRadius: borderRadius, + clipBehavior: Clip.antiAlias, + color: theme.colorTheme.barsBg, + child: Padding( + padding: padding.add(extraPadding), + child: switch (scrollable) { + false => reactionPicker, + true => SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: reactionPicker, + ), + }, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/reactions/picker/reaction_picker_bubble_overlay.dart b/packages/stream_chat_flutter/lib/src/reactions/picker/reaction_picker_bubble_overlay.dart new file mode 100644 index 0000000000..ff4259aa96 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/reactions/picker/reaction_picker_bubble_overlay.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/reactions/picker/reaction_picker.dart'; +import 'package:stream_chat_flutter/src/reactions/picker/reaction_picker_icon_list.dart'; +import 'package:stream_chat_flutter/src/reactions/reaction_bubble_overlay.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// {@template reactionPickerBubbleOverlay} +/// A widget that displays a reaction picker bubble overlay attached to a +/// [child] widget. Typically used with the [MessageWidget] as the child. +/// +/// It positions the reaction picker relative to the provided [child] widget, +/// using the given [anchorOffset] and [childSizeDelta] for fine-tuned placement +/// {@endtemplate} +class ReactionPickerBubbleOverlay extends StatelessWidget { + /// {@macro reactionPickerBubbleOverlay} + const ReactionPickerBubbleOverlay({ + super.key, + required this.message, + required this.child, + this.onReactionPicked, + this.visible = true, + this.reverse = false, + this.anchorOffset = Offset.zero, + this.childSizeDelta = Offset.zero, + this.reactionPickerBuilder = StreamReactionPicker.builder, + }); + + /// Whether the overlay should be visible. + final bool visible; + + /// Whether to reverse the alignment of the overlay. + final bool reverse; + + /// The widget to which the overlay is anchored. + final Widget child; + + /// The message to attach the reaction to. + final Message message; + + /// Callback triggered when a reaction is picked. + final OnReactionPicked? onReactionPicked; + + /// Builder for the reaction picker widget. + final ReactionPickerBuilder reactionPickerBuilder; + + /// The offset to apply to the anchor position. + final Offset anchorOffset; + + /// The additional size delta to apply to the child widget for positioning. + final Offset childSizeDelta; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final colorTheme = theme.colorTheme; + + return ReactionBubbleOverlay( + visible: visible, + childSizeDelta: childSizeDelta, + config: ReactionBubbleConfig( + fillColor: colorTheme.barsBg, + maskColor: Colors.transparent, + borderColor: Colors.transparent, + ), + anchor: ReactionBubbleAnchor( + offset: anchorOffset, + follower: AlignmentDirectional.bottomCenter, + target: AlignmentDirectional(reverse ? -1 : 1, -1), + ), + reaction: reactionPickerBuilder.call(context, message, onReactionPicked), + child: child, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/reactions/picker/reaction_picker_icon_list.dart b/packages/stream_chat_flutter/lib/src/reactions/picker/reaction_picker_icon_list.dart new file mode 100644 index 0000000000..0fc775fad0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/reactions/picker/reaction_picker_icon_list.dart @@ -0,0 +1,279 @@ +import 'package:collection/collection.dart'; +import 'package:ezanimation/ezanimation.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/misc/reaction_icon.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// {@template onReactionPressed} +/// Callback called when a reaction icon is pressed. +/// {@endtemplate} +typedef OnReactionPicked = ValueSetter; + +/// {@template onReactionPickerIconPressed} +/// Callback called when a reaction picker icon is pressed. +/// {@endtemplate} +typedef OnReactionPickerIconPressed = ValueSetter; + +/// {@template reactionPickerIconBuilder} +/// Function signature for building a custom reaction icon widget. +/// +/// This is used to customize how each reaction icon is displayed in the +/// [ReactionPickerIconList]. +/// +/// Parameters: +/// - [context]: The build context. +/// - [icon]: The reaction icon data containing type and selection state. +/// - [onPressed]: Callback when the reaction icon is pressed. +/// {@endtemplate} +typedef ReactionPickerIconBuilder = Widget Function( + BuildContext context, + ReactionPickerIcon icon, + VoidCallback? onPressed, +); + +/// {@template reactionPickerIconList} +/// A widget that displays a list of reactionIcons that can be picked by a user. +/// +/// This widget shows a row of reaction icons with animated entry. When a user +/// taps on a reaction icon, the [onReactionPicked] callback is invoked with the +/// selected reaction. +/// +/// The reactions displayed are configured via [reactionIcons] and the widget +/// tracks which reactions the current user has already added to the [message]. +/// +/// also see: +/// - [StreamReactionPicker], which is a higher-level widget that uses this +/// widget to display a reaction picker in a modal or inline. +/// {@endtemplate} +class ReactionPickerIconList extends StatefulWidget { + /// {@macro reactionPickerIconList} + const ReactionPickerIconList({ + super.key, + required this.message, + required this.reactionIcons, + this.iconBuilder = _defaultIconBuilder, + this.onReactionPicked, + }); + + /// The message to display reactions for. + final Message message; + + /// The list of available reaction picker icons. + final List reactionIcons; + + /// The builder used to create the reaction picker icons. + final ReactionPickerIconBuilder iconBuilder; + + /// {@macro onReactionPressed} + final OnReactionPicked? onReactionPicked; + + static Widget _defaultIconBuilder( + BuildContext context, + ReactionPickerIcon icon, + VoidCallback? onPressed, + ) { + return ReactionIconButton( + icon: icon, + onPressed: onPressed, + ); + } + + @override + State createState() => _ReactionPickerIconListState(); +} + +class _ReactionPickerIconListState extends State { + List _iconAnimations = []; + + void _triggerAnimations() async { + for (final animation in _iconAnimations) { + if (mounted) animation.start(); + // Add a small delay between the start of each animation. + await Future.delayed(const Duration(milliseconds: 100)); + } + } + + void _dismissAnimations() { + for (final animation in _iconAnimations) { + animation.stop(); + } + } + + void _disposeAnimations() { + for (final animation in _iconAnimations) { + animation.dispose(); + } + } + + @override + void initState() { + super.initState(); + _iconAnimations = List.generate( + widget.reactionIcons.length, + (index) => EzAnimation.tween( + Tween(begin: 0.0, end: 1.0), + kThemeAnimationDuration, + curve: Curves.easeInOutBack, + ), + ); + + // Trigger animations at the end of the frame to avoid jank. + WidgetsBinding.instance.endOfFrame.then((_) => _triggerAnimations()); + } + + @override + void didUpdateWidget(covariant ReactionPickerIconList oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.reactionIcons.length != widget.reactionIcons.length) { + // Dismiss and dispose old animations. + _dismissAnimations(); + _disposeAnimations(); + + // Initialize new animations. + _iconAnimations = List.generate( + widget.reactionIcons.length, + (index) => EzAnimation.tween( + Tween(begin: 0.0, end: 1.0), + kThemeAnimationDuration, + curve: Curves.easeInOutBack, + ), + ); + + // Trigger animations at the end of the frame to avoid jank. + WidgetsBinding.instance.endOfFrame.then((_) => _triggerAnimations()); + } + } + + @override + void dispose() { + _dismissAnimations(); + _disposeAnimations(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final child = Wrap( + spacing: 4, + runSpacing: 4, + runAlignment: WrapAlignment.center, + alignment: WrapAlignment.spaceAround, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...widget.reactionIcons.mapIndexed((index, icon) { + bool reactionCheck(Reaction reaction) => reaction.type == icon.type; + + final ownReactions = [...?widget.message.ownReactions]; + final reaction = ownReactions.firstWhereOrNull(reactionCheck); + + final animation = _iconAnimations[index]; + return AnimatedBuilder( + animation: animation, + builder: (context, child) => Transform.scale( + scale: animation.value, + child: child, + ), + child: Builder( + builder: (context) { + final pickerIcon = ReactionPickerIcon( + type: icon.type, + builder: icon.builder, + // If the reaction is present in ownReactions, it is selected. + isSelected: reaction != null, + ); + + final onPressed = switch (widget.onReactionPicked) { + final onPicked? => () { + final picked = reaction ?? icon.toReaction(); + return onPicked(picked); + }, + _ => null, + }; + + return widget.iconBuilder(context, pickerIcon, onPressed); + }, + ), + ); + }), + ], + ); + + return TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + curve: Curves.easeOutBack, + duration: const Duration(milliseconds: 335), + builder: (context, scale, child) { + return Transform.scale(scale: scale, child: child); + }, + child: child, + ); + } +} + +/// {@template reactionPickerIcon} +/// A data class that represents a reaction icon within the reaction picker. +/// +/// This class holds information about a specific reaction, such as its type, +/// whether it's currently selected by the user, and a builder function +/// to construct its visual representation. +/// {@endtemplate} +class ReactionPickerIcon { + /// {@macro reactionPickerIcon} + const ReactionPickerIcon({ + required this.type, + this.isSelected = false, + this.iconSize = 24, + required ReactionIconBuilder builder, + }) : _builder = builder; + + /// The unique identifier for the reaction type (e.g., "like", "love"). + final String type; + + /// A boolean indicating whether this reaction is currently selected by the + /// user. + final bool isSelected; + + /// The size of the reaction icon. + final double iconSize; + + /// Builds the actual widget for this reaction icon using the provided + /// [context], selection state, and icon size. + Widget build(BuildContext context) => _builder(context, isSelected, iconSize); + final ReactionIconBuilder _builder; +} + +/// {@template reactionIconButton} +/// A button that displays a reaction icon. +/// +/// This button is used in the reaction picker to display individual reaction +/// options. +/// {@endtemplate} +class ReactionIconButton extends StatelessWidget { + /// {@macro reactionIconButton} + const ReactionIconButton({ + super.key, + required this.icon, + this.onPressed, + }); + + /// The reaction icon to display. + final ReactionPickerIcon icon; + + /// Callback triggered when the reaction picker icon is pressed. + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return IconButton( + key: Key(icon.type), + iconSize: icon.iconSize, + onPressed: onPressed, + icon: icon.build(context), + padding: const EdgeInsets.all(4), + style: IconButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: Size.square(icon.iconSize), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/reactions/reaction_bubble.dart similarity index 76% rename from packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart rename to packages/stream_chat_flutter/lib/src/reactions/reaction_bubble.dart index 82e5b34d02..88386c9d09 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/reactions/reaction_bubble.dart @@ -1,6 +1,5 @@ import 'dart:math'; -import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -60,44 +59,33 @@ class StreamReactionBubble extends StatelessWidget { padding: const EdgeInsets.all(2), decoration: BoxDecoration( color: maskColor, - borderRadius: const BorderRadius.all(Radius.circular(16)), + borderRadius: const BorderRadius.all(Radius.circular(26)), ), child: Container( padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: totalReactions > 1 ? 4.0 : 0, + vertical: 8, + horizontal: totalReactions > 1 ? 12.0 : 8.0, ), decoration: BoxDecoration( - border: Border.all( - color: borderColor, - ), color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(14)), + border: Border.all(color: borderColor), + borderRadius: const BorderRadius.all(Radius.circular(24)), ), - child: LayoutBuilder( - builder: (context, constraints) => Flex( - direction: Axis.horizontal, - mainAxisSize: MainAxisSize.min, - children: [ - if (constraints.maxWidth < double.infinity) - ...reactions - .take((constraints.maxWidth) ~/ 24) - .map((reaction) => _buildReaction( - reactionIcons, - reaction, - context, - )) - .toList(), - if (constraints.maxWidth == double.infinity) - ...reactions - .map((reaction) => _buildReaction( - reactionIcons, - reaction, - context, - )) - .toList(), - ], - ), + child: Wrap( + spacing: 8, + runSpacing: 4, + runAlignment: WrapAlignment.center, + alignment: WrapAlignment.spaceAround, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...reactions.map( + (reaction) => _buildReaction( + context, + reaction, + reactionIcons, + ), + ) + ], ), ), ), @@ -113,35 +101,20 @@ class StreamReactionBubble extends StatelessWidget { } Widget _buildReaction( - List reactionIcons, - Reaction reaction, BuildContext context, + Reaction reaction, + List reactionIcons, ) { - final reactionIcon = reactionIcons.firstWhereOrNull( - (r) => r.type == reaction.type, + final reactionIcon = reactionIcons.firstWhere( + (it) => it.type == reaction.type, + orElse: () => const StreamReactionIcon.unknown(), ); - final chatThemeData = StreamChatTheme.of(context); - final userId = StreamChat.of(context).currentUser?.id; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: reactionIcon != null - ? ConstrainedBox( - constraints: BoxConstraints.tight(const Size.square(14)), - child: reactionIcon.builder( - context, - highlightOwnReactions && reaction.user?.id == userId, - 16, - ), - ) - : Icon( - Icons.help_outline_rounded, - size: 14, - color: (highlightOwnReactions && reaction.user?.id == userId) - ? chatThemeData.colorTheme.accentPrimary - : chatThemeData.colorTheme.textLowEmphasis, - ), - ); + final currentUser = StreamChat.of(context).currentUser; + final isMyReaction = reaction.userId == currentUser?.id; + final isHighlighted = highlightOwnReactions && isMyReaction; + + return reactionIcon.builder(context, isHighlighted, 16); } Widget _buildReactionsTail(BuildContext context) { diff --git a/packages/stream_chat_flutter/lib/src/reactions/reaction_bubble_overlay.dart b/packages/stream_chat_flutter/lib/src/reactions/reaction_bubble_overlay.dart new file mode 100644 index 0000000000..bd1b50034e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/reactions/reaction_bubble_overlay.dart @@ -0,0 +1,373 @@ +// ignore_for_file: cascade_invocations + +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:stream_chat_flutter/src/misc/size_change_listener.dart'; + +/// Signature for building a custom ReactionBubble widget. +typedef ReactionBubbleBuilder = Widget Function( + BuildContext context, + ReactionBubbleConfig config, + Widget child, +); + +/// Defines the anchor settings for positioning a ReactionBubble relative to a +/// target widget. +class ReactionBubbleAnchor { + /// Creates an anchor with custom alignment and offset. + const ReactionBubbleAnchor({ + this.offset = Offset.zero, + required this.target, + required this.follower, + this.shiftToWithinBound = const AxisFlag(x: true), + }); + + /// Creates an anchor that positions the bubble at the top-end of the + /// target widget. + const ReactionBubbleAnchor.topEnd({ + this.offset = Offset.zero, + this.shiftToWithinBound = const AxisFlag(x: true), + }) : target = AlignmentDirectional.topEnd, + follower = AlignmentDirectional.bottomCenter; + + /// Creates an anchor that positions the bubble at the top-start of the + /// target widget. + const ReactionBubbleAnchor.topStart({ + this.offset = Offset.zero, + this.shiftToWithinBound = const AxisFlag(x: true), + }) : target = AlignmentDirectional.topStart, + follower = AlignmentDirectional.bottomCenter; + + /// Additional offset applied to the bubble position. + final Offset offset; + + /// Target alignment relative to the target widget. + final AlignmentDirectional target; + + /// Alignment of the bubble follower relative to the target alignment. + final AlignmentDirectional follower; + + /// Whether to shift the bubble within the visible screen bounds along each + /// axis if it exceeds the screen size. + final AxisFlag shiftToWithinBound; +} + +/// An overlay widget that displays a reaction bubble near a child widget. +class ReactionBubbleOverlay extends StatefulWidget { + /// Creates a new instance of [ReactionBubbleOverlay]. + const ReactionBubbleOverlay({ + super.key, + this.visible = true, + required this.child, + required this.reaction, + this.childSizeDelta = Offset.zero, + this.builder = _defaultBuilder, + this.config = const ReactionBubbleConfig(), + this.anchor = const ReactionBubbleAnchor.topEnd(), + }); + + /// The target child widget to anchor the reaction bubble to. + final Widget child; + + /// The reaction widget to display inside the bubble. + final Widget reaction; + + /// Whether the reaction bubble is visible. + final bool visible; + + /// Optional adjustment to the child's reported size. + final Offset childSizeDelta; + + /// The configuration used for rendering the reaction bubble. + final ReactionBubbleConfig config; + + /// The anchor configuration to control bubble positioning. + final ReactionBubbleAnchor anchor; + + /// The builder used to create the bubble appearance. + final ReactionBubbleBuilder builder; + + static Widget _defaultBuilder( + BuildContext context, + ReactionBubbleConfig config, + Widget child, + ) { + return RepaintBoundary( + child: CustomPaint( + painter: ReactionBubblePainter(config: config), + child: child, + ), + ); + } + + @override + State createState() => _ReactionBubbleOverlayState(); +} + +class _ReactionBubbleOverlayState extends State { + Size? _childSize; + + /// Calculates the alignment for the bubble tail relative to the bubble rect. + AlignmentGeometry _calculateTailAlignment({ + required Size childSize, + required Rect bubbleRect, + required Size availableSpace, + bool reverse = false, + }) { + final childEdgeX = switch (reverse) { + true => availableSpace.width - childSize.width, + false => childSize.width + }; + + final idealBubbleLeft = childEdgeX - (bubbleRect.width / 2); + final maxLeft = availableSpace.width - bubbleRect.width; + final actualBubbleLeft = idealBubbleLeft.clamp(0, math.max(0, maxLeft)); + final tailOffset = childEdgeX - actualBubbleLeft; + + if (tailOffset == 0) return AlignmentDirectional.bottomCenter; + return AlignmentDirectional((tailOffset * 2 / bubbleRect.width) - 1, 1); + } + + @override + Widget build(BuildContext context) { + final child = SizeChangeListener( + onSizeChanged: (size) => setState(() => _childSize = size), + child: widget.child, + ); + + final childSize = _childSize; + // If the child size is not available or the overlay should not be visible, + // return the child without any overlay. + if (childSize == null || !widget.visible) return child; + + final alignment = widget.anchor; + final direction = Directionality.maybeOf(context); + final targetAlignment = alignment.target.resolve(direction); + final followerAlignment = alignment.follower.resolve(direction); + final availableSpace = MediaQuery.sizeOf(context); + + final reverse = targetAlignment.x < 0; + final config = widget.config.copyWith( + flipTail: reverse, + tailAlignment: (bubbleRect) { + final alignment = _calculateTailAlignment( + reverse: reverse, + bubbleRect: bubbleRect, + availableSpace: availableSpace, + childSize: childSize + widget.childSizeDelta, + ); + + return alignment.resolve(direction); + }, + ); + + return PortalTarget( + anchor: Aligned( + target: targetAlignment, + follower: followerAlignment, + offset: widget.anchor.offset, + shiftToWithinBound: widget.anchor.shiftToWithinBound, + ), + portalFollower: widget.builder(context, config, widget.reaction), + child: child, + ); + } +} + +/// Defines the visual configuration of a ReactionBubble. +class ReactionBubbleConfig { + /// Creates a new instance of [ReactionBubbleConfig] with default values. + const ReactionBubbleConfig({ + this.flipTail = false, + this.fillColor, + this.maskColor, + this.borderColor, + this.maskWidth = 2.0, + this.borderWidth = 1.0, + this.bigTailCircleRadius = 4.0, + this.smallTailCircleRadius = 2.0, + this.tailAlignment = _defaultTailAlignment, + }); + + /// Whether to flip the tail horizontally. + final bool flipTail; + + /// Fill color of the bubble. + final Color? fillColor; + + /// Mask color of the bubble (used for visual masking). + final Color? maskColor; + + /// Border color of the bubble. + final Color? borderColor; + + /// Width of the mask stroke. + final double maskWidth; + + /// Width of the border stroke. + final double borderWidth; + + /// Radius of the larger circle at the bubble tail. + final double bigTailCircleRadius; + + /// Radius of the smaller circle at the bubble tail. + final double smallTailCircleRadius; + + /// Function that defines the alignment of the tail within the bubble rect. + final Alignment Function(Rect) tailAlignment; + + static Alignment _defaultTailAlignment(Rect rect) => Alignment.bottomCenter; + + /// The total height contribution of the bubble tail. + double get tailHeight => bigTailCircleRadius * 2 + smallTailCircleRadius * 2; + + /// Returns a copy of this config with optional overrides. + ReactionBubbleConfig copyWith({ + bool? flipTail, + Color? fillColor, + Color? maskColor, + Color? borderColor, + double? maskWidth, + double? borderWidth, + double? bigTailCircleRadius, + double? smallTailCircleRadius, + Alignment Function(Rect)? tailAlignment, + }) { + return ReactionBubbleConfig( + flipTail: flipTail ?? this.flipTail, + fillColor: fillColor ?? this.fillColor, + maskColor: maskColor ?? this.maskColor, + borderColor: borderColor ?? this.borderColor, + maskWidth: maskWidth ?? this.maskWidth, + borderWidth: borderWidth ?? this.borderWidth, + bigTailCircleRadius: bigTailCircleRadius ?? this.bigTailCircleRadius, + smallTailCircleRadius: + smallTailCircleRadius ?? this.smallTailCircleRadius, + tailAlignment: tailAlignment ?? this.tailAlignment, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is ReactionBubbleConfig && + runtimeType == other.runtimeType && + flipTail == other.flipTail && + fillColor == other.fillColor && + maskColor == other.maskColor && + borderColor == other.borderColor && + maskWidth == other.maskWidth && + borderWidth == other.borderWidth && + bigTailCircleRadius == other.bigTailCircleRadius && + smallTailCircleRadius == other.smallTailCircleRadius && + tailAlignment == other.tailAlignment; + } + + @override + int get hashCode => + flipTail.hashCode ^ + fillColor.hashCode ^ + maskColor.hashCode ^ + borderColor.hashCode ^ + maskWidth.hashCode ^ + borderWidth.hashCode ^ + bigTailCircleRadius.hashCode ^ + smallTailCircleRadius.hashCode ^ + tailAlignment.hashCode; +} + +/// A CustomPainter that draws a ReactionBubble based on a ReactionBubbleConfig. +class ReactionBubblePainter extends CustomPainter { + /// Creates a [ReactionBubblePainter] with the specified configuration. + ReactionBubblePainter({ + this.config = const ReactionBubbleConfig(), + }) : _fillPaint = Paint() + ..color = config.fillColor ?? Colors.white + ..style = PaintingStyle.fill, + _maskPaint = Paint() + ..color = config.maskColor ?? Colors.white + ..style = PaintingStyle.fill, + _borderPaint = Paint() + ..color = config.borderColor ?? Colors.black + ..style = PaintingStyle.stroke + ..strokeWidth = config.borderWidth; + + /// Configuration used to style the bubble. + final ReactionBubbleConfig config; + + final Paint _fillPaint; + final Paint _borderPaint; + final Paint _maskPaint; + + @override + void paint(Canvas canvas, Size size) { + final tailHeight = config.tailHeight; + final fullHeight = size.height + tailHeight; + final bubbleHeight = fullHeight - tailHeight; + final bubbleWidth = size.width; + + final bubbleRect = RRect.fromRectAndRadius( + Rect.fromLTRB(0, 0, bubbleWidth, bubbleHeight), + Radius.circular(bubbleHeight / 2), + ); + + final alignment = config.tailAlignment.call(bubbleRect.outerRect); + final bigTailCircleCenter = alignment.withinRect(bubbleRect.tallMiddleRect); + + final bigTailCircleRect = Rect.fromCircle( + center: bigTailCircleCenter, + radius: config.bigTailCircleRadius, + ); + + final smallTailCircleOffset = Offset( + config.flipTail ? bigTailCircleRect.right : bigTailCircleRect.left, + bigTailCircleRect.bottom + config.smallTailCircleRadius, + ); + + final smallTailCircleRect = Rect.fromCircle( + center: smallTailCircleOffset, + radius: config.smallTailCircleRadius, + ); + + final reactionBubbleMaskPath = _buildCombinedPath( + bubbleRect.inflate(config.maskWidth), + bigTailCircleRect.inflate(config.maskWidth), + smallTailCircleRect.inflate(config.maskWidth), + ); + + canvas.drawPath(reactionBubbleMaskPath, _maskPaint); + + final reactionBubblePath = _buildCombinedPath( + bubbleRect, + bigTailCircleRect, + smallTailCircleRect, + ); + + canvas.drawPath(reactionBubblePath, _borderPaint); + canvas.drawPath(reactionBubblePath, _fillPaint); + } + + /// Builds a combined path of the bubble and tail circles. + Path _buildCombinedPath( + RRect bubble, + Rect bigCircle, + Rect smallCircle, + ) { + final bubblePath = Path()..addRRect(bubble); + final bigTailPath = Path()..addOval(bigCircle); + final smallTailPath = Path()..addOval(smallCircle); + + return Path.combine( + PathOperation.union, + Path.combine(PathOperation.union, bubblePath, bigTailPath), + smallTailPath, + ); + } + + @override + bool shouldRepaint(covariant ReactionBubblePainter oldDelegate) { + return true; + } +} diff --git a/packages/stream_chat_flutter/lib/src/reactions/user_reactions.dart b/packages/stream_chat_flutter/lib/src/reactions/user_reactions.dart new file mode 100644 index 0000000000..68065e8afc --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/reactions/user_reactions.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/avatars/user_avatar.dart'; +import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; +import 'package:stream_chat_flutter/src/misc/reaction_icon.dart'; +import 'package:stream_chat_flutter/src/reactions/indicator/reaction_indicator_icon_list.dart'; +import 'package:stream_chat_flutter/src/reactions/reaction_bubble_overlay.dart'; +import 'package:stream_chat_flutter/src/stream_chat_configuration.dart'; +import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// {@template streamUserReactions} +/// A widget that displays the reactions of a user to a message. +/// +/// This widget is typically used in a modal or a dedicated section +/// to show all reactions made by users on a specific message. +/// {@endtemplate} +class StreamUserReactions extends StatelessWidget { + /// {@macro streamUserReactions} + const StreamUserReactions({ + super.key, + required this.message, + this.onUserAvatarTap, + }); + + /// Message to display reactions of. + final Message message; + + /// {@macro onUserAvatarTap} + final ValueSetter? onUserAvatarTap; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final textTheme = theme.textTheme; + final colorTheme = theme.colorTheme; + + return Material( + color: colorTheme.barsBg, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.translations.messageReactionsLabel, + style: textTheme.headlineBold, + ), + const SizedBox(height: 16), + Flexible( + child: SingleChildScrollView( + child: Wrap( + spacing: 16, + runSpacing: 16, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + ...?message.latestReactions?.map((reaction) { + return _UserReactionItem( + key: Key('${reaction.userId}-${reaction.type}'), + reaction: reaction, + onTap: onUserAvatarTap, + ); + }), + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +class _UserReactionItem extends StatelessWidget { + const _UserReactionItem({ + super.key, + required this.reaction, + this.onTap, + }); + + final Reaction reaction; + + /// {@macro onUserAvatarTap} + final ValueSetter? onTap; + + @override + Widget build(BuildContext context) { + final reactionUser = reaction.user; + if (reactionUser == null) return const Empty(); + + final currentUser = StreamChatCore.of(context).currentUser; + final isCurrentUserReaction = reactionUser.id == currentUser?.id; + + final theme = StreamChatTheme.of(context); + final messageTheme = theme.getMessageTheme(reverse: isCurrentUserReaction); + + final config = StreamChatConfiguration.of(context); + final reactionIcons = config.reactionIcons; + + final reactionIcon = reactionIcons.firstWhere( + (it) => it.type == reaction.type, + orElse: () => const StreamReactionIcon.unknown(), + ); + + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + StreamUserAvatar( + onTap: onTap, + user: reactionUser, + showOnlineStatus: false, + borderRadius: BorderRadius.circular(32), + constraints: const BoxConstraints.tightFor(height: 64, width: 64), + ), + PositionedDirectional( + bottom: 8, + end: isCurrentUserReaction ? null : 0, + start: isCurrentUserReaction ? 0 : null, + child: IgnorePointer( + child: RepaintBoundary( + child: CustomPaint( + painter: ReactionBubblePainter( + config: ReactionBubbleConfig( + flipTail: isCurrentUserReaction, + fillColor: messageTheme.reactionsBackgroundColor, + borderColor: messageTheme.reactionsBorderColor, + maskColor: messageTheme.reactionsMaskColor, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: ReactionIndicatorIconList( + indicatorIcons: [ + ReactionIndicatorIcon( + type: reactionIcon.type, + isSelected: isCurrentUserReaction, + builder: reactionIcon.builder, + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + reactionUser.name.split(' ')[0], + style: theme.textTheme.footnoteBold, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart index e13d607fa2..64a482d4e8 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart @@ -21,18 +21,69 @@ class StreamChatConfiguration extends InheritedWidget { bool updateShouldNotify(StreamChatConfiguration oldWidget) => data != oldWidget.data; - /// Use this method to get the current [StreamChatThemeData] instance + /// Finds the [StreamChatConfigurationData] from the closest + /// [StreamChatConfiguration] ancestor that encloses the given context. + /// + /// This will throw a [FlutterError] if no [StreamChatConfiguration] is found + /// in the widget tree above the given context. + /// + /// Typical usage: + /// + /// ```dart + /// final config = StreamChatConfiguration.of(context); + /// ``` + /// + /// If you're calling this in the same `build()` method that creates the + /// `StreamChatConfiguration`, consider using a `Builder` or refactoring into + /// a separate widget to obtain a context below the [StreamChatConfiguration]. + /// + /// If you want to return null instead of throwing, use [maybeOf]. static StreamChatConfigurationData of(BuildContext context) { + final result = maybeOf(context); + if (result != null) return result; + + throw FlutterError.fromParts([ + ErrorSummary( + 'StreamChatConfiguration.of() called with a context that does not ' + 'contain a StreamChatConfiguration.', + ), + ErrorDescription( + 'No StreamChatConfiguration ancestor could be found starting from the ' + 'context that was passed to StreamChatConfiguration.of(). This usually ' + 'happens when the context used comes from the widget that creates the ' + 'StreamChatConfiguration itself.', + ), + ErrorHint( + 'To fix this, ensure that you are using a context that is a descendant ' + 'of the StreamChatConfiguration. You can use a Builder to get a new ' + 'context that is under the StreamChatConfiguration:\n\n' + ' Builder(\n' + ' builder: (context) {\n' + ' final config = StreamChatConfiguration.of(context);\n' + ' ...\n' + ' },\n' + ' )', + ), + ErrorHint( + 'Alternatively, split your build method into smaller widgets so that ' + 'you get a new BuildContext that is below the StreamChatConfiguration ' + 'in the widget tree.', + ), + context.describeElement('The context used was'), + ]); + } + + /// Finds the [StreamChatConfigurationData] from the closest + /// [StreamChatConfiguration] ancestor that encloses the given context. + /// + /// Returns null if no such ancestor exists. + /// + /// See also: + /// * [of], which throws if no [StreamChatConfiguration] is found. + static StreamChatConfigurationData? maybeOf(BuildContext context) { final streamChatConfiguration = context.dependOnInheritedWidgetOfExactType(); - - assert( - streamChatConfiguration != null, - ''' -You must have a StreamChatConfigurationProvider widget at the top of your widget tree''', - ); - - return streamChatConfiguration!.data; + return streamChatConfiguration?.data; } } @@ -120,13 +171,13 @@ class StreamChatConfigurationData { loadingIndicator: loadingIndicator, defaultUserImage: defaultUserImage ?? _defaultUserImage, placeholderUserImage: placeholderUserImage, - reactionIcons: reactionIcons ?? _defaultReactionIcons, + reactionIcons: reactionIcons ?? StreamReactionIcon.defaultReactions, enforceUniqueReactions: enforceUniqueReactions ?? true, draftMessagesEnabled: draftMessagesEnabled, ); } - StreamChatConfigurationData._({ + const StreamChatConfigurationData._({ required this.loadingIndicator, required this.defaultUserImage, required this.placeholderUserImage, @@ -176,78 +227,15 @@ class StreamChatConfigurationData { /// Whether a new reaction should replace the existing one. final bool enforceUniqueReactions; - static final _defaultReactionIcons = [ - StreamReactionIcon( - type: 'love', - builder: (context, highlighted, size) { - final theme = StreamChatTheme.of(context); - return StreamSvgIcon( - icon: StreamSvgIcons.loveReaction, - color: highlighted - ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color, - size: size, - ); - }, - ), - StreamReactionIcon( - type: 'like', - builder: (context, highlighted, size) { - final theme = StreamChatTheme.of(context); - return StreamSvgIcon( - icon: StreamSvgIcons.thumbsUpReaction, - color: highlighted - ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color, - size: size, - ); - }, - ), - StreamReactionIcon( - type: 'sad', - builder: (context, highlighted, size) { - final theme = StreamChatTheme.of(context); - return StreamSvgIcon( - icon: StreamSvgIcons.thumbsDownReaction, - color: highlighted - ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color, - size: size, - ); - }, - ), - StreamReactionIcon( - type: 'haha', - builder: (context, highlighted, size) { - final theme = StreamChatTheme.of(context); - return StreamSvgIcon( - icon: StreamSvgIcons.lolReaction, - color: highlighted - ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color, - size: size, - ); - }, - ), - StreamReactionIcon( - type: 'wow', - builder: (context, highlighted, size) { - final theme = StreamChatTheme.of(context); - return StreamSvgIcon( - icon: StreamSvgIcons.wutReaction, - color: highlighted - ? theme.colorTheme.accentPrimary - : theme.primaryIconTheme.color, - size: size, - ); - }, - ), - ]; - - static Widget _defaultUserImage(BuildContext context, User user) => Center( - child: StreamGradientAvatar( - name: user.name, - userId: user.id, - ), - ); + static Widget _defaultUserImage( + BuildContext context, + User user, + ) { + return Center( + child: StreamGradientAvatar( + name: user.name, + userId: user.id, + ), + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/theme/color_theme.dart b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart index 167c68ee61..785af3c404 100644 --- a/packages/stream_chat_flutter/lib/src/theme/color_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart @@ -1,84 +1,110 @@ import 'package:flutter/material.dart'; -/// {@template color_theme} -/// Theme that holds colors -/// {@endtemplate} +/// Defines a color theme for the Stream Chat UI, +/// including core surfaces, text colors, accents, and visual effects. +/// +/// This theme provides two variants: +/// - `StreamColorTheme.light`: for light mode +/// - `StreamColorTheme.dark`: for dark mode class StreamColorTheme { - /// Initialise with light theme - StreamColorTheme.light({ + /// Creates a [StreamColorTheme] instance based on the provided [brightness]. + /// + /// Returns a light theme when [brightness] is [Brightness.light] and + /// a dark theme when [brightness] is [Brightness.dark]. + factory StreamColorTheme({ + Brightness brightness = Brightness.light, + }) { + return switch (brightness) { + Brightness.light => const StreamColorTheme.light(), + Brightness.dark => const StreamColorTheme.dark(), + }; + } + + /// Creates a light mode [StreamColorTheme] using design system values. + const StreamColorTheme.light({ this.textHighEmphasis = const Color(0xff000000), - this.textLowEmphasis = const Color(0xff7a7a7a), - this.disabled = const Color(0xffdbdbdb), - this.borders = const Color(0xffecebeb), + this.textLowEmphasis = const Color(0xff72767e), + this.disabled = const Color(0xffb4b7bb), + this.borders = const Color(0xffdbdde1), this.inputBg = const Color(0xffe9eaed), - this.appBg = const Color(0xfff7f7f8), + this.appBg = const Color(0xffffffff), this.barsBg = const Color(0xffffffff), this.linkBg = const Color(0xffe9f2ff), - this.accentPrimary = const Color(0xff005FFF), - this.accentError = const Color(0xffFF3842), - this.accentInfo = const Color(0xff20E070), + this.accentPrimary = const Color(0xff005fff), + this.accentError = const Color(0xffff3742), + this.accentInfo = const Color(0xff20e070), this.highlight = const Color(0xfffbf4dd), this.overlay = const Color.fromRGBO(0, 0, 0, 0.2), this.overlayDark = const Color.fromRGBO(0, 0, 0, 0.6), this.bgGradient = const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [Color(0xfff7f7f7), Color(0xfffcfcfc)], + colors: [Color(0xfff7f7f8), Color(0xffe9eaed)], stops: [0, 1], ), this.borderTop = const Effect( sigmaX: 0, sigmaY: -1, - color: Color(0xff000000), + color: Color(0xffdbdde1), blur: 0, - alpha: 0.08, + alpha: 1, ), this.borderBottom = const Effect( sigmaX: 0, sigmaY: 1, - color: Color(0xff000000), + color: Color(0xffdbdde1), blur: 0, - alpha: 0.08, + alpha: 1, ), this.shadowIconButton = const Effect( sigmaX: 0, sigmaY: 2, color: Color(0xff000000), - alpha: 0.5, blur: 4, + alpha: 0.25, ), this.modalShadow = const Effect( sigmaX: 0, sigmaY: 0, color: Color(0xff000000), - alpha: 1, - blur: 8, + blur: 4, + alpha: 0.6, ), }) : brightness = Brightness.light; - /// Initialise with dark theme - StreamColorTheme.dark({ + /// Creates a dark mode [StreamColorTheme] using design system values. + const StreamColorTheme.dark({ this.textHighEmphasis = const Color(0xffffffff), - this.textLowEmphasis = const Color(0xff7a7a7a), - this.disabled = const Color(0xff2d2f2f), - this.borders = const Color(0xff1c1e22), - this.inputBg = const Color(0xff13151b), + this.textLowEmphasis = const Color(0xff72767e), + this.disabled = const Color(0xff4c525c), + this.borders = const Color(0xff272a30), + this.inputBg = const Color(0xff1c1e22), this.appBg = const Color(0xff000000), this.barsBg = const Color(0xff121416), - this.linkBg = const Color(0xff00193D), + this.linkBg = const Color(0xff00193d), this.accentPrimary = const Color(0xff337eff), - this.accentError = const Color(0xffFF3742), - this.accentInfo = const Color(0xff20E070), + this.accentError = const Color(0xffff3742), + this.accentInfo = const Color(0xff20e070), + this.highlight = const Color(0xff302d22), + this.overlay = const Color.fromRGBO(0, 0, 0, 0.4), + this.overlayDark = const Color.fromRGBO(255, 255, 255, 0.6), + this.bgGradient = const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xff101214), Color(0xff070a0d)], + stops: [0, 1], + ), this.borderTop = const Effect( sigmaX: 0, sigmaY: -1, - color: Color(0xff141924), + color: Color(0xff272a30), blur: 0, + alpha: 1, ), this.borderBottom = const Effect( sigmaX: 0, sigmaY: 1, - color: Color(0xff141924), + color: Color(0xff272a30), blur: 0, alpha: 1, ), @@ -86,93 +112,81 @@ class StreamColorTheme { sigmaX: 0, sigmaY: 2, color: Color(0xff000000), - alpha: 0.5, blur: 4, + alpha: 0.5, ), this.modalShadow = const Effect( sigmaX: 0, sigmaY: 0, color: Color(0xff000000), - alpha: 1, blur: 8, - ), - this.highlight = const Color(0xff302d22), - this.overlay = const Color.fromRGBO(0, 0, 0, 0.4), - this.overlayDark = const Color.fromRGBO(255, 255, 255, 0.6), - this.bgGradient = const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0xff101214), - Color(0xff070a0d), - ], - stops: [0, 1], + alpha: 1, ), }) : brightness = Brightness.dark; - /// + /// Main body text or primary icons. final Color textHighEmphasis; - /// + /// Secondary or less prominent text/icons. final Color textLowEmphasis; - /// + /// Disabled UI elements (icons, inputs). final Color disabled; - /// + /// Standard UI borders and dividers. final Color borders; - /// + /// Background for input fields. final Color inputBg; - /// + /// Main app background. final Color appBg; - /// + /// Bars: headers, footers, and toolbars. final Color barsBg; - /// + /// Background for links and link cards. final Color linkBg; - /// + /// Primary action color (buttons, active states). final Color accentPrimary; - /// + /// Error color (alerts, badges). final Color accentError; - /// + /// Informational highlights (e.g., status). final Color accentInfo; - /// - final Effect borderTop; - - /// - final Effect borderBottom; - - /// - final Effect shadowIconButton; - - /// - final Effect modalShadow; - - /// + /// Highlighted rows, pinned messages. final Color highlight; - /// + /// General translucent overlay for modals, sheets. final Color overlay; - /// + /// Overlay for dark mode interactions or highlight effects. final Color overlayDark; - /// + /// Background gradient for section headers. final Gradient bgGradient; - /// + /// Theme brightness indicator. final Brightness brightness; - /// Copy with theme + /// Top border effect (for elevation). + final Effect borderTop; + + /// Bottom border effect. + final Effect borderBottom; + + /// Icon button drop shadow effect. + final Effect shadowIconButton; + + /// Modal shadow effect. + final Effect modalShadow; + + /// Returns a new [StreamColorTheme] by overriding selected fields. StreamColorTheme copyWith({ - Brightness brightness = Brightness.light, + Brightness? brightness, Color? textHighEmphasis, Color? textLowEmphasis, Color? disabled, @@ -184,16 +198,16 @@ class StreamColorTheme { Color? accentPrimary, Color? accentError, Color? accentInfo, - Effect? borderTop, - Effect? borderBottom, - Effect? shadowIconButton, - Effect? modalShadow, Color? highlight, Color? overlay, Color? overlayDark, Gradient? bgGradient, + Effect? borderTop, + Effect? borderBottom, + Effect? shadowIconButton, + Effect? modalShadow, }) { - return brightness == Brightness.light + return (brightness ?? this.brightness) == Brightness.light ? StreamColorTheme.light( textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, @@ -206,14 +220,14 @@ class StreamColorTheme { accentPrimary: accentPrimary ?? this.accentPrimary, accentError: accentError ?? this.accentError, accentInfo: accentInfo ?? this.accentInfo, - borderTop: borderTop ?? this.borderTop, - borderBottom: borderBottom ?? this.borderBottom, - shadowIconButton: shadowIconButton ?? this.shadowIconButton, - modalShadow: modalShadow ?? this.modalShadow, highlight: highlight ?? this.highlight, overlay: overlay ?? this.overlay, overlayDark: overlayDark ?? this.overlayDark, bgGradient: bgGradient ?? this.bgGradient, + borderTop: borderTop ?? this.borderTop, + borderBottom: borderBottom ?? this.borderBottom, + shadowIconButton: shadowIconButton ?? this.shadowIconButton, + modalShadow: modalShadow ?? this.modalShadow, ) : StreamColorTheme.dark( textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, @@ -227,18 +241,18 @@ class StreamColorTheme { accentPrimary: accentPrimary ?? this.accentPrimary, accentError: accentError ?? this.accentError, accentInfo: accentInfo ?? this.accentInfo, - borderTop: borderTop ?? this.borderTop, - borderBottom: borderBottom ?? this.borderBottom, - shadowIconButton: shadowIconButton ?? this.shadowIconButton, - modalShadow: modalShadow ?? this.modalShadow, highlight: highlight ?? this.highlight, overlay: overlay ?? this.overlay, overlayDark: overlayDark ?? this.overlayDark, bgGradient: bgGradient ?? this.bgGradient, + borderTop: borderTop ?? this.borderTop, + borderBottom: borderBottom ?? this.borderBottom, + shadowIconButton: shadowIconButton ?? this.shadowIconButton, + modalShadow: modalShadow ?? this.modalShadow, ); } - /// Merge color theme + /// Merges this theme with [other], replacing any fields that [other] defines. StreamColorTheme merge(StreamColorTheme? other) { if (other == null) return this; return copyWith( @@ -265,9 +279,9 @@ class StreamColorTheme { } } -/// Effect store +/// Visual effect such as blur or shadow used by the theme. class Effect { - /// Constructor for creating [Effect] + /// Creates an [Effect] instance. const Effect({ this.sigmaX, this.sigmaY, @@ -276,22 +290,22 @@ class Effect { this.blur, }); - /// + /// Horizontal shadow offset. final double? sigmaX; - /// + /// Vertical shadow offset. final double? sigmaY; - /// + /// Color of the shadow or border. final Color? color; - /// + /// Opacity (0โ€“1) of the effect. final double? alpha; - /// + /// Blur radius. final double? blur; - /// Copy with new effect + /// Returns a copy with updated fields. Effect copyWith({ double? sigmaX, double? sigmaY, @@ -303,7 +317,7 @@ class Effect { sigmaX: sigmaX ?? this.sigmaX, sigmaY: sigmaY ?? this.sigmaY, color: color ?? this.color, - alpha: color as double? ?? this.alpha, + alpha: alpha ?? this.alpha, blur: blur ?? this.blur, ); } diff --git a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart index 771f4ccb41..28c3ffa996 100644 --- a/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/stream_chat_theme.dart @@ -53,14 +53,9 @@ class StreamChatThemeData { Widget Function(BuildContext, User)? defaultUserImage, PlaceholderUserImage? placeholderUserImage, IconThemeData? primaryIconTheme, - @Deprecated('Use StreamChatConfigurationData.reactionIcons instead') - List? reactionIcons, StreamGalleryHeaderThemeData? imageHeaderTheme, StreamGalleryFooterThemeData? imageFooterTheme, StreamMessageListViewThemeData? messageListViewTheme, - @Deprecated( - "Use 'StreamChatThemeData.voiceRecordingAttachmentTheme' instead") - StreamVoiceRecordingThemeData? voiceRecordingTheme, StreamPollCreatorThemeData? pollCreatorTheme, StreamPollInteractorThemeData? pollInteractorTheme, StreamPollOptionsDialogThemeData? pollOptionsDialogTheme, @@ -74,9 +69,8 @@ class StreamChatThemeData { StreamVoiceRecordingAttachmentThemeData? voiceRecordingAttachmentTheme, }) { brightness ??= colorTheme?.brightness ?? Brightness.light; - final isDark = brightness == Brightness.dark; - textTheme ??= isDark ? StreamTextTheme.dark() : StreamTextTheme.light(); - colorTheme ??= isDark ? StreamColorTheme.dark() : StreamColorTheme.light(); + textTheme ??= StreamTextTheme(brightness: brightness); + colorTheme ??= StreamColorTheme(brightness: brightness); final defaultData = StreamChatThemeData.fromColorAndTextTheme( colorTheme, @@ -93,11 +87,9 @@ class StreamChatThemeData { defaultUserImage: defaultUserImage, placeholderUserImage: placeholderUserImage, primaryIconTheme: primaryIconTheme, - reactionIcons: reactionIcons, galleryHeaderTheme: imageHeaderTheme, galleryFooterTheme: imageFooterTheme, messageListViewTheme: messageListViewTheme, - voiceRecordingTheme: voiceRecordingTheme, pollCreatorTheme: pollCreatorTheme, pollInteractorTheme: pollInteractorTheme, pollOptionsDialogTheme: pollOptionsDialogTheme, @@ -136,7 +128,6 @@ class StreamChatThemeData { required this.galleryHeaderTheme, required this.galleryFooterTheme, required this.messageListViewTheme, - required this.voiceRecordingTheme, required this.pollCreatorTheme, required this.pollInteractorTheme, required this.pollResultsDialogTheme, @@ -180,7 +171,7 @@ class StreamChatThemeData { color: colorTheme.barsBg, titleStyle: textTheme.headlineBold, subtitleStyle: textTheme.footnote.copyWith( - color: const Color(0xff7A7A7A), + color: colorTheme.textLowEmphasis, ), ); final channelPreviewTheme = StreamChannelPreviewThemeData( @@ -245,7 +236,7 @@ class StreamChatThemeData { createdAtStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), repliesStyle: textTheme.footnoteBold.copyWith(color: accentColor), - messageBackgroundColor: colorTheme.borders, + messageBackgroundColor: colorTheme.inputBg, messageBorderColor: colorTheme.borders, reactionsBackgroundColor: colorTheme.barsBg, reactionsBorderColor: colorTheme.borders, @@ -309,14 +300,14 @@ class StreamChatThemeData { linkHighlightColor: colorTheme.accentPrimary, idleBorderGradient: LinearGradient( colors: [ - colorTheme.disabled, - colorTheme.disabled, + colorTheme.borders, + colorTheme.borders, ], ), activeBorderGradient: LinearGradient( colors: [ - colorTheme.disabled, - colorTheme.disabled, + colorTheme.borders, + colorTheme.borders, ], ), useSystemAttachmentPicker: false, @@ -340,7 +331,7 @@ class StreamChatThemeData { bottomSheetCloseIconColor: colorTheme.textHighEmphasis, ), messageListViewTheme: StreamMessageListViewThemeData( - backgroundColor: colorTheme.barsBg, + backgroundColor: colorTheme.appBg, ), pollCreatorTheme: StreamPollCreatorThemeData( backgroundColor: colorTheme.appBg, @@ -592,9 +583,6 @@ class StreamChatThemeData { ), audioWaveformSliderTheme: audioWaveformSliderTheme, ), - voiceRecordingTheme: colorTheme.brightness == Brightness.dark - ? StreamVoiceRecordingThemeData.dark() - : StreamVoiceRecordingThemeData.light(), ); } @@ -636,10 +624,6 @@ class StreamChatThemeData { /// Theme configuration for the [StreamMessageListView] widget. final StreamMessageListViewThemeData messageListViewTheme; - /// Theme configuration for the [StreamVoiceRecordingListPLayer] widget. - @Deprecated("Use 'StreamChatThemeData.voiceRecordingAttachmentTheme' instead") - final StreamVoiceRecordingThemeData voiceRecordingTheme; - /// Theme configuration for the [StreamPollCreatorWidget] widget. final StreamPollCreatorThemeData pollCreatorTheme; @@ -673,6 +657,15 @@ class StreamChatThemeData { /// Theme configuration for the [StreamDraftListTile] widget. final StreamDraftListTileThemeData draftListTileTheme; + /// Returns the theme for the message based on the [reverse] parameter. + /// + /// If [reverse] is true, it returns the [otherMessageTheme], otherwise it + /// returns the [ownMessageTheme]. + StreamMessageThemeData getMessageTheme({bool reverse = false}) { + if (reverse) return ownMessageTheme; + return otherMessageTheme; + } + /// Creates a copy of [StreamChatThemeData] with specified attributes /// overridden. StreamChatThemeData copyWith({ @@ -687,13 +680,9 @@ class StreamChatThemeData { PlaceholderUserImage? placeholderUserImage, IconThemeData? primaryIconTheme, StreamChannelListHeaderThemeData? channelListHeaderTheme, - @Deprecated('Use StreamChatConfigurationData.reactionIcons instead') - List? reactionIcons, StreamGalleryHeaderThemeData? galleryHeaderTheme, StreamGalleryFooterThemeData? galleryFooterTheme, StreamMessageListViewThemeData? messageListViewTheme, - @Deprecated("Use 'voiceRecordingAttachmentTheme' instead") - StreamVoiceRecordingThemeData? voiceRecordingTheme, StreamPollCreatorThemeData? pollCreatorTheme, StreamPollInteractorThemeData? pollInteractorTheme, StreamPollResultsDialogThemeData? pollResultsDialogTheme, @@ -721,7 +710,6 @@ class StreamChatThemeData { galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme, galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme, messageListViewTheme: messageListViewTheme ?? this.messageListViewTheme, - voiceRecordingTheme: voiceRecordingTheme ?? this.voiceRecordingTheme, pollCreatorTheme: pollCreatorTheme ?? this.pollCreatorTheme, pollInteractorTheme: pollInteractorTheme ?? this.pollInteractorTheme, pollResultsDialogTheme: @@ -759,7 +747,6 @@ class StreamChatThemeData { galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme), messageListViewTheme: messageListViewTheme.merge(other.messageListViewTheme), - voiceRecordingTheme: voiceRecordingTheme.merge(other.voiceRecordingTheme), pollCreatorTheme: pollCreatorTheme.merge(other.pollCreatorTheme), pollInteractorTheme: pollInteractorTheme.merge(other.pollInteractorTheme), pollResultsDialogTheme: diff --git a/packages/stream_chat_flutter/lib/src/theme/text_theme.dart b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart index 9662b4bcd9..62a30d1245 100644 --- a/packages/stream_chat_flutter/lib/src/theme/text_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart @@ -4,8 +4,21 @@ import 'package:flutter/material.dart'; /// Class for holding text theme /// {@endtemplate} class StreamTextTheme { + /// Creates a [StreamTextTheme] instance based on the provided [brightness]. + /// + /// Returns a light theme when [brightness] is [Brightness.light] and + /// a dark theme when [brightness] is [Brightness.dark]. + factory StreamTextTheme({ + Brightness brightness = Brightness.light, + }) { + return switch (brightness) { + Brightness.light => const StreamTextTheme.light(), + Brightness.dark => const StreamTextTheme.dark(), + }; + } + /// Initialise light text theme - StreamTextTheme.light({ + const StreamTextTheme.light({ this.title = const TextStyle( fontSize: 22, fontWeight: FontWeight.w500, @@ -49,7 +62,7 @@ class StreamTextTheme { }); /// Initialise with dark theme - StreamTextTheme.dark({ + const StreamTextTheme.dark({ this.title = const TextStyle( fontSize: 22, fontWeight: FontWeight.w500, diff --git a/packages/stream_chat_flutter/lib/src/theme/themes.dart b/packages/stream_chat_flutter/lib/src/theme/themes.dart index 9e41282aa6..867033b09e 100644 --- a/packages/stream_chat_flutter/lib/src/theme/themes.dart +++ b/packages/stream_chat_flutter/lib/src/theme/themes.dart @@ -18,5 +18,4 @@ export 'poll_options_dialog_theme.dart'; export 'poll_results_dialog_theme.dart'; export 'text_theme.dart'; export 'thread_list_tile_theme.dart'; -export 'voice_attachment_theme.dart'; export 'voice_recording_attachment_theme.dart'; diff --git a/packages/stream_chat_flutter/lib/src/theme/voice_attachment_theme.dart b/packages/stream_chat_flutter/lib/src/theme/voice_attachment_theme.dart deleted file mode 100644 index db1ed7db65..0000000000 --- a/packages/stream_chat_flutter/lib/src/theme/voice_attachment_theme.dart +++ /dev/null @@ -1,466 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template StreamVoiceRecordingThemeData} -/// The theme data for the voice recording attachment builder. -/// {@endtemplate} -@Deprecated("Use 'StreamVoiceRecordingAttachmentThemeData' instead.") -class StreamVoiceRecordingThemeData with Diagnosticable { - /// {@macro StreamVoiceRecordingThemeData} - const StreamVoiceRecordingThemeData({ - required this.loadingTheme, - required this.sliderTheme, - required this.listPlayerTheme, - required this.playerTheme, - }); - - /// {@template ThemeDataLight} - /// Creates a theme data with light values. - /// {@endtemplate} - factory StreamVoiceRecordingThemeData.light() { - return StreamVoiceRecordingThemeData( - loadingTheme: StreamVoiceRecordingLoadingThemeData.light(), - sliderTheme: StreamVoiceRecordingSliderTheme.light(), - listPlayerTheme: StreamVoiceRecordingListPlayerThemeData.light(), - playerTheme: StreamVoiceRecordingPlayerThemeData.light(), - ); - } - - /// {@template ThemeDataDark} - /// Creates a theme data with dark values. - /// {@endtemplate} - factory StreamVoiceRecordingThemeData.dark() { - return StreamVoiceRecordingThemeData( - loadingTheme: StreamVoiceRecordingLoadingThemeData.dark(), - sliderTheme: StreamVoiceRecordingSliderTheme.dark(), - listPlayerTheme: StreamVoiceRecordingListPlayerThemeData.dark(), - playerTheme: StreamVoiceRecordingPlayerThemeData.dark(), - ); - } - - /// The theme for the loading widget. - final StreamVoiceRecordingLoadingThemeData loadingTheme; - - /// The theme for the slider widget. - final StreamVoiceRecordingSliderTheme sliderTheme; - - /// The theme for the list player widget. - final StreamVoiceRecordingListPlayerThemeData listPlayerTheme; - - /// The theme for the player widget. - final StreamVoiceRecordingPlayerThemeData playerTheme; - - /// {@template ThemeDataMerge} - /// Used to merge the values of another theme data object into this. - /// {@endtemplate} - StreamVoiceRecordingThemeData merge(StreamVoiceRecordingThemeData? other) { - if (other == null) return this; - return StreamVoiceRecordingThemeData( - loadingTheme: loadingTheme.merge(other.loadingTheme), - sliderTheme: sliderTheme.merge(other.sliderTheme), - listPlayerTheme: listPlayerTheme.merge(other.listPlayerTheme), - playerTheme: playerTheme.merge(other.playerTheme), - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('loadingTheme', loadingTheme)) - ..add(DiagnosticsProperty('sliderTheme', sliderTheme)) - ..add(DiagnosticsProperty('listPlayerTheme', listPlayerTheme)) - ..add(DiagnosticsProperty('playerTheme', playerTheme)); - } -} - -/// {@template StreamAudioPlayerLoadingTheme} -/// The theme data for the voice recording attachment builder -/// loading widget [StreamVoiceRecordingLoading]. -/// {@endtemplate} -@Deprecated("Use 'StreamVoiceRecordingAttachmentThemeData' instead.") -class StreamVoiceRecordingLoadingThemeData with Diagnosticable { - /// {@macro StreamAudioPlayerLoadingTheme} - const StreamVoiceRecordingLoadingThemeData({ - this.size, - this.strokeWidth, - this.color, - this.padding, - }); - - /// {@macro ThemeDataLight} - factory StreamVoiceRecordingLoadingThemeData.light() { - return const StreamVoiceRecordingLoadingThemeData( - size: Size(20, 20), - strokeWidth: 2, - color: Color(0xFF005FFF), - padding: EdgeInsets.all(8), - ); - } - - /// {@macro ThemeDataDark} - factory StreamVoiceRecordingLoadingThemeData.dark() { - return const StreamVoiceRecordingLoadingThemeData( - size: Size(20, 20), - strokeWidth: 2, - color: Color(0xFF005FFF), - padding: EdgeInsets.all(8), - ); - } - - /// The size of the loading indicator. - final Size? size; - - /// The stroke width of the loading indicator. - final double? strokeWidth; - - /// The color of the loading indicator. - final Color? color; - - /// The padding around the loading indicator. - final EdgeInsets? padding; - - /// {@macro ThemeDataMerge} - StreamVoiceRecordingLoadingThemeData merge( - StreamVoiceRecordingLoadingThemeData? other) { - if (other == null) return this; - return StreamVoiceRecordingLoadingThemeData( - size: other.size, - strokeWidth: other.strokeWidth, - color: other.color, - padding: other.padding, - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('size', size)) - ..add(DiagnosticsProperty('strokeWidth', strokeWidth)) - ..add(ColorProperty('color', color)) - ..add(DiagnosticsProperty('padding', padding)); - } -} - -/// {@template StreamAudioPlayerSliderTheme} -/// The theme data for the voice recording attachment builder audio player -/// slider [StreamVoiceRecordingSlider]. -/// {@endtemplate} -@Deprecated("Use 'StreamVoiceRecordingAttachmentThemeData' instead.") -class StreamVoiceRecordingSliderTheme with Diagnosticable { - /// {@macro StreamAudioPlayerSliderTheme} - const StreamVoiceRecordingSliderTheme({ - this.horizontalPadding = 10, - this.spacingRatio = 0.007, - this.waveHeightRatio = 1, - this.buttonBorderRadius = const BorderRadius.all(Radius.circular(8)), - this.buttonColor, - this.buttonBorderColor, - this.buttonBorderWidth = 1, - this.waveColorPlayed, - this.waveColorUnplayed, - this.buttonShadow = const BoxShadow( - color: Color(0x33000000), - blurRadius: 4, - offset: Offset(0, 2), - ), - }); - - /// {@macro ThemeDataLight} - factory StreamVoiceRecordingSliderTheme.light() { - return const StreamVoiceRecordingSliderTheme( - buttonColor: Color(0xFFFFFFFF), - buttonBorderColor: Color(0x3308070733), - waveColorPlayed: Color(0xFF005DFF), - waveColorUnplayed: Color(0xFF7E828B), - ); - } - - /// {@macro ThemeDataDark} - factory StreamVoiceRecordingSliderTheme.dark() { - return const StreamVoiceRecordingSliderTheme( - buttonColor: Color(0xFF005FFF), - buttonBorderColor: Color(0x3308070766), - waveColorPlayed: Color(0xFF337EFF), - waveColorUnplayed: Color(0xFF7E828B), - ); - } - - /// The color of the slider button. - final Color? buttonColor; - - /// The color of the border of the slider button. - final Color? buttonBorderColor; - - /// The width of the border of the slider button. - final double? buttonBorderWidth; - - /// The shadow of the slider button. - final BoxShadow? buttonShadow; - - /// The border radius of the slider button. - final BorderRadius buttonBorderRadius; - - /// The horizontal padding of the slider. - final double horizontalPadding; - - /// Spacing ratios. This is the percentage that the space takes from the whole - /// available space. Typically this value should be between 0.003 to 0.02. - /// Default = 0.01 - final double spacingRatio; - - /// The percentage maximum value of waves. This can be used to reduce the - /// height of bars. Default = 1; - final double waveHeightRatio; - - /// Color of the waves to the left side of the slider button. - final Color? waveColorPlayed; - - /// Color of the waves to the right side of the slider button. - final Color? waveColorUnplayed; - - /// {@macro ThemeDataMerge} - StreamVoiceRecordingSliderTheme merge( - StreamVoiceRecordingSliderTheme? other) { - if (other == null) return this; - return StreamVoiceRecordingSliderTheme( - buttonColor: other.buttonColor, - buttonBorderColor: other.buttonBorderColor, - buttonBorderRadius: other.buttonBorderRadius, - horizontalPadding: other.horizontalPadding, - spacingRatio: other.spacingRatio, - waveHeightRatio: other.waveHeightRatio, - waveColorPlayed: other.waveColorPlayed, - waveColorUnplayed: other.waveColorUnplayed, - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(ColorProperty('buttonColor', buttonColor)) - ..add(ColorProperty('buttonBorderColor', buttonBorderColor)) - ..add(DiagnosticsProperty('buttonBorderRadius', buttonBorderRadius)) - ..add(DoubleProperty('horizontalPadding', horizontalPadding)) - ..add(DoubleProperty('spacingRatio', spacingRatio)) - ..add(DoubleProperty('waveHeightRatio', waveHeightRatio)) - ..add(ColorProperty('waveColorPlayed', waveColorPlayed)) - ..add(ColorProperty('waveColorUnplayed', waveColorUnplayed)); - } -} - -/// {@template StreamAudioListPlayerTheme} -/// The theme data for the voice recording attachment builder audio player -/// [StreamVoiceRecordingListPlayer]. -/// {@endtemplate} -@Deprecated("Use 'StreamVoiceRecordingAttachmentThemeData' instead.") -class StreamVoiceRecordingListPlayerThemeData with Diagnosticable { - /// {@macro StreamAudioListPlayerTheme} - const StreamVoiceRecordingListPlayerThemeData({ - this.backgroundColor, - this.borderColor, - this.borderRadius, - this.margin, - }); - - /// {@macro ThemeDataLight} - factory StreamVoiceRecordingListPlayerThemeData.light() { - return StreamVoiceRecordingListPlayerThemeData( - backgroundColor: const Color(0xFFFFFFFF), - borderColor: const Color(0xFFDBDDE1), - borderRadius: BorderRadius.circular(14), - margin: const EdgeInsets.all(4), - ); - } - - /// {@macro ThemeDataDark} - factory StreamVoiceRecordingListPlayerThemeData.dark() { - return StreamVoiceRecordingListPlayerThemeData( - backgroundColor: const Color(0xFF17191C), - borderColor: const Color(0xFF272A30), - borderRadius: BorderRadius.circular(14), - margin: const EdgeInsets.all(4), - ); - } - - /// The background color of the list. - final Color? backgroundColor; - - /// The border color of the list. - final Color? borderColor; - - /// The border radius of the list. - final BorderRadius? borderRadius; - - /// The margin of the list. - final EdgeInsets? margin; - - /// {@macro ThemeDataMerge} - StreamVoiceRecordingListPlayerThemeData merge( - StreamVoiceRecordingListPlayerThemeData? other) { - if (other == null) return this; - return StreamVoiceRecordingListPlayerThemeData( - backgroundColor: other.backgroundColor, - borderColor: other.borderColor, - borderRadius: other.borderRadius, - margin: other.margin, - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(ColorProperty('backgroundColor', backgroundColor)) - ..add(ColorProperty('borderColor', borderColor)) - ..add(DiagnosticsProperty('borderRadius', borderRadius)) - ..add(DiagnosticsProperty('margin', margin)); - } -} - -/// {@template StreamVoiceRecordingPlayerTheme} -/// The theme data for the voice recording attachment builder audio player -/// {@endtemplate} -@Deprecated("Use 'StreamVoiceRecordingAttachmentThemeData' instead.") -class StreamVoiceRecordingPlayerThemeData with Diagnosticable { - /// {@macro StreamVoiceRecordingPlayerTheme} - const StreamVoiceRecordingPlayerThemeData({ - this.playIcon = Icons.play_arrow, - this.pauseIcon = Icons.pause, - this.iconColor, - this.buttonBackgroundColor, - this.buttonPadding = const EdgeInsets.symmetric(horizontal: 6), - this.buttonShape = const CircleBorder(), - this.buttonElevation = 2, - this.speedButtonSize = const Size(44, 36), - this.speedButtonElevation = 2, - this.speedButtonPadding = const EdgeInsets.symmetric(horizontal: 8), - this.speedButtonBackgroundColor = const Color(0xFFFFFFFF), - this.speedButtonShape = const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(50)), - ), - this.speedButtonTextStyle = const TextStyle( - fontSize: 12, - color: Color(0xFF080707), - ), - this.fileTypeIcon = const StreamSvgIcon( - icon: StreamSvgIcons.filetypeAudioAac, - ), - this.fileSizeTextStyle = const TextStyle(fontSize: 10), - this.timerTextStyle, - }); - - /// {@macro ThemeDataLight} - factory StreamVoiceRecordingPlayerThemeData.light() { - return const StreamVoiceRecordingPlayerThemeData( - iconColor: Color(0xFF080707), - buttonBackgroundColor: Color(0xFFFFFFFF), - ); - } - - /// {@macro ThemeDataDark} - factory StreamVoiceRecordingPlayerThemeData.dark() { - return const StreamVoiceRecordingPlayerThemeData( - iconColor: Color(0xFF080707), - buttonBackgroundColor: Color(0xFFFFFFFF), - ); - } - - /// The icon to display when the player is paused/stopped. - final IconData playIcon; - - /// The icon to display when the player is playing. - final IconData pauseIcon; - - /// The color of the icons. - final Color? iconColor; - - /// The background color of the buttons. - final Color? buttonBackgroundColor; - - /// The padding of the buttons. - final EdgeInsets? buttonPadding; - - /// The shape of the buttons. - final OutlinedBorder? buttonShape; - - /// The elevation of the buttons. - final double? buttonElevation; - - /// The size of the speed button. - final Size? speedButtonSize; - - /// The elevation of the speed button. - final double? speedButtonElevation; - - /// The padding of the speed button. - final EdgeInsets? speedButtonPadding; - - /// The background color of the speed button. - final Color? speedButtonBackgroundColor; - - /// The shape of the speed button. - final OutlinedBorder? speedButtonShape; - - /// The text style of the speed button. - final TextStyle? speedButtonTextStyle; - - /// The icon to display for the file type. - final Widget? fileTypeIcon; - - /// The text style of the file size. - final TextStyle? fileSizeTextStyle; - - /// The text style of the timer. - final TextStyle? timerTextStyle; - - /// {@macro ThemeDataMerge} - StreamVoiceRecordingPlayerThemeData merge( - StreamVoiceRecordingPlayerThemeData? other) { - if (other == null) return this; - return StreamVoiceRecordingPlayerThemeData( - playIcon: other.playIcon, - pauseIcon: other.pauseIcon, - iconColor: other.iconColor, - buttonBackgroundColor: other.buttonBackgroundColor, - buttonPadding: other.buttonPadding, - buttonShape: other.buttonShape, - buttonElevation: other.buttonElevation, - speedButtonSize: other.speedButtonSize, - speedButtonElevation: other.speedButtonElevation, - speedButtonPadding: other.speedButtonPadding, - speedButtonBackgroundColor: other.speedButtonBackgroundColor, - speedButtonShape: other.speedButtonShape, - speedButtonTextStyle: other.speedButtonTextStyle, - fileTypeIcon: other.fileTypeIcon, - fileSizeTextStyle: other.fileSizeTextStyle, - timerTextStyle: other.timerTextStyle, - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty('playIcon', playIcon)) - ..add(DiagnosticsProperty('pauseIcon', pauseIcon)) - ..add(ColorProperty('iconColor', iconColor)) - ..add(ColorProperty('buttonBackgroundColor', buttonBackgroundColor)) - ..add(DiagnosticsProperty('buttonPadding', buttonPadding)) - ..add(DiagnosticsProperty('buttonShape', buttonShape)) - ..add(DoubleProperty('buttonElevation', buttonElevation)) - ..add(DiagnosticsProperty('speedButtonSize', speedButtonSize)) - ..add(DoubleProperty('speedButtonElevation', speedButtonElevation)) - ..add(DiagnosticsProperty('speedButtonPadding', speedButtonPadding)) - ..add(ColorProperty( - 'speedButtonBackgroundColor', speedButtonBackgroundColor)) - ..add(DiagnosticsProperty('speedButtonShape', speedButtonShape)) - ..add(DiagnosticsProperty('speedButtonTextStyle', speedButtonTextStyle)) - ..add(DiagnosticsProperty('fileTypeIcon', fileTypeIcon)) - ..add(DiagnosticsProperty('fileSizeTextStyle', fileSizeTextStyle)) - ..add(DiagnosticsProperty('timerTextStyle', timerTextStyle)); - } -} diff --git a/packages/stream_chat_flutter/lib/src/utils/extensions.dart b/packages/stream_chat_flutter/lib/src/utils/extensions.dart index dd33838495..ac638ca579 100644 --- a/packages/stream_chat_flutter/lib/src/utils/extensions.dart +++ b/packages/stream_chat_flutter/lib/src/utils/extensions.dart @@ -472,18 +472,12 @@ extension TypeX on T? { extension FileTypeX on FileType { /// Converts the [FileType] to a [String]. String toAttachmentType() { - switch (this) { - case FileType.image: - return AttachmentType.image; - case FileType.video: - return AttachmentType.video; - case FileType.audio: - return AttachmentType.audio; - case FileType.any: - case FileType.media: - case FileType.custom: - return AttachmentType.file; - } + return switch (this) { + FileType.image => AttachmentType.image, + FileType.video => AttachmentType.video, + FileType.audio => AttachmentType.audio, + FileType.any || FileType.media || FileType.custom => AttachmentType.file, + }; } } @@ -491,18 +485,16 @@ extension FileTypeX on FileType { extension AttachmentPickerTypeX on AttachmentPickerType { /// Converts the [AttachmentPickerType] to a [FileType]. FileType get fileType { - switch (this) { - case AttachmentPickerType.images: - return FileType.image; - case AttachmentPickerType.videos: - return FileType.video; - case AttachmentPickerType.files: - return FileType.any; - case AttachmentPickerType.audios: - return FileType.audio; - case AttachmentPickerType.poll: - throw Exception('Polls do not have a file type'); - } + return switch (this) { + ImagesPickerType() => FileType.image, + VideosPickerType() => FileType.video, + AudiosPickerType() => FileType.audio, + FilesPickerType() => FileType.any, + _ => throw Exception( + 'Unsupported AttachmentPickerType: $this. ' + 'Only Images, Videos, Audios and Files are supported.', + ), + }; } } @@ -569,24 +561,6 @@ extension MessageListX on Iterable { /// /// The [userRead] is the last read message by the user. /// - /// The last unread message is the last message in the list that is not - /// sent by the current user and is sent after the last read message. - @Deprecated("Use 'StreamChannel.getFirstUnreadMessage' instead.") - Message? lastUnreadMessage(Read? userRead) { - if (isEmpty || userRead == null) return null; - - if (first.createdAt.isAfter(userRead.lastRead) && - last.createdAt.isBefore(userRead.lastRead)) { - return lastWhereOrNull( - (it) => - it.user?.id != userRead.user.id && - it.id != userRead.lastReadMessageId && - it.createdAt.compareTo(userRead.lastRead) > 0, - ); - } - - return null; - } } /// Useful extensions on [ChannelModel]. @@ -694,3 +668,27 @@ extension AttachmentPlaylistExtension on Iterable { ]; } } + +/// Extension to convert [AlignmentGeometry] to the corresponding +/// [CrossAxisAlignment]. +extension ColumnAlignmentExtension on AlignmentGeometry { + /// Converts an [AlignmentGeometry] to the most appropriate + /// [CrossAxisAlignment] value. + CrossAxisAlignment toColumnCrossAxisAlignment() { + final x = switch (this) { + Alignment(x: final x) => x, + AlignmentDirectional(start: final start) => start, + _ => null, + }; + + // If the alignment is unknown, fallback to the center alignment. + if (x == null) return CrossAxisAlignment.center; + + return switch (x) { + 0.0 => CrossAxisAlignment.center, + < 0 => CrossAxisAlignment.start, + > 0 => CrossAxisAlignment.end, + _ => CrossAxisAlignment.center, // fallback (in case of NaN etc) + }; + } +} diff --git a/packages/stream_chat_flutter/lib/src/video/video_thumbnail_image.dart b/packages/stream_chat_flutter/lib/src/video/video_thumbnail_image.dart index 21018113fc..b53f6ebe86 100644 --- a/packages/stream_chat_flutter/lib/src/video/video_thumbnail_image.dart +++ b/packages/stream_chat_flutter/lib/src/video/video_thumbnail_image.dart @@ -87,10 +87,9 @@ class StreamVideoThumbnailImage } @override - @Deprecated('Will get replaced by loadImage in the next major version.') - ImageStreamCompleter loadBuffer( + ImageStreamCompleter loadImage( StreamVideoThumbnailImage key, - DecoderBufferCallback decode, + ImageDecoderCallback decode, ) { return MultiFrameImageStreamCompleter( codec: _loadAsync(key, decode), @@ -103,10 +102,9 @@ class StreamVideoThumbnailImage ); } - @Deprecated('Will get replaced by loadImage in the next major version.') Future _loadAsync( StreamVideoThumbnailImage key, - DecoderBufferCallback decode, + ImageDecoderCallback decode, ) async { assert(key == this, '$key is not $this'); diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index db05edbbfd..47ede68d4e 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -8,9 +8,6 @@ export 'src/ai_assistant/stream_typewriter_builder.dart'; export 'src/ai_assistant/streaming_message_view.dart'; export 'src/attachment/attachment.dart'; export 'src/attachment/builder/attachment_widget_builder.dart'; -export 'src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_list_player.dart'; -export 'src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_loading.dart'; -export 'src/attachment/builder/voice_recording_attachment_builder/stream_voice_recording_player.dart'; export 'src/attachment/gallery_attachment.dart'; export 'src/attachment/handler/stream_attachment_handler.dart'; export 'src/attachment/image_attachment.dart'; @@ -52,9 +49,14 @@ export 'src/indicators/upload_progress_indicator.dart'; export 'src/keyboard_shortcuts/keyboard_shortcut_runner.dart'; export 'src/localization/stream_chat_localizations.dart'; export 'src/localization/translations.dart' show DefaultTranslations; -export 'src/message_actions_modal/message_action.dart'; +export 'src/message_action/message_action.dart'; +export 'src/message_action/message_action_item.dart'; +export 'src/message_action/message_actions_builder.dart'; export 'src/message_input/attachment_picker/stream_attachment_picker.dart'; export 'src/message_input/attachment_picker/stream_attachment_picker_bottom_sheet.dart'; +export 'src/message_input/attachment_picker/stream_attachment_picker_controller.dart'; +export 'src/message_input/attachment_picker/stream_attachment_picker_option.dart'; +export 'src/message_input/attachment_picker/stream_attachment_picker_result.dart'; export 'src/message_input/audio_recorder/audio_recorder_controller.dart'; export 'src/message_input/audio_recorder/audio_recorder_state.dart'; export 'src/message_input/audio_recorder/stream_audio_recorder.dart'; @@ -67,15 +69,19 @@ export 'src/message_input/stream_message_send_button.dart'; export 'src/message_input/stream_message_text_field.dart'; export 'src/message_list_view/message_details.dart'; export 'src/message_list_view/message_list_view.dart'; +export 'src/message_modal/message_action_confirmation_modal.dart'; +export 'src/message_modal/message_actions_modal.dart'; +export 'src/message_modal/message_modal.dart'; +export 'src/message_modal/message_reactions_modal.dart'; +export 'src/message_modal/moderated_message_actions_modal.dart'; export 'src/message_widget/deleted_message.dart'; export 'src/message_widget/message_text.dart'; export 'src/message_widget/message_widget.dart'; export 'src/message_widget/message_widget_content_components.dart'; export 'src/message_widget/moderated_message.dart'; -export 'src/message_widget/poll_message.dart'; -export 'src/message_widget/reactions/reaction_picker.dart'; export 'src/message_widget/system_message.dart'; export 'src/message_widget/text_bubble.dart'; +export 'src/misc/adaptive_dialog_action.dart'; export 'src/misc/animated_circle_border_painter.dart'; export 'src/misc/back_button.dart'; export 'src/misc/connection_status_builder.dart'; @@ -84,6 +90,7 @@ export 'src/misc/info_tile.dart'; export 'src/misc/markdown_message.dart'; export 'src/misc/option_list_tile.dart'; export 'src/misc/reaction_icon.dart'; +export 'src/misc/stream_modal.dart'; export 'src/misc/stream_neumorphic_button.dart'; export 'src/misc/swipeable.dart'; export 'src/misc/thread_header.dart'; @@ -96,6 +103,10 @@ export 'src/poll/stream_poll_option_votes_dialog.dart'; export 'src/poll/stream_poll_options_dialog.dart'; export 'src/poll/stream_poll_results_dialog.dart'; export 'src/poll/stream_poll_text_field.dart'; +export 'src/reactions/picker/reaction_picker.dart'; +export 'src/reactions/picker/reaction_picker_icon_list.dart'; +export 'src/reactions/reaction_bubble.dart'; +export 'src/reactions/user_reactions.dart'; export 'src/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart'; export 'src/scroll_view/channel_scroll_view/stream_channel_grid_view.dart'; export 'src/scroll_view/channel_scroll_view/stream_channel_list_tile.dart'; @@ -131,5 +142,3 @@ export 'src/utils/device_segmentation.dart'; export 'src/utils/extensions.dart'; export 'src/utils/helpers.dart'; export 'src/utils/typedefs.dart'; -// TODO: Remove this in favor of StreamVideoAttachmentThumbnail. -export 'src/video/video_thumbnail_image.dart'; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 1ca5dcce84..b0ce12f458 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 9.16.0 +version: 10.0.0-beta.5 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -55,7 +55,7 @@ dependencies: rxdart: ^0.28.0 share_plus: ^11.0.0 shimmer: ^3.0.0 - stream_chat_flutter_core: ^9.16.0 + stream_chat_flutter_core: ^10.0.0-beta.5 svg_icon_widget: ^0.0.1 synchronized: ^3.1.0+1 thumblr: ^0.0.4 diff --git a/packages/stream_chat_flutter/test/platform_widget_builder/platform_widget_builder_test.dart b/packages/stream_chat_flutter/test/platform_widget_builder/platform_widget_builder_test.dart index 08425ebc9b..cfb7e774f5 100644 --- a/packages/stream_chat_flutter/test/platform_widget_builder/platform_widget_builder_test.dart +++ b/packages/stream_chat_flutter/test/platform_widget_builder/platform_widget_builder_test.dart @@ -71,4 +71,29 @@ void main() { }, variant: const TargetPlatformVariant({TargetPlatform.fuchsia}), // hacky :/ ); + + testWidgets( + 'PlatformWidgetBuilder builds the correct widget for desktopOrWeb', + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: PlatformWidgetBuilder( + desktopOrWeb: (context, child) => const Text('DesktopOrWeb'), + ), + ), + ), + ), + ); + + expect(find.text('DesktopOrWeb'), findsOneWidget); + }, + variant: const TargetPlatformVariant({ + TargetPlatform.macOS, + TargetPlatform.windows, + TargetPlatform.linux, + TargetPlatform.fuchsia, // Quick hack for web variant. + }), + ); } diff --git a/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_idle_dark.png b/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_idle_dark.png index cf951c9a8d..50e70cf490 100644 Binary files a/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_idle_dark.png and b/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_idle_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_idle_light.png b/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_idle_light.png index 247512100d..378d8e621f 100644 Binary files a/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_idle_light.png and b/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_idle_light.png differ diff --git a/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playing_dark.png b/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playing_dark.png index 7a927e33ca..c9e294aee6 100644 Binary files a/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playing_dark.png and b/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playing_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playing_light.png b/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playing_light.png index 8e26369492..dc5dd1fba0 100644 Binary files a/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playing_light.png and b/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playing_light.png differ diff --git a/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playlist_dark.png b/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playlist_dark.png index c00c4952f6..b39b7214a7 100644 Binary files a/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playlist_dark.png and b/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playlist_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playlist_light.png b/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playlist_light.png index d4e41db472..09ee72c034 100644 Binary files a/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playlist_light.png and b/packages/stream_chat_flutter/test/src/attachment/goldens/ci/stream_voice_recording_attachment_playlist_light.png differ diff --git a/packages/stream_chat_flutter/test/src/attachment/voice_recording_attachment_playlist_test.dart b/packages/stream_chat_flutter/test/src/attachment/voice_recording_attachment_playlist_test.dart index 0f6786f3bb..fc3ffd107a 100644 --- a/packages/stream_chat_flutter/test/src/attachment/voice_recording_attachment_playlist_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/voice_recording_attachment_playlist_test.dart @@ -1,7 +1,6 @@ import 'package:alchemist/alchemist.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/src/attachment/voice_recording_attachment.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import '../mocks.dart'; diff --git a/packages/stream_chat_flutter/test/src/attachment/voice_recording_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/voice_recording_attachment_test.dart index ef95e4b220..529e14f133 100644 --- a/packages/stream_chat_flutter/test/src/attachment/voice_recording_attachment_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/voice_recording_attachment_test.dart @@ -2,7 +2,6 @@ import 'package:alchemist/alchemist.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/src/attachment/voice_recording_attachment.dart'; import 'package:stream_chat_flutter/src/audio/audio_playlist_state.dart'; import 'package:stream_chat_flutter/src/misc/audio_waveform.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/edit_message_sheet_0.png b/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/edit_message_sheet_0.png index 386f6c481f..154cdd7a39 100644 Binary files a/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/edit_message_sheet_0.png and b/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/edit_message_sheet_0.png differ diff --git a/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/error_alert_sheet_0.png b/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/error_alert_sheet_0.png index eecd466710..dc792011fd 100644 Binary files a/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/error_alert_sheet_0.png and b/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/error_alert_sheet_0.png differ diff --git a/packages/stream_chat_flutter/test/src/channel/goldens/ci/channel_header_bottom_widget.png b/packages/stream_chat_flutter/test/src/channel/goldens/ci/channel_header_bottom_widget.png index 6c3aa5467e..4c1c782fa9 100644 Binary files a/packages/stream_chat_flutter/test/src/channel/goldens/ci/channel_header_bottom_widget.png and b/packages/stream_chat_flutter/test/src/channel/goldens/ci/channel_header_bottom_widget.png differ diff --git a/packages/stream_chat_flutter/test/src/context_menu_items/download_menu_item_test.dart b/packages/stream_chat_flutter/test/src/context_menu_items/download_menu_item_test.dart deleted file mode 100644 index 23b3a4c533..0000000000 --- a/packages/stream_chat_flutter/test/src/context_menu_items/download_menu_item_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:alchemist/alchemist.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/src/context_menu_items/download_menu_item.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../material_app_wrapper.dart'; -import '../mocks.dart'; - -void main() { - group('DownloadMenuItem tests', () { - testWidgets('renders ListTile widget', (tester) async { - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: MockClient(), - child: child, - ), - home: Scaffold( - body: Center( - child: DownloadMenuItem( - attachment: MockAttachment(), - ), - ), - ), - ), - ); - - expect(find.byType(ListTile), findsOneWidget); - }); - - goldenTest( - 'golden test for DownloadMenuItem', - fileName: 'download_menu_item_0', - constraints: const BoxConstraints.tightFor(width: 300, height: 100), - builder: () => MaterialAppWrapper( - builder: (context, child) => StreamChatTheme( - data: StreamChatThemeData.light(), - child: child!, - ), - home: Scaffold( - body: Center( - child: DownloadMenuItem( - attachment: MockAttachment(), - ), - ), - ), - ), - ); - }); -} diff --git a/packages/stream_chat_flutter/test/src/context_menu_items/goldens/ci/download_menu_item_0.png b/packages/stream_chat_flutter/test/src/context_menu_items/goldens/ci/download_menu_item_0.png deleted file mode 100644 index d4751f48c5..0000000000 Binary files a/packages/stream_chat_flutter/test/src/context_menu_items/goldens/ci/download_menu_item_0.png and /dev/null differ diff --git a/packages/stream_chat_flutter/test/src/context_menu_items/goldens/ci/stream_chat_context_menu_item_0.png b/packages/stream_chat_flutter/test/src/context_menu_items/goldens/ci/stream_chat_context_menu_item_0.png deleted file mode 100644 index 1cfb96377a..0000000000 Binary files a/packages/stream_chat_flutter/test/src/context_menu_items/goldens/ci/stream_chat_context_menu_item_0.png and /dev/null differ diff --git a/packages/stream_chat_flutter/test/src/context_menu_items/stream_chat_context_menu_item_test.dart b/packages/stream_chat_flutter/test/src/context_menu_items/stream_chat_context_menu_item_test.dart deleted file mode 100644 index 798e649cfb..0000000000 --- a/packages/stream_chat_flutter/test/src/context_menu_items/stream_chat_context_menu_item_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:alchemist/alchemist.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/src/context_menu_items/stream_chat_context_menu_item.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../material_app_wrapper.dart'; -import '../mocks.dart'; - -void main() { - group('StreamChatContextMenuItem tests', () { - testWidgets('renders ListTile widget', (tester) async { - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: MockClient(), - child: child, - ), - home: const Scaffold( - body: Center( - child: StreamChatContextMenuItem(), - ), - ), - ), - ); - - expect(find.byType(ListTile), findsOneWidget); - }); - - goldenTest( - 'golden test for StreamChatContextMenuItem', - fileName: 'stream_chat_context_menu_item_0', - constraints: const BoxConstraints.tightFor(width: 300, height: 80), - builder: () => MaterialAppWrapper( - builder: (context, child) => StreamChatTheme( - data: StreamChatThemeData.light(), - child: child!, - ), - home: Scaffold( - body: Center( - child: StreamChatContextMenuItem( - leading: const Icon(Icons.download), - title: const Text('Download'), - onClick: () {}, - ), - ), - ), - ), - ); - }); -} diff --git a/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/confirmation_dialog_0.png b/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/confirmation_dialog_0.png index 28fd090881..5d0d97458b 100644 Binary files a/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/confirmation_dialog_0.png and b/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/confirmation_dialog_0.png differ diff --git a/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/delete_message_dialog_0.png b/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/delete_message_dialog_0.png index 9314acdc86..b4b97e104b 100644 Binary files a/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/delete_message_dialog_0.png and b/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/delete_message_dialog_0.png differ diff --git a/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/message_dialog_0.png b/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/message_dialog_0.png index e6bf309bc3..ff5aae491b 100644 Binary files a/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/message_dialog_0.png and b/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/message_dialog_0.png differ diff --git a/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/message_dialog_1.png b/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/message_dialog_1.png index 58f5634347..74ed9d5d5d 100644 Binary files a/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/message_dialog_1.png and b/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/message_dialog_1.png differ diff --git a/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/message_dialog_2.png b/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/message_dialog_2.png index e6bf309bc3..ff5aae491b 100644 Binary files a/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/message_dialog_2.png and b/packages/stream_chat_flutter/test/src/dialogs/goldens/ci/message_dialog_2.png differ diff --git a/packages/stream_chat_flutter/test/src/icons/goldens/ci/stream_svg_icon_light.png b/packages/stream_chat_flutter/test/src/icons/goldens/ci/stream_svg_icon_light.png index 3e82952ab4..42eefb8b11 100644 Binary files a/packages/stream_chat_flutter/test/src/icons/goldens/ci/stream_svg_icon_light.png and b/packages/stream_chat_flutter/test/src/icons/goldens/ci/stream_svg_icon_light.png differ diff --git a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_1.png b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_1.png index be1ab4d6f5..06e22f1b1a 100644 Binary files a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_1.png and b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_1.png differ diff --git a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_2.png b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_2.png index 0c31a3c88b..31704acd50 100644 Binary files a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_2.png and b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_2.png differ diff --git a/packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart b/packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart index 61cf5b941a..71f91c0235 100644 --- a/packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/indicators/unread_indicator_test.dart @@ -38,7 +38,7 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: Scaffold( + child: const Scaffold( body: StreamUnreadIndicator(), ), ), diff --git a/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_custom_child_dark.png b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_custom_child_dark.png new file mode 100644 index 0000000000..23412da5e8 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_custom_child_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_custom_child_light.png b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_custom_child_light.png new file mode 100644 index 0000000000..e3ddc3bab7 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_custom_child_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_custom_styling_dark.png b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_custom_styling_dark.png new file mode 100644 index 0000000000..f212165ce5 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_custom_styling_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_custom_styling_light.png b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_custom_styling_light.png new file mode 100644 index 0000000000..2f341ee9e8 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_custom_styling_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_delete_dark.png b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_delete_dark.png new file mode 100644 index 0000000000..ceb15cacdd Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_delete_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_delete_light.png b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_delete_light.png new file mode 100644 index 0000000000..8911ab5aa2 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_delete_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_reply_dark.png b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_reply_dark.png new file mode 100644 index 0000000000..eb2407025e Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_reply_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_reply_light.png b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_reply_light.png new file mode 100644 index 0000000000..ec6c889e52 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_action/goldens/ci/stream_message_action_item_reply_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_action/message_action_item_test.dart b/packages/stream_chat_flutter/test/src/message_action/message_action_item_test.dart new file mode 100644 index 0000000000..1edf748ec1 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_action/message_action_item_test.dart @@ -0,0 +1,167 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + final message = Message( + id: 'test-message', + text: 'Hello, world!', + user: User(id: 'test-user'), + ); + + testWidgets('renders with title and icon', (tester) async { + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageActionItem( + action: StreamMessageAction( + title: const Text('Reply'), + leading: const Icon(Icons.reply), + action: QuotedReply(message: message), + ), + ), + ), + ); + + expect(find.text('Reply'), findsOneWidget); + expect(find.byIcon(Icons.reply), findsOneWidget); + }); + + testWidgets('calls onTap when tapped', (tester) async { + Message? tappedMessage; + + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageActionItem( + onTap: (action) => tappedMessage = action.message, + action: StreamMessageAction( + title: const Text('Reply'), + leading: const Icon(Icons.reply), + action: QuotedReply(message: message), + ), + ), + ), + ); + + await tester.tap(find.byType(InkWell)); + await tester.pump(); + + expect(tappedMessage, message); + }); + + testWidgets( + 'applies destructive styling when isDestructive is true', + (tester) async { + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageActionItem( + action: StreamMessageAction( + isDestructive: true, + title: const Text('Delete'), + leading: const Icon(Icons.delete), + action: DeleteMessage(message: message), + ), + ), + ), + ); + + expect(find.text('Delete'), findsOneWidget); + expect(find.byIcon(Icons.delete), findsOneWidget); + + // The icon and text should have the error color + final theme = StreamChatTheme.of(tester.element(find.text('Delete'))); + final iconTheme = IconTheme.of(tester.element(find.byIcon(Icons.delete))); + + expect(iconTheme.color, theme.colorTheme.accentError); + }, + ); + + group('StreamMessageActionItem Golden Tests', () { + for (final brightness in Brightness.values) { + final theme = brightness.name; + + // Test standard action + goldenTest( + 'StreamMessageActionItem (Reply) in $theme theme', + fileName: 'stream_message_action_item_reply_$theme', + constraints: const BoxConstraints(maxWidth: 300, maxHeight: 60), + builder: () => _wrapWithMaterialApp( + brightness: brightness, + StreamMessageActionItem( + action: StreamMessageAction( + title: const Text('Reply'), + leading: const Icon(Icons.reply), + action: QuotedReply(message: message), + ), + ), + ), + ); + + // Test destructive action (like delete) + goldenTest( + 'StreamMessageActionItem (Delete) in $theme theme', + fileName: 'stream_message_action_item_delete_$theme', + constraints: const BoxConstraints(maxWidth: 300, maxHeight: 60), + builder: () => _wrapWithMaterialApp( + brightness: brightness, + StreamMessageActionItem( + action: StreamMessageAction( + isDestructive: true, + title: const Text('Delete Message'), + leading: const Icon(Icons.delete), + action: DeleteMessage(message: message), + ), + ), + ), + ); + + // Test with custom styling + goldenTest( + 'StreamMessageActionItem with custom styling in $theme theme', + fileName: 'stream_message_action_item_custom_styling_$theme', + constraints: const BoxConstraints(maxWidth: 300, maxHeight: 60), + builder: () => _wrapWithMaterialApp( + brightness: brightness, + StreamMessageActionItem( + action: StreamMessageAction( + title: const Text('Styled Action'), + titleTextStyle: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.purple[700], + ), + leading: const Icon(Icons.favorite), + iconColor: Colors.pink[400], + backgroundColor: Colors.amber[100], + action: CustomMessageAction(message: message), + ), + ), + ), + ); + } + }); +} + +Widget _wrapWithMaterialApp( + Widget child, { + Brightness? brightness, +}) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: StreamChatTheme( + data: StreamChatThemeData(brightness: brightness), + child: Builder(builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: child, + ), + ), + ); + }), + ), + ); +} diff --git a/packages/stream_chat_flutter/test/src/message_action/message_actions_builder_test.dart b/packages/stream_chat_flutter/test/src/message_action/message_actions_builder_test.dart new file mode 100644 index 0000000000..e6960c0712 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_action/message_actions_builder_test.dart @@ -0,0 +1,570 @@ +// ignore_for_file: cascade_invocations, avoid_redundant_argument_values + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +/// Creates a test message with customizable properties. +Message createTestMessage({ + String id = 'test-message', + String text = 'Test message', + String userId = 'test-user', + bool pinned = false, + String? parentId, + Poll? poll, + MessageType type = MessageType.regular, + int? replyCount, +}) { + final message = Message( + id: id, + text: text, + user: User(id: userId), + pinned: pinned, + parentId: parentId, + poll: poll, + type: type, + deletedAt: type == MessageType.deleted ? DateTime.now() : null, + replyCount: replyCount, + moderation: switch (type) { + MessageType.error => const Moderation( + action: ModerationAction.bounce, + originalText: 'Original message text that violated policy', + ), + _ => null, + }, + ); + + var state = MessageState.sent; + if (message.deletedAt != null) { + state = MessageState.softDeleted; + } else if (message.updatedAt.isAfter(message.createdAt)) { + state = MessageState.updated; + } + + return message.copyWith(state: state); +} + +const allChannelCapabilities = [ + ChannelCapability.sendMessage, + ChannelCapability.sendReply, + ChannelCapability.pinMessage, + ChannelCapability.readEvents, + ChannelCapability.deleteOwnMessage, + ChannelCapability.deleteAnyMessage, + ChannelCapability.updateOwnMessage, + ChannelCapability.updateAnyMessage, + ChannelCapability.quoteMessage, +]; + +void main() { + final message = createTestMessage(); + final currentUser = OwnUser(id: 'current-user'); + + setUpAll(() { + registerFallbackValue(Message()); + // registerFallbackValue(const StreamMessageActionType('any')); + }); + + MockChannel _getChannelWithCapabilities( + List capabilities, { + bool enableMutes = true, + }) { + final customChannel = MockChannel(ownCapabilities: capabilities); + final channelConfig = ChannelConfig(mutes: enableMutes); + when(() => customChannel.config).thenReturn(channelConfig); + return customChannel; + } + + Future _getContext(WidgetTester tester) async { + late BuildContext context; + await tester.pumpWidget( + StreamChatTheme( + data: StreamChatThemeData.light(), + child: Builder(builder: (ctx) { + context = ctx; + return const SizedBox.shrink(); + }), + ), + ); + return context; + } + + testWidgets('builds default message actions', (tester) async { + final context = await _getContext(tester); + + final channel = _getChannelWithCapabilities(allChannelCapabilities); + final actions = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channel, + currentUser: currentUser, + ); + + // Verify default actions + actions.expects(); + actions.expects(); + actions.expects(); + actions.expects(); + actions.expects(); + actions.expects(); + actions.expects(); + actions.expects(); + actions.expects(); + }); + + testWidgets('returns empty set for deleted messages', (tester) async { + final context = await _getContext(tester); + final deletedMessage = createTestMessage(type: MessageType.deleted); + + final channel = _getChannelWithCapabilities(allChannelCapabilities); + final actions = StreamMessageActionsBuilder.buildActions( + context: context, + message: deletedMessage, + channel: channel, + currentUser: currentUser, + ); + + expect(actions.isEmpty, isTrue); + }); + + testWidgets('includes custom actions', (tester) async { + final context = await _getContext(tester); + final customAction = StreamMessageAction( + title: const Text('Custom'), + leading: const Icon(Icons.star), + action: CustomMessageAction( + message: message, + extraData: {'customKey': 'customValue'}, + ), + ); + + final channel = _getChannelWithCapabilities(allChannelCapabilities); + final actions = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channel, + currentUser: currentUser, + customActions: [customAction], + ); + + actions.expects( + reason: 'Custom action should be included', + ); + }); + + group('permission-based actions', () { + testWidgets( + 'includes/excludes edit action based on authorship', + (tester) async { + final context = await _getContext(tester); + + // Own message test + final channel = _getChannelWithCapabilities(allChannelCapabilities); + final ownMessage = createTestMessage(userId: currentUser.id); + final ownActions = StreamMessageActionsBuilder.buildActions( + context: context, + message: ownMessage, + channel: channel, + currentUser: currentUser, + ); + + ownActions.expects( + reason: 'Edit action should be available for own messages', + ); + + // Other user's message test + final otherUserActions = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channel, + currentUser: currentUser, + ); + + otherUserActions.expects( + reason: 'Edit action should be available for others messages', + ); + }, + ); + + testWidgets('excludes edit action for messages with polls', (tester) async { + final context = await _getContext(tester); + + final pollMessage = createTestMessage( + userId: currentUser.id, + poll: Poll( + id: 'poll-id', + name: 'What is your favorite color?', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + options: const [ + PollOption(text: 'Option 1'), + PollOption(text: 'Option 2'), + ], + ), + ); + + final channel = _getChannelWithCapabilities(allChannelCapabilities); + final actions = StreamMessageActionsBuilder.buildActions( + context: context, + message: pollMessage, + channel: channel, + currentUser: currentUser, + ); + + actions.notExpects( + reason: 'Edit action should not be available for poll messages', + ); + }); + + testWidgets( + 'includes/excludes delete action based on permission', + (tester) async { + final context = await _getContext(tester); + + // With delete permission + final channel = _getChannelWithCapabilities([ + ChannelCapability.sendMessage, + ChannelCapability.updateOwnMessage, + ChannelCapability.deleteOwnMessage, + ]); + + final ownMessage = createTestMessage(userId: currentUser.id); + final actionsWithPerm = StreamMessageActionsBuilder.buildActions( + context: context, + message: ownMessage, + channel: channel, + currentUser: currentUser, + ); + + actionsWithPerm.expects( + reason: 'Delete action should be available with permission', + ); + + // Without delete permission + final channelWithoutDeletePerm = _getChannelWithCapabilities([ + ChannelCapability.sendMessage, + ChannelCapability.updateOwnMessage, + ]); + + final actionsWithoutPerm = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channelWithoutDeletePerm, + currentUser: currentUser, + ); + + actionsWithoutPerm.notExpects( + reason: 'Delete action should not be available without permission', + ); + }, + ); + + testWidgets( + 'includes/excludes pin action based on permission', + (tester) async { + final context = await _getContext(tester); + + // With pin permission + final channel = _getChannelWithCapabilities([ + ChannelCapability.sendMessage, + ChannelCapability.sendReply, + ChannelCapability.pinMessage, + ]); + + final actionsWithPerm = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channel, + currentUser: currentUser, + ); + + actionsWithPerm.expects( + reason: 'Pin action should be available with pin permission', + ); + + // Without pin permission + final channelWithoutPinPerm = _getChannelWithCapabilities([ + ChannelCapability.sendMessage, + ChannelCapability.sendReply, + ]); + + final actionsWithoutPerm = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channelWithoutPinPerm, + currentUser: currentUser, + ); + + actionsWithoutPerm.notExpects( + reason: 'Pin action should not be available without permission', + ); + }, + ); + + testWidgets('shows unpin action for pinned messages', (tester) async { + final context = await _getContext(tester); + + final channel = _getChannelWithCapabilities(allChannelCapabilities); + final pinnedMessage = createTestMessage(pinned: true); + final actions = StreamMessageActionsBuilder.buildActions( + context: context, + message: pinnedMessage, + channel: channel, + currentUser: currentUser, + ); + + actions.expects( + reason: 'Unpin action should be available for pinned messages', + ); + }); + + testWidgets( + 'includes/excludes flag action based on authorship', + (tester) async { + final context = await _getContext(tester); + + // Other user's message + final channel = _getChannelWithCapabilities(allChannelCapabilities); + final actionsOtherUser = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channel, + currentUser: currentUser, + ); + + actionsOtherUser.expects( + reason: "Flag action should be available for others' messages", + ); + + // Own message + final ownMessage = createTestMessage(userId: currentUser.id); + final actionsOwnMessage = StreamMessageActionsBuilder.buildActions( + context: context, + message: ownMessage, + channel: channel, + currentUser: currentUser, + ); + + actionsOwnMessage.notExpects( + reason: 'Flag action should not be available for own messages', + ); + }, + ); + + testWidgets( + 'handles mute action correctly based on user and config', + (tester) async { + final context = await _getContext(tester); + + // User with no mutes + final channel = _getChannelWithCapabilities(allChannelCapabilities); + final userWithNoMutes = OwnUser(id: 'current-user', mutes: const []); + final actionsForNoMutes = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channel, + currentUser: userWithNoMutes, + ); + + actionsForNoMutes.expects( + reason: 'Mute action should be available for users with no mutes', + ); + + // User with mutes + final userWithMutes = OwnUser( + id: 'current-user', + mutes: [ + Mute( + user: User(id: 'test-user'), + target: User(id: 'test-user'), + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ), + ], + ); + + final actionsForMutedUser = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channel, + currentUser: userWithMutes, + ); + + actionsForMutedUser.expects( + reason: 'Unmute action should be available for already muted users', + ); + + // Own message + final ownMessage = createTestMessage(userId: currentUser.id); + final actionsForOwnMessage = StreamMessageActionsBuilder.buildActions( + context: context, + message: ownMessage, + channel: channel, + currentUser: currentUser, + ); + + actionsForOwnMessage.notExpects( + reason: 'Mute action should not be available for own messages', + ); + + // Channel without mutes enabled + final channelWithoutMutes = _getChannelWithCapabilities( + allChannelCapabilities, + enableMutes: false, + ); + + final muteDisabledActions = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channelWithoutMutes, + currentUser: currentUser, + ); + + muteDisabledActions.notExpects( + reason: 'Mute action unavailable when channel mutes are disabled', + ); + }, + ); + + testWidgets( + 'handles thread and quote reply actions correctly', + (tester) async { + final context = await _getContext(tester); + + // Thread message + final channel = _getChannelWithCapabilities(allChannelCapabilities); + final threadMessage = createTestMessage(parentId: 'parent-message-id'); + final actionsForThreadMessage = + StreamMessageActionsBuilder.buildActions( + context: context, + message: threadMessage, + channel: channel, + currentUser: currentUser, + ); + + actionsForThreadMessage.notExpects( + reason: 'Thread reply unavailable for thread messages', + ); + + // Channel without quote permission + final channelWithoutQuote = _getChannelWithCapabilities([ + ChannelCapability.sendMessage, + ChannelCapability.sendReply, + ]); + + final actionsWithoutQuote = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channelWithoutQuote, + currentUser: currentUser, + ); + + actionsWithoutQuote.notExpects( + reason: 'Quote reply unavailable without quote permission', + ); + }, + ); + + testWidgets('handles mark unread action correctly', (tester) async { + final context = await _getContext(tester); + + // With read events capability + final parentMessage = createTestMessage( + id: 'parent-message', + text: 'Parent message', + replyCount: 5, + ); + + final channelWithReadEvents = _getChannelWithCapabilities([ + ChannelCapability.sendMessage, + ChannelCapability.sendReply, + ChannelCapability.readEvents, + ]); + + final actionsWithReadEvents = StreamMessageActionsBuilder.buildActions( + context: context, + message: parentMessage, + channel: channelWithReadEvents, + currentUser: currentUser, + ); + + actionsWithReadEvents.expects( + reason: 'Mark unread available with read events capability', + ); + + // Without read events capability + final channelWithoutReadEvents = _getChannelWithCapabilities([ + ChannelCapability.sendMessage, + ChannelCapability.sendReply, + ]); + + final actionsWithoutReadEvents = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channelWithoutReadEvents, + currentUser: currentUser, + ); + + actionsWithoutReadEvents.notExpects( + reason: 'Mark unread unavailable without read events capability', + ); + }); + }); + + group('buildBouncedErrorActions', () { + testWidgets('returns empty set for non-bounced messages', (tester) async { + final context = await _getContext(tester); + final regularMessage = createTestMessage(); + + final actions = StreamMessageActionsBuilder.buildBouncedErrorActions( + context: context, + message: regularMessage, + ); + + expect(actions.isEmpty, isTrue, reason: 'No actions for regular message'); + }); + + testWidgets( + 'builds actions for bounced messages with error', + (tester) async { + final context = await _getContext(tester); + final bouncedMessage = createTestMessage(type: MessageType.error); + + final actions = StreamMessageActionsBuilder.buildBouncedErrorActions( + context: context, + message: bouncedMessage, + ); + + // Verify the specific actions for bounced messages + actions.expects( + reason: 'Send Anyway action should be included', + ); + actions.expects( + reason: 'Edit Message action should be included', + ); + actions.expects( + reason: 'Delete Message action should be included', + ); + + // Verify the count is correct + expect(actions.length, 3, reason: 'Should have exactly 3 actions'); + }, + ); + }); +} + +/// Extension on Set to simplify action type checks. +extension StreamMessageActionSetExtension on List { + void expects({String? reason}) { + final containsActionType = this.any((it) => it.action is T); + return expect(containsActionType, isTrue, reason: reason); + } + + void notExpects({String? reason}) { + final containsActionType = this.any((it) => it.action is T); + return expect(containsActionType, isFalse, reason: reason); + } +} diff --git a/packages/stream_chat_flutter/test/src/message_actions_modal/message_actions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_actions_modal/message_actions_modal_test.dart deleted file mode 100644 index 194652b336..0000000000 --- a/packages/stream_chat_flutter/test/src/message_actions_modal/message_actions_modal_test.dart +++ /dev/null @@ -1,916 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/src/message_actions_modal/message_actions_modal.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../mocks.dart'; - -void main() { - setUpAll(() { - registerFallbackValue( - MaterialPageRoute(builder: (context) => const SizedBox())); - registerFallbackValue(Message()); - }); - - testWidgets( - 'it should show the all actions', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - streamChatThemeData: streamTheme, - client: client, - child: SizedBox( - child: StreamChannel( - channel: channel, - child: MessageActionsModal( - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - state: MessageState.sent, - ), - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - expect(find.byKey(const Key('MessageWidget')), findsOneWidget); - expect(find.text('Thread Reply'), findsOneWidget); - expect(find.text('Reply'), findsOneWidget); - expect(find.text('Edit Message'), findsOneWidget); - expect(find.text('Delete Message'), findsOneWidget); - expect(find.text('Copy Message'), findsOneWidget); - expect(find.text('Mark as Unread'), findsOneWidget); - }, - ); - - testWidgets( - 'it should show the reaction picker', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel( - ownCapabilities: [ - ChannelCapability.sendMessage, - ChannelCapability.sendReaction, - ], - ); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - streamChatThemeData: streamTheme, - client: client, - child: SizedBox( - child: StreamChannel( - channel: channel, - child: MessageActionsModal( - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - state: MessageState.sent, - ), - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - expect(find.byType(StreamReactionPicker), findsOneWidget); - }, - ); - - testWidgets( - 'it should not show the reaction picker', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - streamChatThemeData: streamTheme, - client: client, - child: SizedBox( - child: StreamChannel( - channel: channel, - child: MessageActionsModal( - showReactionPicker: false, - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - state: MessageState.sent, - ), - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - expect(find.byType(StreamReactionPicker), findsNothing); - }, - ); - - testWidgets( - 'it should show some actions', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - streamChatThemeData: streamTheme, - client: client, - child: SizedBox( - child: StreamChannel( - channel: channel, - child: MessageActionsModal( - showCopyMessage: false, - showReplyMessage: false, - showThreadReplyMessage: false, - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - ), - messageTheme: streamTheme.ownMessageTheme, - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), - ), - ), - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - expect(find.byKey(const Key('MessageWidget')), findsOneWidget); - expect(find.text('Reply'), findsNothing); - expect(find.text('Thread reply'), findsNothing); - expect(find.text('Edit message'), findsNothing); - expect(find.text('Delete message'), findsNothing); - expect(find.text('Copy message'), findsNothing); - }, - ); - - testWidgets( - 'it should show custom actions', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - var tapped = false; - await tester.pumpWidget(MaterialApp( - theme: themeData, - home: StreamChat( - streamChatThemeData: streamTheme, - client: client, - child: SizedBox( - child: StreamChannel( - channel: channel, - child: StreamChannel( - channel: channel, - child: MessageActionsModal( - messageWidget: const Text('test'), - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - ), - messageTheme: streamTheme.ownMessageTheme, - customActions: [ - StreamMessageAction( - leading: const Icon(Icons.check), - title: const Text('title'), - onTap: (m) { - tapped = true; - }, - ), - ], - ), - ), - ), - ), - ), - )); - - await tester.pumpAndSettle(); - - expect(find.byIcon(Icons.check), findsOneWidget); - expect(find.text('title'), findsOneWidget); - - await tester.tap(find.text('title')); - - expect(tapped, true); - }, - ); - - testWidgets( - 'tapping on reply should call the callback', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - var tapped = false; - - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - streamChatThemeData: streamTheme, - client: client, - child: SizedBox( - child: StreamChannel( - channel: channel, - child: MessageActionsModal( - messageWidget: const Text('test'), - onReplyTap: (m) { - tapped = true; - }, - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - state: MessageState.sent, - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Reply')); - - expect(tapped, true); - }, - ); - - testWidgets( - 'tapping on thread reply should call the callback', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - var tapped = false; - - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - streamChatThemeData: streamTheme, - client: client, - child: SizedBox( - child: StreamChannel( - channel: channel, - child: MessageActionsModal( - messageWidget: const Text('test'), - onThreadReplyTap: (m) { - tapped = true; - }, - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - state: MessageState.sent, - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Thread Reply')); - - expect(tapped, true); - }, - ); - - testWidgets( - 'tapping on edit should show the edit bottom sheet', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - final channelState = MockChannelState(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => channel.state).thenReturn(channelState); - when(channel.getRemainingCooldown).thenReturn(0); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: child, - ), - theme: themeData, - home: Builder( - builder: (context) => StreamChannel( - showLoading: false, - channel: channel, - child: MessageActionsModal( - messageWidget: const Text('test'), - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - ), - messageTheme: streamTheme.ownMessageTheme, - onEditMessageTap: (message) => showEditMessageSheet( - context: context, - message: message, - channel: channel, - ), - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Edit Message')); - - await tester.pumpAndSettle(); - - expect(find.byType(StreamMessageInput), findsOneWidget); - }, - ); - - testWidgets( - 'tapping on edit should show use the custom builder', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: child, - ), - theme: themeData, - home: StreamChannel( - showLoading: false, - channel: channel, - child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - editMessageInputBuilder: (context, m) => const Text('test'), - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Edit Message')); - - await tester.pumpAndSettle(); - - expect(find.text('test'), findsOneWidget); - }, - ); - - testWidgets( - 'tapping on copy should use the callback', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - var tapped = false; - - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: child, - ), - theme: themeData, - home: StreamChannel( - showLoading: false, - channel: channel, - child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - onCopyTap: (m) => tapped = true, - message: Message( - text: 'test', - user: User( - id: 'user-id', - ), - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Copy Message')); - - expect(tapped, true); - }, - ); - - testWidgets( - 'tapping on resend should call retry message', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - final message = Message( - state: MessageState.sendingFailed, - text: 'test', - user: User( - id: 'user-id', - ), - ); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => channel.retryMessage(message)) - .thenAnswer((_) async => SendMessageResponse()); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: child, - ), - theme: themeData, - home: StreamChannel( - showLoading: false, - channel: channel, - child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - message: message, - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Resend')); - - verify(() => channel.retryMessage(message)).called(1); - }, - ); - - testWidgets( - 'tapping on flag message should show the dialog', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: child, - ), - theme: themeData, - home: StreamChannel( - showLoading: false, - channel: channel, - child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - message: Message( - id: 'testid', - text: 'test', - user: User( - id: 'user-id', - ), - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Flag Message')); - await tester.pumpAndSettle(); - - expect(find.text('Flag Message'), findsNWidgets(2)); - - await tester.tap(find.text('FLAG')); - await tester.pumpAndSettle(); - - verify(() => client.flagMessage('testid')).called(1); - }, - ); - - testWidgets( - 'if flagging a message throws an error the error dialog should appear', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => client.flagMessage(any())) - .thenThrow(StreamChatNetworkError(ChatErrorCode.internalSystemError)); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: child, - ), - theme: themeData, - home: StreamChannel( - showLoading: false, - channel: channel, - child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - message: Message( - id: 'testid', - text: 'test', - user: User( - id: 'user-id', - ), - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Flag Message')); - await tester.pumpAndSettle(); - - expect(find.text('Flag Message'), findsNWidgets(2)); - - await tester.tap(find.text('FLAG')); - await tester.pumpAndSettle(); - - expect(find.text('Something went wrong'), findsOneWidget); - }, - ); - - testWidgets( - 'if flagging an already flagged message no error should appear', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => client.flagMessage(any())) - .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: child, - ), - theme: themeData, - home: StreamChannel( - showLoading: false, - channel: channel, - child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - message: Message( - id: 'testid', - text: 'test', - user: User( - id: 'user-id', - ), - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Flag Message')); - await tester.pumpAndSettle(); - - expect(find.text('Flag Message'), findsNWidgets(2)); - - await tester.tap(find.text('FLAG')); - await tester.pumpAndSettle(); - - expect(find.text('Message flagged'), findsOneWidget); - }, - ); - - testWidgets( - 'tapping on delete message should call client.delete', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: child, - ), - theme: themeData, - home: StreamChannel( - showLoading: false, - channel: channel, - child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - message: Message( - id: 'testid', - text: 'test', - user: User( - id: 'user-id', - ), - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Delete Message')); - await tester.pumpAndSettle(); - - expect(find.text('Delete Message'), findsOneWidget); - - await tester.tap(find.text('DELETE')); - await tester.pumpAndSettle(); - - verify(() => channel.deleteMessage(any())).called(1); - }, - ); - - testWidgets( - 'tapping on delete message should call client.delete', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => channel.deleteMessage(any())) - .thenThrow(StreamChatNetworkError(ChatErrorCode.internalSystemError)); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: child, - ), - theme: themeData, - home: StreamChannel( - showLoading: false, - channel: channel, - child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - message: Message( - id: 'testid', - text: 'test', - user: User( - id: 'user-id', - ), - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Delete Message')); - await tester.pumpAndSettle(); - - expect(find.text('Delete Message'), findsOneWidget); - - await tester.tap(find.text('DELETE')); - await tester.pumpAndSettle(); - - expect(find.text('Something went wrong'), findsOneWidget); - }, - ); - - testWidgets( - 'tapping on unread message should call client.unread', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - await tester.pumpWidget( - MaterialApp( - builder: (context, child) => StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: child, - ), - theme: themeData, - home: Scaffold( - body: StreamChannel( - showLoading: false, - channel: channel, - child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - message: Message( - id: 'testid', - text: 'test', - user: User( - id: 'user-id', - ), - ), - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text('Mark as Unread')); - await tester.pumpAndSettle(); - - verify(() => channel.markUnread(any())).called(1); - }, - ); -} diff --git a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_idle_dark.png b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_idle_dark.png index a928f3e658..7f8b19262c 100644 Binary files a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_idle_dark.png and b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_idle_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_idle_light.png b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_idle_light.png index 68f02ee6f6..619207c4bf 100644 Binary files a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_idle_light.png and b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_idle_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_hold_dark.png b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_hold_dark.png index 5c7a7afaa2..f93c0444b6 100644 Binary files a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_hold_dark.png and b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_hold_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_hold_light.png b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_hold_light.png index 5dcce88080..428591aea6 100644 Binary files a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_hold_light.png and b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_hold_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_locked_dark.png b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_locked_dark.png index ff99d92898..b60dc5659f 100644 Binary files a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_locked_dark.png and b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_locked_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_locked_light.png b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_locked_light.png index 6de3d7b6dd..aa81d0a052 100644 Binary files a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_locked_light.png and b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_locked_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_stopped_dark.png b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_stopped_dark.png index 6710cf41f4..e3c2592aaa 100644 Binary files a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_stopped_dark.png and b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_stopped_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_stopped_light.png b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_stopped_light.png index 40a2c8be8f..1050773f6e 100644 Binary files a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_stopped_light.png and b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/goldens/ci/stream_audio_recorder_button_recording_stopped_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_input/dm_checkbox_test.dart b/packages/stream_chat_flutter/test/src/message_input/dm_checkbox_test.dart deleted file mode 100644 index 36e67a15e6..0000000000 --- a/packages/stream_chat_flutter/test/src/message_input/dm_checkbox_test.dart +++ /dev/null @@ -1,134 +0,0 @@ -import 'package:alchemist/alchemist.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/src/message_input/dm_checkbox.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../material_app_wrapper.dart'; - -@Deprecated('') -void main() { - testWidgets('DmCheckbox onTap works', (tester) async { - var count = 0; - await tester.pumpWidget( - MaterialApp( - home: StreamChatTheme( - data: StreamChatThemeData.light(), - child: Scaffold( - body: Center( - child: DmCheckbox( - foregroundDecoration: BoxDecoration( - border: Border.all( - color: StreamChatThemeData.light() - .colorTheme - .textHighEmphasis - // ignore: deprecated_member_use - .withOpacity(0.5), - width: 2, - ), - borderRadius: BorderRadius.circular(3), - ), - color: StreamChatThemeData.light().colorTheme.accentPrimary, - onTap: () { - count++; - }, - crossFadeState: CrossFadeState.showFirst, - ), - ), - ), - ), - ), - ); - - expect(find.byType(AnimatedCrossFade), findsOneWidget); - final checkbox = find.byType(InkWell); - await tester.tap(checkbox); - await tester.pumpAndSettle(); - expect(count, 1); - }); - - goldenTest( - 'golden test for checked DmCheckbox with border', - fileName: 'dm_checkbox_0', - constraints: const BoxConstraints.tightFor(width: 200, height: 50), - builder: () => MaterialAppWrapper( - home: StreamChatTheme( - data: StreamChatThemeData.light(), - child: Scaffold( - body: Center( - child: DmCheckbox( - foregroundDecoration: BoxDecoration( - border: Border.all( - color: StreamChatThemeData.light() - .colorTheme - .textHighEmphasis - // ignore: deprecated_member_use - .withOpacity(0.5), - width: 2, - ), - borderRadius: BorderRadius.circular(3), - ), - color: StreamChatThemeData.light().colorTheme.accentPrimary, - onTap: () {}, - crossFadeState: CrossFadeState.showFirst, - ), - ), - ), - ), - ), - ); - - goldenTest( - 'golden test for checked DmCheckbox without border', - fileName: 'dm_checkbox_1', - constraints: const BoxConstraints.tightFor(width: 200, height: 50), - builder: () => MaterialAppWrapper( - home: StreamChatTheme( - data: StreamChatThemeData.light(), - child: Scaffold( - body: Center( - child: DmCheckbox( - foregroundDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(3), - ), - color: StreamChatThemeData.light().colorTheme.accentPrimary, - onTap: () {}, - crossFadeState: CrossFadeState.showFirst, - ), - ), - ), - ), - ), - ); - - goldenTest( - 'golden test for unchecked DmCheckbox with border', - fileName: 'dm_checkbox_2', - constraints: const BoxConstraints.tightFor(width: 200, height: 50), - builder: () => MaterialAppWrapper( - home: StreamChatTheme( - data: StreamChatThemeData.light(), - child: Scaffold( - body: Center( - child: DmCheckbox( - foregroundDecoration: BoxDecoration( - border: Border.all( - color: StreamChatThemeData.light() - .colorTheme - .textHighEmphasis - // ignore: deprecated_member_use - .withOpacity(0.5), - width: 2, - ), - borderRadius: BorderRadius.circular(3), - ), - color: StreamChatThemeData.light().colorTheme.barsBg, - onTap: () {}, - crossFadeState: CrossFadeState.showSecond, - ), - ), - ), - ), - ), - ); -} diff --git a/packages/stream_chat_flutter/test/src/message_input/goldens/ci/attachment_button_0.png b/packages/stream_chat_flutter/test/src/message_input/goldens/ci/attachment_button_0.png index cac3a620ac..b7f33c7539 100644 Binary files a/packages/stream_chat_flutter/test/src/message_input/goldens/ci/attachment_button_0.png and b/packages/stream_chat_flutter/test/src/message_input/goldens/ci/attachment_button_0.png differ diff --git a/packages/stream_chat_flutter/test/src/message_input/goldens/ci/countdown_button_0.png b/packages/stream_chat_flutter/test/src/message_input/goldens/ci/countdown_button_0.png index 519ff3f2c0..ff13bb3fff 100644 Binary files a/packages/stream_chat_flutter/test/src/message_input/goldens/ci/countdown_button_0.png and b/packages/stream_chat_flutter/test/src/message_input/goldens/ci/countdown_button_0.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/moderated_message_actions_modal_dark.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/moderated_message_actions_modal_dark.png new file mode 100644 index 0000000000..0c67a41ec5 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/moderated_message_actions_modal_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/moderated_message_actions_modal_light.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/moderated_message_actions_modal_light.png new file mode 100644 index 0000000000..97ecba385d Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/moderated_message_actions_modal_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_dark.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_dark.png new file mode 100644 index 0000000000..42832c2e75 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_light.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_light.png new file mode 100644 index 0000000000..bd4c34d70c Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_dark.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_dark.png new file mode 100644 index 0000000000..c9e02502f3 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_light.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_light.png new file mode 100644 index 0000000000..253c820e86 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_dark.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_dark.png new file mode 100644 index 0000000000..73e3760669 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_light.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_light.png new file mode 100644 index 0000000000..1a5047f04b Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_reversed_with_reactions_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_dark.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_dark.png new file mode 100644 index 0000000000..77030a12a0 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_light.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_light.png new file mode 100644 index 0000000000..e33c359089 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_actions_modal_with_reactions_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_reactions_modal_dark.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_reactions_modal_dark.png new file mode 100644 index 0000000000..2c0cd63385 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_reactions_modal_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_reactions_modal_light.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_reactions_modal_light.png new file mode 100644 index 0000000000..738acac9a4 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_reactions_modal_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_reactions_modal_reversed_dark.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_reactions_modal_reversed_dark.png new file mode 100644 index 0000000000..56eca318c2 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_reactions_modal_reversed_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_reactions_modal_reversed_light.png b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_reactions_modal_reversed_light.png new file mode 100644 index 0000000000..ff2a215e75 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/message_modal/goldens/ci/stream_message_reactions_modal_reversed_light.png differ diff --git a/packages/stream_chat_flutter/test/src/message_modal/message_actions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_modal/message_actions_modal_test.dart new file mode 100644 index 0000000000..c035a9cb4a --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_modal/message_actions_modal_test.dart @@ -0,0 +1,258 @@ +// ignore_for_file: lines_longer_than_80_chars + +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + final message = Message( + id: 'test-message', + text: 'This is a test message', + createdAt: DateTime.now(), + user: User(id: 'test-user', name: 'Test User'), + ); + + final messageActions = [ + StreamMessageAction( + title: const Text('Reply'), + leading: const StreamSvgIcon(icon: StreamSvgIcons.reply), + action: QuotedReply(message: message), + ), + StreamMessageAction( + title: const Text('Thread Reply'), + leading: const StreamSvgIcon(icon: StreamSvgIcons.threadReply), + action: ThreadReply(message: message), + ), + StreamMessageAction( + title: const Text('Copy Message'), + leading: const StreamSvgIcon(icon: StreamSvgIcons.copy), + action: CopyMessage(message: message), + ), + StreamMessageAction( + isDestructive: true, + title: const Text('Delete Message'), + leading: const StreamSvgIcon(icon: StreamSvgIcons.delete), + action: DeleteMessage(message: message), + ), + ]; + + group('StreamMessageActionsModal', () { + testWidgets('renders message widget and actions correctly', (tester) async { + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageActionsModal( + message: message, + messageActions: messageActions, + messageWidget: const Text('Message Widget'), + ), + ), + ); + + // Use a longer timeout to ensure everything is rendered + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(find.text('Message Widget'), findsOneWidget); + expect(find.text('Reply'), findsOneWidget); + expect(find.text('Thread Reply'), findsOneWidget); + expect(find.text('Copy Message'), findsOneWidget); + expect(find.text('Delete Message'), findsOneWidget); + }); + + testWidgets('renders with reaction picker when enabled', (tester) async { + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageActionsModal( + message: message, + messageActions: messageActions, + messageWidget: const Text('Message Widget'), + showReactionPicker: true, + ), + ), + ); + + // Use a longer timeout to ensure everything is rendered + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(find.byType(StreamReactionPicker), findsOneWidget); + }); + + testWidgets( + 'calls onActionTap with SelectReaction when reaction is selected', + (tester) async { + MessageAction? messageAction; + + // Define custom reaction icons for testing + final testReactionIcons = [ + StreamReactionIcon( + type: 'like', + builder: (context, isActive, size) => const Icon(Icons.thumb_up), + ), + StreamReactionIcon( + type: 'love', + builder: (context, isActive, size) => const Icon(Icons.favorite), + ), + ]; + + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageActionsModal( + message: message, + messageActions: messageActions, + messageWidget: const Text('Message Widget'), + showReactionPicker: true, + onActionTap: (action) => messageAction = action, + ), + reactionIcons: testReactionIcons, + ), + ); + + // Use a longer timeout to ensure everything is rendered + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Verify reaction picker is shown + expect(find.byType(StreamReactionPicker), findsOneWidget); + + // Find and tap the first reaction (like) + final reactionIconFinder = find.byIcon(Icons.thumb_up); + expect(reactionIconFinder, findsOneWidget); + await tester.tap(reactionIconFinder); + await tester.pumpAndSettle(); + + expect(messageAction, isA()); + // Verify callback was called with correct reaction type + expect((messageAction! as SelectReaction).reaction.type, 'like'); + + // Find and tap the second reaction (love) + final loveIconFinder = find.byIcon(Icons.favorite); + expect(loveIconFinder, findsOneWidget); + await tester.tap(loveIconFinder); + await tester.pumpAndSettle(); + + expect(messageAction, isA()); + // Verify callback was called with correct reaction type + expect((messageAction! as SelectReaction).reaction.type, 'love'); + }, + ); + }); + + group('StreamMessageActionsModal Golden Tests', () { + Widget buildMessageWidget({bool reverse = false}) { + return Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + final messageTheme = theme.getMessageTheme(reverse: reverse); + + return Container( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + color: messageTheme.messageBackgroundColor, + ), + child: Text( + message.text ?? '', + style: messageTheme.messageTextStyle, + ), + ); + }, + ); + } + + for (final brightness in Brightness.values) { + final theme = brightness.name; + + goldenTest( + 'StreamMessageActionsModal in $theme theme', + fileName: 'stream_message_actions_modal_$theme', + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 600), + builder: () => _wrapWithMaterialApp( + brightness: brightness, + StreamMessageActionsModal( + message: message, + messageActions: messageActions, + messageWidget: buildMessageWidget(), + ), + ), + ); + + goldenTest( + 'StreamMessageActionsModal with reaction picker in $theme theme', + fileName: 'stream_message_actions_modal_with_reactions_$theme', + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 600), + builder: () => _wrapWithMaterialApp( + brightness: brightness, + StreamMessageActionsModal( + message: message, + messageActions: messageActions, + messageWidget: buildMessageWidget(), + showReactionPicker: true, + ), + ), + ); + + goldenTest( + 'StreamMessageActionsModal reversed in $theme theme', + fileName: 'stream_message_actions_modal_reversed_$theme', + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 600), + builder: () => _wrapWithMaterialApp( + brightness: brightness, + StreamMessageActionsModal( + message: message, + messageActions: messageActions, + messageWidget: buildMessageWidget(reverse: true), + reverse: true, + ), + ), + ); + + goldenTest( + 'StreamMessageActionsModal reversed with reaction picker in $theme theme', + fileName: 'stream_message_actions_modal_reversed_with_reactions_$theme', + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 600), + builder: () => _wrapWithMaterialApp( + brightness: brightness, + StreamMessageActionsModal( + message: message, + messageActions: messageActions, + messageWidget: buildMessageWidget(reverse: true), + showReactionPicker: true, + reverse: true, + ), + ), + ); + } + }); +} + +Widget _wrapWithMaterialApp( + Widget child, { + Brightness? brightness, + List? reactionIcons, +}) { + return Portal( + child: MaterialApp( + debugShowCheckedModeBanner: false, + home: StreamChatConfiguration( + data: StreamChatConfigurationData(reactionIcons: reactionIcons), + child: StreamChatTheme( + data: StreamChatThemeData(brightness: brightness), + child: Builder(builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: ColoredBox( + color: theme.colorTheme.overlay, + child: Padding( + padding: const EdgeInsets.all(8), + child: child, + ), + ), + ); + }), + ), + ), + ), + ); +} diff --git a/packages/stream_chat_flutter/test/src/message_modal/message_reactions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_modal/message_reactions_modal_test.dart new file mode 100644 index 0000000000..2b942ad0de --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_modal/message_reactions_modal_test.dart @@ -0,0 +1,276 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + final message = Message( + id: 'test-message', + text: 'This is a test message', + createdAt: DateTime.now(), + user: User(id: 'test-user', name: 'Test User'), + latestReactions: [ + Reaction( + type: 'love', + messageId: 'test-message', + user: User(id: 'user-1', name: 'User 1'), + createdAt: DateTime.now(), + ), + Reaction( + type: 'like', + messageId: 'test-message', + user: User(id: 'user-2', name: 'User 2'), + createdAt: DateTime.now(), + ), + ], + reactionGroups: { + 'love': ReactionGroup(count: 1, sumScores: 1), + 'like': ReactionGroup(count: 1, sumScores: 1), + }, + ); + + late MockClient mockClient; + + setUp(() { + mockClient = MockClient(); + + final mockClientState = MockClientState(); + when(() => mockClient.state).thenReturn(mockClientState); + + // Mock the current user for the message reactions test + final currentUser = OwnUser(id: 'current-user', name: 'Current User'); + when(() => mockClientState.currentUser).thenReturn(currentUser); + }); + + tearDown(() => reset(mockClient)); + + group('StreamMessageReactionsModal', () { + testWidgets( + 'renders message widget and reactions correctly', + (tester) async { + await tester.pumpWidget( + _wrapWithMaterialApp( + client: mockClient, + StreamMessageReactionsModal( + message: message, + messageWidget: const Text('Message Widget'), + ), + ), + ); + + // Use a longer timeout to ensure everything is rendered + await tester.pumpAndSettle(const Duration(seconds: 2)); + expect(find.text('Message Widget'), findsOneWidget); + // Check for reaction picker + expect(find.byType(StreamReactionPicker), findsOneWidget); + // Check for reaction details + expect(find.byType(StreamUserReactions), findsOneWidget); + }, + ); + + testWidgets( + 'calls onUserAvatarTap when user avatar is tapped', + (tester) async { + User? tappedUser; + + // Create just the StreamUserAvatar directly + await tester.pumpWidget( + _wrapWithMaterialApp( + client: mockClient, + StreamMessageReactionsModal( + message: message, + messageWidget: const Text('Message Widget'), + onUserAvatarTap: (user) { + tappedUser = user; + }, + ), + ), + ); + + await tester.pumpAndSettle(); + + final avatar = find.descendant( + of: find.byType(StreamUserReactions), + matching: find.byType(StreamUserAvatar), + ); + + // Verify the avatar is rendered + expect(avatar, findsNWidgets(2)); + + // Tap on the first avatar directly + await tester.tap(avatar.first); + await tester.pumpAndSettle(); + + // Verify the callback was called + expect(tappedUser, isNotNull); + }, + ); + + testWidgets( + 'calls onReactionPicked with SelectReaction when reaction is selected', + (tester) async { + MessageAction? messageAction; + + // Define custom reaction icons for testing + final testReactionIcons = [ + StreamReactionIcon( + type: 'like', + builder: (context, isActive, size) => const Icon(Icons.thumb_up), + ), + StreamReactionIcon( + type: 'love', + builder: (context, isActive, size) => const Icon(Icons.favorite), + ), + StreamReactionIcon( + type: 'camera', + builder: (context, isActive, size) => const Icon(Icons.camera), + ), + StreamReactionIcon( + type: 'call', + builder: (context, isActive, size) => const Icon(Icons.call), + ), + ]; + + await tester.pumpWidget( + _wrapWithMaterialApp( + client: mockClient, + reactionIcons: testReactionIcons, + StreamMessageReactionsModal( + message: message, + messageWidget: const Text('Message Widget'), + onReactionPicked: (action) => messageAction = action, + ), + ), + ); + + // Use a longer timeout to ensure everything is rendered + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Verify reaction picker is shown + expect(find.byType(StreamReactionPicker), findsOneWidget); + + // Find and tap the camera reaction (camera) + final reactionIconFinder = find.byIcon(Icons.camera); + expect(reactionIconFinder, findsOneWidget); + await tester.tap(reactionIconFinder); + await tester.pumpAndSettle(); + + expect(messageAction, isA()); + // Verify callback was called with correct reaction type + expect((messageAction! as SelectReaction).reaction.type, 'camera'); + + // Find and tap the call reaction (call) + final loveIconFinder = find.byIcon(Icons.call); + expect(loveIconFinder, findsOneWidget); + await tester.tap(loveIconFinder); + await tester.pumpAndSettle(); + + expect(messageAction, isA()); + // Verify callback was called with correct reaction type + expect((messageAction! as SelectReaction).reaction.type, 'call'); + }, + ); + }); + + group('StreamMessageReactionsModal Golden Tests', () { + Widget buildMessageWidget({bool reverse = false}) { + return Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + final messageTheme = theme.getMessageTheme(reverse: reverse); + + return Container( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + color: messageTheme.messageBackgroundColor, + ), + child: Text( + message.text ?? '', + style: messageTheme.messageTextStyle, + ), + ); + }, + ); + } + + for (final brightness in Brightness.values) { + final theme = brightness.name; + + goldenTest( + 'StreamMessageReactionsModal in $theme theme', + fileName: 'stream_message_reactions_modal_$theme', + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 600), + builder: () => _wrapWithMaterialApp( + client: mockClient, + brightness: brightness, + StreamMessageReactionsModal( + message: message, + messageWidget: buildMessageWidget(), + onReactionPicked: (_) {}, + ), + ), + ); + + goldenTest( + 'StreamMessageReactionsModal reversed in $theme theme', + fileName: 'stream_message_reactions_modal_reversed_$theme', + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 600), + builder: () => _wrapWithMaterialApp( + client: mockClient, + brightness: brightness, + StreamMessageReactionsModal( + message: message, + messageWidget: buildMessageWidget(reverse: true), + reverse: true, + onReactionPicked: (_) {}, + ), + ), + ); + } + }); +} + +Widget _wrapWithMaterialApp( + Widget child, { + required StreamChatClient client, + Brightness? brightness, + List? reactionIcons, +}) { + return Portal( + child: MaterialApp( + debugShowCheckedModeBanner: false, + home: StreamChat( + client: client, + // Mock the connectivity stream to always return wifi. + connectivityStream: Stream.value([ConnectivityResult.wifi]), + child: StreamChatConfiguration( + data: StreamChatConfigurationData(reactionIcons: reactionIcons), + child: StreamChatTheme( + data: StreamChatThemeData(brightness: brightness), + child: Builder(builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: ColoredBox( + color: theme.colorTheme.overlay, + child: Padding( + padding: const EdgeInsets.all(8), + child: child, + ), + ), + ); + }), + ), + ), + ), + ), + ); +} diff --git a/packages/stream_chat_flutter/test/src/message_modal/moderated_message_actions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_modal/moderated_message_actions_modal_test.dart new file mode 100644 index 0000000000..59e76cf347 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_modal/moderated_message_actions_modal_test.dart @@ -0,0 +1,139 @@ +// ignore_for_file: lines_longer_than_80_chars + +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + final message = Message( + id: 'test-message', + type: MessageType.error, + text: 'This is a test message', + createdAt: DateTime.now(), + user: User(id: 'test-user', name: 'Test User'), + moderation: const Moderation( + action: ModerationAction.bounce, + originalText: 'This is a test message with flagged content', + ), + ); + + final messageActions = [ + StreamMessageAction( + title: const Text('Send Anyway'), + leading: const StreamSvgIcon(icon: StreamSvgIcons.send), + action: ResendMessage(message: message), + ), + StreamMessageAction( + title: const Text('Edit Message'), + leading: const StreamSvgIcon(icon: StreamSvgIcons.edit), + action: EditMessage(message: message), + ), + StreamMessageAction( + isDestructive: true, + title: const Text('Delete Message'), + leading: const StreamSvgIcon(icon: StreamSvgIcons.delete), + action: HardDeleteMessage(message: message), + ), + ]; + + group('ModeratedMessageActionsModal', () { + testWidgets('renders title, content and actions correctly', (tester) async { + await tester.pumpWidget( + _wrapWithMaterialApp( + ModeratedMessageActionsModal( + message: message, + messageActions: messageActions, + ), + ), + ); + + // Use a longer timeout to ensure everything is rendered + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Check for icon, title and content + expect(find.byType(StreamSvgIcon), findsWidgets); + expect(find.byType(Text), findsWidgets); + + // Check for actions + expect(find.text('Send Anyway'), findsOneWidget); + expect(find.text('Edit Message'), findsOneWidget); + expect(find.text('Delete Message'), findsOneWidget); + }); + + testWidgets('action buttons call the correct callbacks', (tester) async { + MessageAction? messageAction; + + await tester.pumpWidget( + _wrapWithMaterialApp( + ModeratedMessageActionsModal( + message: message, + messageActions: messageActions, + onActionTap: (action) => messageAction = action, + ), + ), + ); + + await tester.pumpAndSettle(); + + // Tap on Send Anyway button + await tester.tap(find.text('Send Anyway')); + await tester.pumpAndSettle(); + expect(messageAction, isA()); + + // Tap on Edit Message button + await tester.tap(find.text('Edit Message')); + await tester.pumpAndSettle(); + expect(messageAction, isA()); + + // Tap on Delete Message button + await tester.tap(find.text('Delete Message')); + await tester.pumpAndSettle(); + expect(messageAction, isA()); + }); + }); + + group('ModeratedMessageActionsModal Golden Tests', () { + for (final brightness in Brightness.values) { + final theme = brightness.name; + + goldenTest( + 'ModeratedMessageActionsModal in $theme theme', + fileName: 'moderated_message_actions_modal_$theme', + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 350), + builder: () => _wrapWithMaterialApp( + brightness: brightness, + ModeratedMessageActionsModal( + message: message, + messageActions: messageActions, + ), + ), + ); + } + }); +} + +Widget _wrapWithMaterialApp( + Widget child, { + Brightness? brightness, +}) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: StreamChatTheme( + data: StreamChatThemeData(brightness: brightness), + child: Builder(builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: ColoredBox( + color: theme.colorTheme.overlay, + child: Padding( + padding: const EdgeInsets.all(8), + child: child, + ), + ), + ); + }), + ), + ); +} diff --git a/packages/stream_chat_flutter/test/src/message_reactions_modal/message_reactions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reactions_modal/message_reactions_modal_test.dart deleted file mode 100644 index 8cf78680b3..0000000000 --- a/packages/stream_chat_flutter/test/src/message_reactions_modal/message_reactions_modal_test.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../mocks.dart'; - -void main() { - testWidgets( - 'control test', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - final themeData = ThemeData(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - final message = Message( - id: 'test', - text: 'test message', - user: User( - id: 'test-user', - ), - ); - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: StreamChannel( - channel: channel, - child: StreamMessageReactionsModal( - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), - ), - message: message, - messageTheme: streamTheme.ownMessageTheme, - ), - ), - ), - ), - ); - - await tester.pump(const Duration(milliseconds: 1000)); - - expect(find.byType(StreamReactionBubble), findsNothing); - - expect(find.byType(StreamUserAvatar), findsNothing); - }, - ); - - testWidgets( - 'it should apply passed parameters', - (WidgetTester tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - final themeData = ThemeData(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - final message = Message( - id: 'test', - text: 'test message', - user: User( - id: 'test-user', - ), - latestReactions: [ - Reaction( - messageId: 'test', - user: User(id: 'testid'), - type: 'test', - ), - ], - ); - - // ignore: prefer_function_declarations_over_variables - final onUserAvatarTap = (u) => print('ok'); - - await tester.pumpWidget( - MaterialApp( - theme: themeData, - home: StreamChat( - client: client, - streamChatThemeData: streamTheme, - child: StreamChannel( - channel: channel, - child: StreamMessageReactionsModal( - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), - ), - message: message, - messageTheme: streamTheme.ownMessageTheme, - reverse: true, - showReactionPicker: false, - onUserAvatarTap: onUserAvatarTap, - ), - ), - ), - ), - ); - - await tester.pump(const Duration(milliseconds: 1000)); - - expect(find.byKey(const Key('MessageWidget')), findsOneWidget); - - expect(find.byType(StreamReactionBubble), findsOneWidget); - expect(find.byType(StreamUserAvatar), findsOneWidget); - }, - ); -} diff --git a/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_custom.png b/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_custom.png index 6634d55f12..91d910e7f3 100644 Binary files a/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_custom.png and b/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_custom.png differ diff --git a/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_dark.png b/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_dark.png index 9b7f30f4b5..ed516899af 100644 Binary files a/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_dark.png and b/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_light.png b/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_light.png index 6c46e0739d..df2a1b9a15 100644 Binary files a/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_light.png and b/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_light.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_2.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_2.png index a7daaa3051..e78fe9bfe4 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_2.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_2.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_3_dark.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_3_dark.png index 2a88d12d61..dee1aa31be 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_3_dark.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_3_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_3_light.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_3_light.png index a2f9afc0bb..d4de763486 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_3_light.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_3_light.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_like_dark.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_like_dark.png index b9fdc003ce..08681c327f 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_like_dark.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_like_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_like_light.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_like_light.png index 5d5d6e1ba8..8097b57606 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_like_light.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/reaction_bubble_like_light.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_custom_dark.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_custom_dark.png index 8480b35ee1..8fc9b61c50 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_custom_dark.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_custom_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_custom_light.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_custom_light.png index 38e1ce5bf7..6cade430e4 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_custom_light.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_custom_light.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_dark.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_dark.png index d0e6aab285..a5cfa0663a 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_dark.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_empty_dark.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_empty_dark.png index 6d63fa50ba..b1cfc6c871 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_empty_dark.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_empty_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_empty_light.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_empty_light.png index f914ffb2ad..c13687d05c 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_empty_light.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_empty_light.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_inverted_dark.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_inverted_dark.png index 2d05d18ac3..5ed2a156ac 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_inverted_dark.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_inverted_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_inverted_light.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_inverted_light.png index 1434322d64..685f779862 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_inverted_light.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_inverted_light.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_less_data_dark.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_less_data_dark.png index 77a1e1299b..da0889c86b 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_less_data_dark.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_less_data_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_less_data_light.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_less_data_light.png index 12b942eebc..efb656ce3b 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_less_data_light.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_less_data_light.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_light.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_light.png index 26add2a992..38ee4cfbd0 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_light.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_light.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_progress_dark.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_progress_dark.png index 9d1854a8e0..b8db680f61 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_progress_dark.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_progress_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_progress_light.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_progress_light.png index 4951487559..750abddc9d 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_progress_light.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_audio_waveform_slider_progress_light.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_timestamp_light.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_timestamp_light.png index e8a6642fed..88103910e6 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_timestamp_light.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_timestamp_light.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/system_message_dark.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/system_message_dark.png index c22da26c2b..ebc6a952b3 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/system_message_dark.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/system_message_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/misc/goldens/ci/system_message_light.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/system_message_light.png index 2eae251442..6767b2dda2 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/system_message_light.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/system_message_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_dark.png index cd0a0fcf60..4fb9a2a355 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_dark.png index 5e9b3495ab..eccdc87852 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_light.png index f402fea911..7b2f143ff2 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_error_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_light.png index ca3fd01230..e0ac024ee9 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_option_reorderable_list_view_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_dark.png index a6dfc2bf42..52f0f9d1e2 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_dark.png index 49eba87c27..7420f912f6 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_light.png index 4b873c709d..59404bb891 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_error_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_light.png index 86a99f7cc9..33dae720bb 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_question_text_field_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_dark.png index d35fd3f84e..e2c8fddfcf 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_light.png index 21658c0292..4b88ae986c 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png index 174d489c63..60157c6ef9 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_light.png index 374dd587d2..8930fb31b0 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/stream_poll_creator_full_screen_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_dark.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_dark.png index cd0a0fcf60..4fb9a2a355 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_dark.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_error.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_error.png index f402fea911..7b2f143ff2 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_error.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_error.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_light.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_light.png index ca3fd01230..e0ac024ee9 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_light.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_option_reorderable_list_view_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_dark.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_dark.png index a6dfc2bf42..52f0f9d1e2 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_dark.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_error.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_error.png index 4b873c709d..59404bb891 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_error.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_error.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_light.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_light.png index 86a99f7cc9..33dae720bb 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_light.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/poll_question_text_field_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_dark.png index d35fd3f84e..e2c8fddfcf 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_light.png index 21658c0292..4b88ae986c 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png index 174d489c63..60157c6ef9 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_light.png index 374dd587d2..8930fb31b0 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_creator_full_screen_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_options_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_options_dialog_dark.png index 005eae2c96..cabca13448 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_options_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_options_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_options_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_options_dialog_light.png index 3600948ad2..32a3d52dff 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_options_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_options_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_dark.png index b5625024ee..8c70302aaf 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_light.png index c71faa699e..9e0cf20629 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_with_show_all_dark.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_with_show_all_dark.png index 716fe34def..b41798acd9 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_with_show_all_dark.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_with_show_all_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_with_show_all_light.png b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_with_show_all_light.png index d0b26c7658..f317d58770 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_with_show_all_light.png and b/packages/stream_chat_flutter/test/src/poll/goldens/ci/stream_poll_results_dialog_with_show_all_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_dark.png index e0c0818647..309349c4db 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_light.png index aa03c1e2c8..83b735b47a 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_dark.png index 1271cafe5c..898509b2f2 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_light.png index b0a3fb329d..c3f83045e5 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_add_comment_dialog_with_initial_value_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_end_vote_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_end_vote_dialog_light.png index 3a8a10da92..ed70476642 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_end_vote_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_end_vote_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_dark.png index 31040b0822..f4fb8c771c 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_light.png index c7c233831c..eabe415d79 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_long_question_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_long_question_dark.png index 64b49fed36..43b196316f 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_long_question_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_long_question_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_long_question_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_long_question_light.png index 67159eb2af..c062eaa49f 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_long_question_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_long_question_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_all_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_all_dark.png index c7b28fe98e..335129ec2d 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_all_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_all_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_all_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_all_light.png index 1e25581c3d..8ba4098351 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_all_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_all_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_disabled_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_disabled_dark.png index 45af8b790d..ebc4effb46 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_disabled_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_disabled_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_disabled_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_disabled_light.png index 5d1b41f480..2d79758182 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_disabled_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_disabled_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_limited_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_limited_dark.png index 1bfef3bf83..d7e9fc6c47 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_limited_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_limited_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_limited_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_limited_light.png index f7b36287dd..2279f4d996 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_limited_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_limited_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_unique_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_unique_dark.png index 31040b0822..f4fb8c771c 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_unique_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_unique_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_unique_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_unique_light.png index c7c233831c..eabe415d79 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_unique_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_header_subtitle_voting_mode_unique_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_dark.png index b20fd3c286..c8dd458771 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_light.png index 70b79ba6fb..20e5396e06 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_dark.png index bf66fa5245..672e716494 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_light.png index 354ccd6d92..e01dc8605a 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_suggest_option_dialog_with_initial_option_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_closed_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_closed_dark.png index 974868b683..525bccd016 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_closed_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_closed_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_closed_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_closed_light.png index 8b25be5aaa..6676c7dd05 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_closed_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_closed_light.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_dark.png index 2ef4f6913c..b9b892057e 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_light.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_light.png index cc724f3300..2962d4f1a5 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_light.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/stream_poll_interactor_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_icon_button_selected_dark.png b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_icon_button_selected_dark.png new file mode 100644 index 0000000000..25d222bed1 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_icon_button_selected_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_icon_button_selected_light.png b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_icon_button_selected_light.png new file mode 100644 index 0000000000..670bc5de2f Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_icon_button_selected_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_icon_button_unselected_dark.png b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_icon_button_unselected_dark.png new file mode 100644 index 0000000000..657eee5c62 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_icon_button_unselected_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_icon_button_unselected_light.png b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_icon_button_unselected_light.png new file mode 100644 index 0000000000..c089b20c3b Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_icon_button_unselected_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_picker_icon_list_dark.png b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_picker_icon_list_dark.png new file mode 100644 index 0000000000..f0b4c634f8 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_picker_icon_list_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_picker_icon_list_light.png b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_picker_icon_list_light.png new file mode 100644 index 0000000000..d14ce83155 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_picker_icon_list_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_picker_icon_list_selected_dark.png b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_picker_icon_list_selected_dark.png new file mode 100644 index 0000000000..068b97997f Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_picker_icon_list_selected_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_picker_icon_list_selected_light.png b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_picker_icon_list_selected_light.png new file mode 100644 index 0000000000..0a3feaed26 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/reaction_picker_icon_list_selected_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/goldens/ci/stream_reaction_picker_dark.png b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/stream_reaction_picker_dark.png new file mode 100644 index 0000000000..04a299b283 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/stream_reaction_picker_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/goldens/ci/stream_reaction_picker_light.png b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/stream_reaction_picker_light.png new file mode 100644 index 0000000000..3aaa202678 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/stream_reaction_picker_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/goldens/ci/stream_reaction_picker_selected_dark.png b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/stream_reaction_picker_selected_dark.png new file mode 100644 index 0000000000..845ea2d8d9 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/stream_reaction_picker_selected_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/goldens/ci/stream_reaction_picker_selected_light.png b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/stream_reaction_picker_selected_light.png new file mode 100644 index 0000000000..1a134c74f9 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/goldens/ci/stream_reaction_picker_selected_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/reaction_picker_icon_list_test.dart b/packages/stream_chat_flutter/test/src/reactions/reaction_picker_icon_list_test.dart new file mode 100644 index 0000000000..0c86fc637c --- /dev/null +++ b/packages/stream_chat_flutter/test/src/reactions/reaction_picker_icon_list_test.dart @@ -0,0 +1,396 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + final reactionIcons = [ + StreamReactionIcon( + type: 'love', + builder: (context, isSelected, iconSize) => StreamSvgIcon( + icon: StreamSvgIcons.loveReaction, + size: iconSize, + color: isSelected ? Colors.red : Colors.grey.shade700, + ), + ), + StreamReactionIcon( + type: 'thumbsUp', + builder: (context, isSelected, iconSize) => StreamSvgIcon( + icon: StreamSvgIcons.thumbsUpReaction, + size: iconSize, + color: isSelected ? Colors.blue : Colors.grey.shade700, + ), + ), + StreamReactionIcon( + type: 'thumbsDown', + builder: (context, isSelected, iconSize) => StreamSvgIcon( + icon: StreamSvgIcons.thumbsDownReaction, + size: iconSize, + color: isSelected ? Colors.orange : Colors.grey.shade700, + ), + ), + StreamReactionIcon( + type: 'lol', + builder: (context, isSelected, iconSize) => StreamSvgIcon( + icon: StreamSvgIcons.lolReaction, + size: iconSize, + color: isSelected ? Colors.amber : Colors.grey.shade700, + ), + ), + StreamReactionIcon( + type: 'wut', + builder: (context, isSelected, iconSize) => StreamSvgIcon( + icon: StreamSvgIcons.wutReaction, + size: iconSize, + color: isSelected ? Colors.purple : Colors.grey.shade700, + ), + ), + ]; + + group('ReactionIconButton', () { + testWidgets( + 'renders correctly with selected state', + (WidgetTester tester) async { + await tester.pumpWidget( + _wrapWithMaterialApp( + ReactionIconButton( + icon: ReactionPickerIcon( + isSelected: true, + type: reactionIcons.first.type, + builder: reactionIcons.first.builder, + ), + onPressed: () {}, + ), + ), + ); + + expect(find.byType(ReactionIconButton), findsOneWidget); + expect(find.byType(IconButton), findsOneWidget); + }, + ); + + testWidgets( + 'triggers callback when pressed', + (WidgetTester tester) async { + var callbackTriggered = false; + + await tester.pumpWidget( + _wrapWithMaterialApp( + ReactionIconButton( + icon: ReactionPickerIcon( + type: reactionIcons.first.type, + builder: reactionIcons.first.builder, + ), + onPressed: () { + callbackTriggered = true; + }, + ), + ), + ); + + await tester.tap(find.byType(IconButton)); + await tester.pump(); + + expect(callbackTriggered, isTrue); + }, + ); + + group('Golden tests', () { + for (final brightness in [Brightness.light, Brightness.dark]) { + final theme = brightness.name; + + goldenTest( + 'ReactionIconButton unselected in $theme theme', + fileName: 'reaction_icon_button_unselected_$theme', + constraints: const BoxConstraints.tightFor(width: 60, height: 60), + builder: () => _wrapWithMaterialApp( + brightness: brightness, + ReactionIconButton( + icon: ReactionPickerIcon( + type: reactionIcons.first.type, + builder: reactionIcons.first.builder, + ), + onPressed: () {}, + ), + ), + ); + + goldenTest( + 'ReactionIconButton selected in $theme theme', + fileName: 'reaction_icon_button_selected_$theme', + constraints: const BoxConstraints.tightFor(width: 60, height: 60), + builder: () => _wrapWithMaterialApp( + brightness: brightness, + ReactionIconButton( + icon: ReactionPickerIcon( + isSelected: true, + type: reactionIcons.first.type, + builder: reactionIcons.first.builder, + ), + onPressed: () {}, + ), + ), + ); + } + }); + }); + + group('ReactionPickerIconList', () { + testWidgets( + 'renders all reaction icons', + (WidgetTester tester) async { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + await tester.pumpWidget( + _wrapWithMaterialApp( + ReactionPickerIconList( + message: message, + reactionIcons: reactionIcons, + onReactionPicked: (_) {}, + ), + ), + ); + + // Wait for animations to complete + await tester.pumpAndSettle(); + + // Should find same number of IconButtons as reactionIcons + expect(find.byType(IconButton), findsNWidgets(reactionIcons.length)); + }, + ); + + testWidgets( + 'triggers onReactionPicked when a reaction icon is tapped', + (WidgetTester tester) async { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + Reaction? pickedReaction; + + await tester.pumpWidget( + _wrapWithMaterialApp( + ReactionPickerIconList( + message: message, + reactionIcons: reactionIcons, + onReactionPicked: (reaction) { + pickedReaction = reaction; + }, + ), + ), + ); + + // Wait for animations to complete + await tester.pumpAndSettle(); + + // Tap the first reaction icon + await tester.tap(find.byType(IconButton).first); + await tester.pump(); + + // Verify callback was triggered with correct reaction type + expect(pickedReaction, isNotNull); + expect(pickedReaction!.type, 'love'); + }, + ); + + testWidgets( + 'shows reaction icons with animation', + (WidgetTester tester) async { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + await tester.pumpWidget( + _wrapWithMaterialApp( + ReactionPickerIconList( + message: message, + reactionIcons: reactionIcons, + onReactionPicked: (_) {}, + ), + ), + ); + + // Initially the animations should be starting + await tester.pump(); + + // After animation completes + await tester.pumpAndSettle(); + + // Should have all reactions visible + expect( + find.byType(ReactionIconButton), + findsNWidgets(reactionIcons.length), + ); + }, + ); + + testWidgets( + 'properly handles message with existing reactions', + (WidgetTester tester) async { + // Create a message with an existing reaction + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ownReactions: [ + Reaction( + type: 'love', + messageId: 'test-message', + userId: 'test-user', + ), + ], + ); + + await tester.pumpWidget( + _wrapWithMaterialApp( + Material( + child: ReactionPickerIconList( + message: message, + reactionIcons: reactionIcons, + onReactionPicked: (_) {}, + ), + ), + ), + ); + + // Wait for animations + await tester.pumpAndSettle(); + + // All reaction buttons should be rendered + expect( + find.byType(ReactionIconButton), + findsNWidgets(reactionIcons.length), + ); + }, + ); + + testWidgets( + 'updates when reactionIcons change', + (WidgetTester tester) async { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + // Build with initial set of reaction icons + await tester.pumpWidget( + _wrapWithMaterialApp( + ReactionPickerIconList( + message: message, + // Only first two reactions + reactionIcons: reactionIcons.sublist(0, 2), + onReactionPicked: (_) {}, + ), + ), + ); + + await tester.pumpAndSettle(); + expect(find.byType(IconButton), findsNWidgets(2)); + + // Rebuild with all reaction icons + await tester.pumpWidget( + _wrapWithMaterialApp( + ReactionPickerIconList( + message: message, + reactionIcons: reactionIcons, // All three reactions + onReactionPicked: (_) {}, + ), + ), + ); + + await tester.pumpAndSettle(); + expect(find.byType(IconButton), findsNWidgets(5)); + }, + ); + + group('Golden tests', () { + for (final brightness in [Brightness.light, Brightness.dark]) { + final theme = brightness.name; + + goldenTest( + 'ReactionPickerIconList in $theme theme', + fileName: 'reaction_picker_icon_list_$theme', + constraints: const BoxConstraints.tightFor(width: 400, height: 100), + builder: () { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + return _wrapWithMaterialApp( + brightness: brightness, + ReactionPickerIconList( + message: message, + reactionIcons: reactionIcons, + onReactionPicked: (_) {}, + ), + ); + }, + ); + + goldenTest( + 'ReactionPickerIconList with selected reaction in $theme theme', + fileName: 'reaction_picker_icon_list_selected_$theme', + constraints: const BoxConstraints.tightFor(width: 400, height: 100), + builder: () { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ownReactions: [ + Reaction( + type: 'love', + messageId: 'test-message', + userId: 'test-user', + ), + ], + ); + + return _wrapWithMaterialApp( + brightness: brightness, + ReactionPickerIconList( + message: message, + reactionIcons: reactionIcons, + onReactionPicked: (_) {}, + ), + ); + }, + ); + } + }); + }); +} + +Widget _wrapWithMaterialApp( + Widget child, { + Brightness? brightness, +}) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: StreamChatTheme( + data: StreamChatThemeData(brightness: brightness), + child: Builder(builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.overlay, + body: Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: child, + ), + ), + ); + }), + ), + ); +} diff --git a/packages/stream_chat_flutter/test/src/reactions/reaction_picker_test.dart b/packages/stream_chat_flutter/test/src/reactions/reaction_picker_test.dart new file mode 100644 index 0000000000..3b80f884e4 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/reactions/reaction_picker_test.dart @@ -0,0 +1,197 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + final reactionIcons = [ + StreamReactionIcon( + type: 'love', + builder: (context, isSelected, iconSize) => StreamSvgIcon( + icon: StreamSvgIcons.loveReaction, + size: iconSize, + color: isSelected ? Colors.red : Colors.grey.shade700, + ), + ), + StreamReactionIcon( + type: 'thumbsUp', + builder: (context, isSelected, iconSize) => StreamSvgIcon( + icon: StreamSvgIcons.thumbsUpReaction, + size: iconSize, + color: isSelected ? Colors.blue : Colors.grey.shade700, + ), + ), + StreamReactionIcon( + type: 'thumbsDown', + builder: (context, isSelected, iconSize) => StreamSvgIcon( + icon: StreamSvgIcons.thumbsDownReaction, + size: iconSize, + color: isSelected ? Colors.orange : Colors.grey.shade700, + ), + ), + StreamReactionIcon( + type: 'lol', + builder: (context, isSelected, iconSize) => StreamSvgIcon( + icon: StreamSvgIcons.lolReaction, + size: iconSize, + color: isSelected ? Colors.amber : Colors.grey.shade700, + ), + ), + StreamReactionIcon( + type: 'wut', + builder: (context, isSelected, iconSize) => StreamSvgIcon( + icon: StreamSvgIcons.wutReaction, + size: iconSize, + color: isSelected ? Colors.purple : Colors.grey.shade700, + ), + ), + ]; + + testWidgets( + 'renders with correct message and reaction icons', + (WidgetTester tester) async { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamReactionPicker( + message: message, + reactionIcons: reactionIcons, + onReactionPicked: (_) {}, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Verify the widget renders with correct structure + expect(find.byType(StreamReactionPicker), findsOneWidget); + expect(find.byType(ReactionPickerIconList), findsOneWidget); + + // Verify the correct number of reaction icons + expect(find.byType(IconButton), findsNWidgets(reactionIcons.length)); + }, + ); + + testWidgets( + 'calls onReactionPicked when a reaction is selected', + (WidgetTester tester) async { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + Reaction? pickedReaction; + + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamReactionPicker( + message: message, + reactionIcons: reactionIcons, + onReactionPicked: (reaction) { + pickedReaction = reaction; + }, + ), + ), + ); + + // Wait for animations to complete + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Tap the first reaction icon + await tester.tap(find.byType(IconButton).first); + await tester.pump(); + + // Verify the callback was called with the correct reaction + expect(pickedReaction, isNotNull); + // Updated to match first reaction type in the list + expect(pickedReaction!.type, 'love'); + }, + ); + + group('Golden tests', () { + for (final brightness in [Brightness.light, Brightness.dark]) { + final theme = brightness.name; + + goldenTest( + 'StreamReactionPicker in $theme theme', + fileName: 'stream_reaction_picker_$theme', + constraints: const BoxConstraints.tightFor(width: 400, height: 100), + builder: () { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + return _wrapWithMaterialApp( + brightness: brightness, + StreamReactionPicker( + message: message, + reactionIcons: reactionIcons, + onReactionPicked: (_) {}, + ), + ); + }, + ); + + goldenTest( + 'StreamReactionPicker with selected reaction in $theme theme', + fileName: 'stream_reaction_picker_selected_$theme', + constraints: const BoxConstraints.tightFor(width: 400, height: 100), + builder: () { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ownReactions: [ + Reaction( + type: 'love', + messageId: 'test-message', + userId: 'test-user', + ), + ], + ); + + return _wrapWithMaterialApp( + brightness: brightness, + StreamReactionPicker( + message: message, + reactionIcons: reactionIcons, + onReactionPicked: (_) {}, + ), + ); + }, + ); + } + }); +} + +Widget _wrapWithMaterialApp( + Widget child, { + Brightness? brightness, +}) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: StreamChatTheme( + data: StreamChatThemeData(brightness: brightness), + child: Builder(builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.overlay, + body: Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: child, + ), + ), + ); + }), + ), + ); +} diff --git a/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_dark.png b/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_dark.png index eb47dc88e3..e96b66cffd 100644 Binary files a/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_dark.png and b/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_light.png b/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_light.png index 7d71fa6e70..b2376d9a29 100644 Binary files a/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_light.png and b/packages/stream_chat_flutter/test/src/scroll_view/draft_scroll_view/goldens/ci/stream_draft_list_tile_light.png differ diff --git a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_thread_list_tile_dark.png b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_thread_list_tile_dark.png index c82908a3d8..7e8ce51cee 100644 Binary files a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_thread_list_tile_dark.png and b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_thread_list_tile_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_thread_list_tile_light.png b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_thread_list_tile_light.png index 153e5cf18f..ae1bb58c7c 100644 Binary files a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_thread_list_tile_light.png and b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_thread_list_tile_light.png differ diff --git a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_light.png b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_light.png index f917ecd95f..5ff8e02438 100644 Binary files a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_light.png and b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_light.png differ diff --git a/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart index 3f5a546923..1b9706154a 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart @@ -61,10 +61,10 @@ final _channelThemeControl = StreamChannelHeaderThemeData( ), ), color: const Color(0xff101418), - titleStyle: StreamTextTheme.light().headlineBold.copyWith( + titleStyle: const StreamTextTheme.light().headlineBold.copyWith( color: const Color(0xffffffff), ), - subtitleStyle: StreamTextTheme.light().footnote.copyWith( + subtitleStyle: const StreamTextTheme.light().footnote.copyWith( color: const Color(0xff7a7a7a), ), ); @@ -83,7 +83,7 @@ final _channelThemeControlMidLerp = StreamChannelHeaderThemeData( fontWeight: FontWeight.w500, fontSize: 16, ), - subtitleStyle: StreamTextTheme.light().footnote.copyWith( + subtitleStyle: const StreamTextTheme.light().footnote.copyWith( color: const Color(0xff7a7a7a), ), ); @@ -96,9 +96,9 @@ final _channelThemeControlDark = StreamChannelHeaderThemeData( width: 40, ), ), - color: StreamColorTheme.dark().barsBg, - titleStyle: StreamTextTheme.dark().headlineBold, - subtitleStyle: StreamTextTheme.dark().footnote.copyWith( + color: const StreamColorTheme.dark().barsBg, + titleStyle: const StreamTextTheme.dark().headlineBold, + subtitleStyle: const StreamTextTheme.dark().footnote.copyWith( color: const Color(0xff7A7A7A), ), ); diff --git a/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart index b9bd1f8c04..b7bfcbfebd 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart @@ -66,8 +66,8 @@ final _channelListHeaderThemeControl = StreamChannelListHeaderThemeData( width: 40, ), ), - color: StreamColorTheme.light().barsBg, - titleStyle: StreamTextTheme.light().headlineBold, + color: const StreamColorTheme.light().barsBg, + titleStyle: const StreamTextTheme.light().headlineBold, ); final _channelListHeaderThemeControlMidLerp = StreamChannelListHeaderThemeData( @@ -94,6 +94,6 @@ final _channelListHeaderThemeControlDark = StreamChannelListHeaderThemeData( width: 40, ), ), - color: StreamColorTheme.dark().barsBg, - titleStyle: StreamTextTheme.dark().headlineBold, + color: const StreamColorTheme.dark().barsBg, + titleStyle: const StreamTextTheme.dark().headlineBold, ); diff --git a/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart index b966781e2a..26dd2adbd4 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart @@ -53,7 +53,7 @@ void main() { } final _channelPreviewThemeControl = StreamChannelPreviewThemeData( - unreadCounterColor: StreamColorTheme.light().accentError, + unreadCounterColor: const StreamColorTheme.light().accentError, avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -61,13 +61,13 @@ final _channelPreviewThemeControl = StreamChannelPreviewThemeData( width: 40, ), ), - titleStyle: StreamTextTheme.light().bodyBold, - subtitleStyle: StreamTextTheme.light().footnote.copyWith( + titleStyle: const StreamTextTheme.light().bodyBold, + subtitleStyle: const StreamTextTheme.light().footnote.copyWith( color: const Color(0xff7A7A7A), ), - lastMessageAtStyle: StreamTextTheme.light().footnote.copyWith( + lastMessageAtStyle: const StreamTextTheme.light().footnote.copyWith( // ignore: deprecated_member_use - color: StreamColorTheme.light().textHighEmphasis.withOpacity(0.5), + color: const StreamColorTheme.light().textHighEmphasis.withOpacity(0.5), ), indicatorIconSize: 16, ); @@ -91,7 +91,7 @@ final _channelPreviewThemeControlMidLerp = StreamChannelPreviewThemeData( fontSize: 12, fontWeight: FontWeight.w400, ), - lastMessageAtStyle: StreamTextTheme.light().footnote.copyWith( + lastMessageAtStyle: const StreamTextTheme.light().footnote.copyWith( // ignore: deprecated_member_use color: const Color(0x807f7f7f).withOpacity(0.5), ), @@ -99,7 +99,7 @@ final _channelPreviewThemeControlMidLerp = StreamChannelPreviewThemeData( ); final _channelPreviewThemeControlDark = StreamChannelPreviewThemeData( - unreadCounterColor: StreamColorTheme.dark().accentError, + unreadCounterColor: const StreamColorTheme.dark().accentError, avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -107,13 +107,13 @@ final _channelPreviewThemeControlDark = StreamChannelPreviewThemeData( width: 40, ), ), - titleStyle: StreamTextTheme.dark().bodyBold, - subtitleStyle: StreamTextTheme.dark().footnote.copyWith( + titleStyle: const StreamTextTheme.dark().bodyBold, + subtitleStyle: const StreamTextTheme.dark().footnote.copyWith( color: const Color(0xff7A7A7A), ), - lastMessageAtStyle: StreamTextTheme.dark().footnote.copyWith( + lastMessageAtStyle: const StreamTextTheme.dark().footnote.copyWith( // ignore: deprecated_member_use - color: StreamColorTheme.dark().textHighEmphasis.withOpacity(0.5), + color: const StreamColorTheme.dark().textHighEmphasis.withOpacity(0.5), ), indicatorIconSize: 16, ); diff --git a/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart index e06e0ac157..c02d77cccc 100644 --- a/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart @@ -145,14 +145,14 @@ void main() { // Light theme control final _galleryFooterThemeDataControl = StreamGalleryFooterThemeData( - backgroundColor: StreamColorTheme.light().barsBg, - shareIconColor: StreamColorTheme.light().textHighEmphasis, - titleTextStyle: StreamTextTheme.light().headlineBold, - gridIconButtonColor: StreamColorTheme.light().textHighEmphasis, - bottomSheetBackgroundColor: StreamColorTheme.light().barsBg, - bottomSheetBarrierColor: StreamColorTheme.light().overlay, - bottomSheetCloseIconColor: StreamColorTheme.light().textHighEmphasis, - bottomSheetPhotosTextStyle: StreamTextTheme.light().headlineBold, + backgroundColor: const StreamColorTheme.light().barsBg, + shareIconColor: const StreamColorTheme.light().textHighEmphasis, + titleTextStyle: const StreamTextTheme.light().headlineBold, + gridIconButtonColor: const StreamColorTheme.light().textHighEmphasis, + bottomSheetBackgroundColor: const StreamColorTheme.light().barsBg, + bottomSheetBarrierColor: const StreamColorTheme.light().overlay, + bottomSheetCloseIconColor: const StreamColorTheme.light().textHighEmphasis, + bottomSheetPhotosTextStyle: const StreamTextTheme.light().headlineBold, ); // Mid-lerp theme control @@ -177,12 +177,12 @@ const _galleryFooterThemeDataControlMidLerp = StreamGalleryFooterThemeData( // Dark theme control final _galleryFooterThemeDataControlDark = StreamGalleryFooterThemeData( - backgroundColor: StreamColorTheme.dark().barsBg, - shareIconColor: StreamColorTheme.dark().textHighEmphasis, - titleTextStyle: StreamTextTheme.dark().headlineBold, - gridIconButtonColor: StreamColorTheme.dark().textHighEmphasis, - bottomSheetBackgroundColor: StreamColorTheme.dark().barsBg, - bottomSheetBarrierColor: StreamColorTheme.dark().overlay, - bottomSheetCloseIconColor: StreamColorTheme.dark().textHighEmphasis, - bottomSheetPhotosTextStyle: StreamTextTheme.dark().headlineBold, + backgroundColor: const StreamColorTheme.dark().barsBg, + shareIconColor: const StreamColorTheme.dark().textHighEmphasis, + titleTextStyle: const StreamTextTheme.dark().headlineBold, + gridIconButtonColor: const StreamColorTheme.dark().textHighEmphasis, + bottomSheetBackgroundColor: const StreamColorTheme.dark().barsBg, + bottomSheetBarrierColor: const StreamColorTheme.dark().overlay, + bottomSheetCloseIconColor: const StreamColorTheme.dark().textHighEmphasis, + bottomSheetPhotosTextStyle: const StreamTextTheme.dark().headlineBold, ); diff --git a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart index 425fb5c151..431a113b0d 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart @@ -49,25 +49,25 @@ void main() { final _messageInputThemeControl = StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), - actionButtonColor: StreamColorTheme.light().accentPrimary, - actionButtonIdleColor: StreamColorTheme.light().textLowEmphasis, - expandButtonColor: StreamColorTheme.light().accentPrimary, - sendButtonColor: StreamColorTheme.light().accentPrimary, - sendButtonIdleColor: StreamColorTheme.light().disabled, - inputBackgroundColor: StreamColorTheme.light().barsBg, - inputTextStyle: StreamTextTheme.light().body, + actionButtonColor: const StreamColorTheme.light().accentPrimary, + actionButtonIdleColor: const StreamColorTheme.light().textLowEmphasis, + expandButtonColor: const StreamColorTheme.light().accentPrimary, + sendButtonColor: const StreamColorTheme.light().accentPrimary, + sendButtonIdleColor: const StreamColorTheme.light().disabled, + inputBackgroundColor: const StreamColorTheme.light().barsBg, + inputTextStyle: const StreamTextTheme.light().body, idleBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - StreamColorTheme.light().disabled, - StreamColorTheme.light().disabled, + const StreamColorTheme.light().disabled, + const StreamColorTheme.light().disabled, ], ), activeBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - StreamColorTheme.light().disabled, - StreamColorTheme.light().disabled, + const StreamColorTheme.light().disabled, + const StreamColorTheme.light().disabled, ], ), ); @@ -105,25 +105,25 @@ final _messageInputThemeControlMidLerp = StreamMessageInputThemeData( final _messageInputThemeControlDark = StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), - actionButtonColor: StreamColorTheme.dark().accentPrimary, - actionButtonIdleColor: StreamColorTheme.dark().textLowEmphasis, - expandButtonColor: StreamColorTheme.dark().accentPrimary, - sendButtonColor: StreamColorTheme.dark().accentPrimary, - sendButtonIdleColor: StreamColorTheme.dark().disabled, - inputBackgroundColor: StreamColorTheme.dark().barsBg, - inputTextStyle: StreamTextTheme.dark().body, + actionButtonColor: const StreamColorTheme.dark().accentPrimary, + actionButtonIdleColor: const StreamColorTheme.dark().textLowEmphasis, + expandButtonColor: const StreamColorTheme.dark().accentPrimary, + sendButtonColor: const StreamColorTheme.dark().accentPrimary, + sendButtonIdleColor: const StreamColorTheme.dark().disabled, + inputBackgroundColor: const StreamColorTheme.dark().barsBg, + inputTextStyle: const StreamTextTheme.dark().body, idleBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - StreamColorTheme.dark().disabled, - StreamColorTheme.dark().disabled, + const StreamColorTheme.dark().disabled, + const StreamColorTheme.dark().disabled, ], ), activeBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - StreamColorTheme.dark().disabled, - StreamColorTheme.dark().disabled, + const StreamColorTheme.dark().disabled, + const StreamColorTheme.dark().disabled, ], ), ); diff --git a/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart index ebcc8fbccb..8123029912 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart @@ -142,7 +142,7 @@ void main() { } final _messageListViewThemeDataControl = StreamMessageListViewThemeData( - backgroundColor: StreamColorTheme.light().barsBg, + backgroundColor: const StreamColorTheme.light().appBg, ); const _messageListViewThemeDataControlHalfLerp = StreamMessageListViewThemeData( @@ -150,7 +150,7 @@ const _messageListViewThemeDataControlHalfLerp = StreamMessageListViewThemeData( ); final _messageListViewThemeDataControlDark = StreamMessageListViewThemeData( - backgroundColor: StreamColorTheme.dark().barsBg, + backgroundColor: const StreamColorTheme.dark().appBg, ); const _messageListViewThemeDataImage = StreamMessageListViewThemeData( diff --git a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart index 246d9d9070..96df8a86f6 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart @@ -35,21 +35,21 @@ void main() { } final _messageThemeControl = StreamMessageThemeData( - messageAuthorStyle: StreamTextTheme.light().footnote.copyWith( - color: StreamColorTheme.light().textLowEmphasis, + messageAuthorStyle: const StreamTextTheme.light().footnote.copyWith( + color: const StreamColorTheme.light().textLowEmphasis, ), - messageTextStyle: StreamTextTheme.light().body, - createdAtStyle: StreamTextTheme.light().footnote.copyWith( - color: StreamColorTheme.light().textLowEmphasis, + messageTextStyle: const StreamTextTheme.light().body, + createdAtStyle: const StreamTextTheme.light().footnote.copyWith( + color: const StreamColorTheme.light().textLowEmphasis, ), - repliesStyle: StreamTextTheme.light().footnoteBold.copyWith( - color: StreamColorTheme.light().accentPrimary, + repliesStyle: const StreamTextTheme.light().footnoteBold.copyWith( + color: const StreamColorTheme.light().accentPrimary, ), - messageBackgroundColor: StreamColorTheme.light().disabled, - reactionsBackgroundColor: StreamColorTheme.light().barsBg, - reactionsBorderColor: StreamColorTheme.light().borders, - reactionsMaskColor: StreamColorTheme.light().appBg, - messageBorderColor: StreamColorTheme.light().disabled, + messageBackgroundColor: const StreamColorTheme.light().disabled, + reactionsBackgroundColor: const StreamColorTheme.light().barsBg, + reactionsBorderColor: const StreamColorTheme.light().borders, + reactionsMaskColor: const StreamColorTheme.light().appBg, + messageBorderColor: const StreamColorTheme.light().disabled, avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -58,27 +58,27 @@ final _messageThemeControl = StreamMessageThemeData( ), ), messageLinksStyle: TextStyle( - color: StreamColorTheme.light().accentPrimary, + color: const StreamColorTheme.light().accentPrimary, ), - urlAttachmentBackgroundColor: StreamColorTheme.light().linkBg, + urlAttachmentBackgroundColor: const StreamColorTheme.light().linkBg, ); final _messageThemeControlDark = StreamMessageThemeData( - messageAuthorStyle: StreamTextTheme.dark().footnote.copyWith( - color: StreamColorTheme.dark().textLowEmphasis, + messageAuthorStyle: const StreamTextTheme.dark().footnote.copyWith( + color: const StreamColorTheme.dark().textLowEmphasis, ), - messageTextStyle: StreamTextTheme.dark().body, - createdAtStyle: StreamTextTheme.dark().footnote.copyWith( - color: StreamColorTheme.dark().textLowEmphasis, + messageTextStyle: const StreamTextTheme.dark().body, + createdAtStyle: const StreamTextTheme.dark().footnote.copyWith( + color: const StreamColorTheme.dark().textLowEmphasis, ), - repliesStyle: StreamTextTheme.dark().footnoteBold.copyWith( - color: StreamColorTheme.dark().accentPrimary, + repliesStyle: const StreamTextTheme.dark().footnoteBold.copyWith( + color: const StreamColorTheme.dark().accentPrimary, ), - messageBackgroundColor: StreamColorTheme.dark().disabled, - reactionsBackgroundColor: StreamColorTheme.dark().barsBg, - reactionsBorderColor: StreamColorTheme.dark().borders, - reactionsMaskColor: StreamColorTheme.dark().appBg, - messageBorderColor: StreamColorTheme.dark().disabled, + messageBackgroundColor: const StreamColorTheme.dark().disabled, + reactionsBackgroundColor: const StreamColorTheme.dark().barsBg, + reactionsBorderColor: const StreamColorTheme.dark().borders, + reactionsMaskColor: const StreamColorTheme.dark().appBg, + messageBorderColor: const StreamColorTheme.dark().disabled, avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -87,7 +87,7 @@ final _messageThemeControlDark = StreamMessageThemeData( ), ), messageLinksStyle: TextStyle( - color: StreamColorTheme.dark().accentPrimary, + color: const StreamColorTheme.dark().accentPrimary, ), - urlAttachmentBackgroundColor: StreamColorTheme.dark().linkBg, + urlAttachmentBackgroundColor: const StreamColorTheme.dark().linkBg, ); diff --git a/packages/stream_chat_flutter/test/src/utils/extension_test.dart b/packages/stream_chat_flutter/test/src/utils/extension_test.dart index 4536611fbe..18f157a436 100644 --- a/packages/stream_chat_flutter/test/src/utils/extension_test.dart +++ b/packages/stream_chat_flutter/test/src/utils/extension_test.dart @@ -272,124 +272,4 @@ void main() { expect(modifiedMessage.text, isNot(contains('@Alice'))); }); }); - - group('Message List Extension Tests', () { - group('lastUnreadMessage', () { - test('should return null when list is empty', () { - final messages = []; - final userRead = Read( - lastRead: DateTime.now(), - user: User(id: 'user1'), - ); - expect(messages.lastUnreadMessage(userRead), isNull); - }); - - test('should return null when userRead is null', () { - final messages = [ - Message(id: '1'), - Message(id: '2'), - ]; - expect(messages.lastUnreadMessage(null), isNull); - }); - - test('should return null when all messages are read', () { - final lastRead = DateTime.now(); - final messages = [ - Message( - id: '1', - createdAt: lastRead.subtract(const Duration(seconds: 1))), - Message(id: '2', createdAt: lastRead), - ]; - final userRead = Read( - lastRead: lastRead, - user: User(id: 'user1'), - ); - expect(messages.lastUnreadMessage(userRead), isNull); - }); - - test('should return null when all messages are mine', () { - final lastRead = DateTime.now(); - final userRead = Read( - lastRead: lastRead, - user: User(id: 'user1'), - ); - final messages = [ - Message( - id: '1', - user: userRead.user, - createdAt: lastRead.add(const Duration(seconds: 1))), - Message(id: '2', user: userRead.user, createdAt: lastRead), - ]; - expect(messages.lastUnreadMessage(userRead), isNull); - }); - - test('should return the message', () { - final lastRead = DateTime.now(); - final otherUser = User(id: 'user2'); - final userRead = Read( - lastRead: lastRead, - user: User(id: 'user1'), - ); - - final messages = [ - Message( - id: '1', - user: otherUser, - createdAt: lastRead.add(const Duration(seconds: 2)), - ), - Message( - id: '2', - user: otherUser, - createdAt: lastRead.add(const Duration(seconds: 1)), - ), - Message( - id: '3', - user: otherUser, - createdAt: lastRead.subtract(const Duration(seconds: 1)), - ), - ]; - - final lastUnreadMessage = messages.lastUnreadMessage(userRead); - expect(lastUnreadMessage, isNotNull); - expect(lastUnreadMessage!.id, '2'); - }); - - test('should not return the last message read', () { - final lastRead = DateTime.timestamp(); - final otherUser = User(id: 'user2'); - final userRead = Read( - lastRead: lastRead, - user: User(id: 'user1'), - lastReadMessageId: '3', - ); - - final messages = [ - Message( - id: '1', - user: otherUser, - createdAt: lastRead.add(const Duration(seconds: 2)), - ), - Message( - id: '2', - user: otherUser, - createdAt: lastRead.add(const Duration(milliseconds: 1)), - ), - Message( - id: '3', - user: otherUser, - createdAt: lastRead.add(const Duration(microseconds: 1)), - ), - Message( - id: '4', - user: otherUser, - createdAt: lastRead.subtract(const Duration(seconds: 1)), - ), - ]; - - final lastUnreadMessage = messages.lastUnreadMessage(userRead); - expect(lastUnreadMessage, isNotNull); - expect(lastUnreadMessage!.id, '2'); - }); - }); - }); } diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 56bb82fd16..f404e4b18e 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,7 @@ +## 10.0.0-beta.5 + +- Included the changes from version [`9.16.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + ## 9.16.0 ๐Ÿž Fixed @@ -8,6 +12,10 @@ - Added methods for paginating thread replies in `StreamChannel`. +## 10.0.0-beta.4 + +- Included the changes from version [`9.15.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + ## 9.15.0 โœ… Added @@ -22,6 +30,10 @@ - Ensure `StreamChannel` future builder completes after channel initialization. [[#2323]](https://github.com/GetStream/stream-chat-flutter/issues/2323) +## 10.0.0-beta.3 + +- Included the changes from version [`9.14.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + ## 9.14.0 ๐Ÿž Fixed @@ -29,6 +41,10 @@ - Fixed cached messages are cleared from channels with unread messages when accessed offline. [[#2083]](https://github.com/GetStream/stream-chat-flutter/issues/2083) +## 10.0.0-beta.2 + +- Included the changes from version [`9.13.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + ## 9.13.0 ๐Ÿž Fixed @@ -36,6 +52,10 @@ - Fixed pagination end detection logic to properly determine when the top or bottom of the message list has been reached. +## 10.0.0-beta.1 + +- Updated `stream_chat` dependency to [`10.0.0-beta.1`](https://pub.dev/packages/stream_chat/changelog). + ## 9.12.0 โœ… Added diff --git a/packages/stream_chat_flutter_core/example/pubspec.yaml b/packages/stream_chat_flutter_core/example/pubspec.yaml index 6ef4b76c9e..895edf7f5e 100644 --- a/packages/stream_chat_flutter_core/example/pubspec.yaml +++ b/packages/stream_chat_flutter_core/example/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat_flutter_core: ^9.16.0 + stream_chat_flutter_core: ^10.0.0-beta.5 flutter: uses-material-design: true diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 09cb42f5c6..d7b0a6c56c 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 9.16.0 +version: 10.0.0-beta.5 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -31,7 +31,7 @@ dependencies: meta: ^1.9.1 package_info_plus: ^8.3.0 rxdart: ^0.28.0 - stream_chat: ^9.16.0 + stream_chat: ^10.0.0-beta.5 dev_dependencies: build_runner: ^2.4.9 diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 7c959f6a3c..e50ce703b1 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,19 +1,41 @@ +## 10.0.0-beta.5 + +- Included the changes from version [`9.16.0`](https://pub.dev/packages/stream_chat_localizations/changelog). + ## 9.16.0 - Updated `stream_chat_flutter` dependency to [`9.16.0`](https://pub.dev/packages/stream_chat_flutter/changelog). +## 10.0.0-beta.4 + +- Added translations for new `locationLabel` label. + +- Included the changes from version [`9.15.0`](https://pub.dev/packages/stream_chat_localizations/changelog). + ## 9.15.0 - Updated `stream_chat_flutter` dependency to [`9.15.0`](https://pub.dev/packages/stream_chat_flutter/changelog). +## 10.0.0-beta.3 + +- Included the changes from version [`9.14.0`](https://pub.dev/packages/stream_chat_localizations/changelog). + ## 9.14.0 - Updated `stream_chat_flutter` dependency to [`9.14.0`](https://pub.dev/packages/stream_chat_flutter/changelog). +## 10.0.0-beta.2 + +- Included the changes from version [`9.13.0`](https://pub.dev/packages/stream_chat_localizations/changelog). + ## 9.13.0 - Updated `stream_chat_flutter` dependency to [`9.13.0`](https://pub.dev/packages/stream_chat_flutter/changelog). +## 10.0.0-beta.1 + +- Updated `stream_chat_flutter` dependency to [`10.0.0-beta.1`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 9.12.0 - Updated `stream_chat_flutter` dependency to [`9.12.0`](https://pub.dev/packages/stream_chat_flutter/changelog). diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index aa65d95ce9..0921a52519 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -692,6 +692,12 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String get draftLabel => 'Draft'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ Live Location'; + return '๐Ÿ“ Location'; + } } void main() async { diff --git a/packages/stream_chat_localizations/example/pubspec.yaml b/packages/stream_chat_localizations/example/pubspec.yaml index b93a773d09..9186fcfbfc 100644 --- a/packages/stream_chat_localizations/example/pubspec.yaml +++ b/packages/stream_chat_localizations/example/pubspec.yaml @@ -24,8 +24,8 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat_flutter: ^9.16.0 - stream_chat_localizations: ^9.16.0 + stream_chat_flutter: ^10.0.0-beta.5 + stream_chat_localizations: ^10.0.0-beta.5 flutter: uses-material-design: true \ No newline at end of file diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart index ced86d92f1..eb5894392c 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ca.dart @@ -1,3 +1,5 @@ +// ignore_for_file: lines_longer_than_80_chars + part of 'stream_chat_localizations.dart'; /// The translations for Catalan (`ca`). @@ -172,8 +174,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { @override String get enablePhotoAndVideoAccessMessage => - "Si us plau, permet l'accรฉs a les teves fotos" - '\ni vรญdeos per a que puguis compartir-los'; + "Si us plau, permet l'accรฉs a les teves fotos i vรญdeos per a que puguis compartir-los"; @override String get allowGalleryAccessMessage => "Permet l'accรฉs a la galeria"; @@ -183,8 +184,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { @override String get flagMessageQuestion => - "Vols enviar una cรฒpia d'aquest missatge a un" - '\nmoderador per una major investigaciรณ?'; + "Vols enviar una cรฒpia d'aquest missatge a un moderador per una major investigaciรณ?"; @override String get flagLabel => 'REPORTA'; @@ -207,7 +207,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { @override String get deleteMessageQuestion => - 'Estร s segur que vols esborrar aquest\nmissatge de forma permanent?'; + 'Estร s segur que vols esborrar aquest missatge de forma permanent?'; @override String get operationCouldNotBeCompletedText => @@ -449,8 +449,8 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String unreadMessagesSeparatorText() => 'Missatges nous'; @override - String get enableFileAccessMessage => "Habilita l'accรฉs als fitxers" - '\nper poder compartir-los amb amics'; + String get enableFileAccessMessage => + "Habilita l'accรฉs als fitxers per poder compartir-los amb amics"; @override String get allowFileAccessMessage => "Permet l'accรฉs als fitxers"; @@ -674,4 +674,10 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { @override String get draftLabel => 'Esborrany'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ Ubicaciรณ en directe'; + return '๐Ÿ“ Ubicaciรณ'; + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart index 92d7643b5a..910eb7270b 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_de.dart @@ -1,3 +1,5 @@ +// ignore_for_file: lines_longer_than_80_chars + part of 'stream_chat_localizations.dart'; /// The translations for German (`de`). @@ -162,8 +164,7 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { @override String get enablePhotoAndVideoAccessMessage => - 'Bitte aktivieren Sie den Zugriff auf Ihre Fotos' - '\nund Videos, damit Sie sie mit Freunden teilen kรถnnen.'; + 'Bitte aktivieren Sie den Zugriff auf Ihre Fotos und Videos, damit Sie sie mit Freunden teilen kรถnnen.'; @override String get allowGalleryAccessMessage => 'Zugang zu Ihrer Galerie gewรคhren'; @@ -173,8 +174,7 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { @override String get flagMessageQuestion => - 'Mรถchten Sie eine Kopie dieser Nachricht an einen' - '\nModerator fรผr weitere Untersuchungen senden?'; + 'Mรถchten Sie eine Kopie dieser Nachricht an einen Moderator fรผr weitere Untersuchungen senden?'; @override String get flagLabel => 'MELDEN'; @@ -443,8 +443,7 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { @override String get enableFileAccessMessage => - 'Bitte aktivieren Sie den Zugriff auf Dateien,' - '\ndamit Sie sie mit Freunden teilen kรถnnen.'; + 'Bitte aktivieren Sie den Zugriff auf Dateien, damit Sie sie mit Freunden teilen kรถnnen.'; @override String get allowFileAccessMessage => 'Zugriff auf Dateien zulassen'; @@ -667,4 +666,10 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { @override String get draftLabel => 'Entwurf'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ Live-Standort'; + return '๐Ÿ“ Standort'; + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index a8e28a10e5..9b2a00263a 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -1,3 +1,5 @@ +// ignore_for_file: lines_longer_than_80_chars + part of 'stream_chat_localizations.dart'; /// The translations for English (`en`). @@ -169,8 +171,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get enablePhotoAndVideoAccessMessage => - 'Please enable access to your photos' - '\nand videos so you can share them with friends.'; + 'Please enable access to your photos and videos so you can share them with friends.'; @override String get allowGalleryAccessMessage => 'Allow access to your gallery'; @@ -180,8 +181,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get flagMessageQuestion => - 'Do you want to send a copy of this message to a' - '\nmoderator for further investigation?'; + 'Do you want to send a copy of this message to a moderator for further investigation?'; @override String get flagLabel => 'FLAG'; @@ -204,7 +204,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get deleteMessageQuestion => - 'Are you sure you want to permanently delete this\nmessage?'; + 'Are you sure you want to permanently delete this message?'; @override String get operationCouldNotBeCompletedText => @@ -446,8 +446,8 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String unreadMessagesSeparatorText() => 'New messages'; @override - String get enableFileAccessMessage => 'Please enable access to files' - '\nso you can share them with friends.'; + String get enableFileAccessMessage => + 'Please enable access to files so you can share them with friends.'; @override String get allowFileAccessMessage => 'Allow access to files'; @@ -669,4 +669,10 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get draftLabel => 'Draft'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ Live Location'; + return '๐Ÿ“ Location'; + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index 887ebb58e9..4362dfc712 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -1,3 +1,5 @@ +// ignore_for_file: lines_longer_than_80_chars + part of 'stream_chat_localizations.dart'; /// The translations for Spanish (`es`). @@ -173,8 +175,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { @override String get enablePhotoAndVideoAccessMessage => - 'Por favor, permita el acceso a sus fotos' - '\ny vรญdeos para que pueda compartirlos con sus amigos.'; + 'Por favor, permita el acceso a sus fotos y vรญdeos para que pueda compartirlos con sus amigos.'; @override String get allowGalleryAccessMessage => 'Permitir el acceso a su galerรญa'; @@ -184,8 +185,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { @override String get flagMessageQuestion => - 'ยฟQuiere enviar una copia de este mensaje a un' - '\nmoderador para una mayor investigaciรณn?'; + 'ยฟQuiere enviar una copia de este mensaje a un moderador para una mayor investigaciรณn?'; @override String get flagLabel => 'REPORTAR'; @@ -208,7 +208,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { @override String get deleteMessageQuestion => - 'ยฟEstรกs seguro de que quieres borrar este\nmensaje de forma permanente?'; + 'ยฟEstรกs seguro de que quieres borrar este mensaje de forma permanente?'; @override String get operationCouldNotBeCompletedText => @@ -451,8 +451,8 @@ No es posible aรฑadir mรกs de $limit archivos adjuntos String unreadMessagesSeparatorText() => 'Nuevos mensajes'; @override - String get enableFileAccessMessage => 'Habilite el acceso a los archivos' - '\npara poder compartirlos con amigos.'; + String get enableFileAccessMessage => + 'Habilite el acceso a los archivos para poder compartirlos con amigos.'; @override String get allowFileAccessMessage => 'Permitir el acceso a los archivos'; @@ -676,4 +676,10 @@ No es posible aรฑadir mรกs de $limit archivos adjuntos @override String get draftLabel => 'Borrador'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ Ubicaciรณn en vivo'; + return '๐Ÿ“ Ubicaciรณn'; + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 09f4c0ecb3..7af6c9d6bf 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -1,3 +1,5 @@ +// ignore_for_file: lines_longer_than_80_chars + part of 'stream_chat_localizations.dart'; /// The translations for French (`fr`). @@ -172,8 +174,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { @override String get enablePhotoAndVideoAccessMessage => - "Veuillez autoriser l'accรจs ร  vos photos" - '\net vidรฉos afin de pouvoir les partager avec vos amis.'; + "Veuillez autoriser l'accรจs ร  vos photos et vidรฉos afin de pouvoir les partager avec vos amis."; @override String get allowGalleryAccessMessage => "Autoriser l'accรจs ร  votre galerie"; @@ -183,8 +184,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { @override String get flagMessageQuestion => - 'Voulez-vous envoyer une copie de ce message ร  un' - '\nmodรฉrateur pour une enquรชte plus approfondie ?'; + 'Voulez-vous envoyer une copie de ce message ร  un modรฉrateur pour une enquรชte plus approfondie ?'; @override String get flagLabel => 'SIGNALER'; @@ -207,7 +207,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { @override String get deleteMessageQuestion => - 'รŠtes-vous sรปr de vouloir supprimer dรฉfinitivement ce\nmessage ?'; + 'รŠtes-vous sรปr de vouloir supprimer dรฉfinitivement ce message ?'; @override String get operationCouldNotBeCompletedText => @@ -451,8 +451,7 @@ Limite de piรจces jointes dรฉpassรฉe : il n'est pas possible d'ajouter plus de $ @override String get enableFileAccessMessage => - "Veuillez autoriser l'accรจs aux fichiers" - '\nafin de pouvoir les partager avec des amis.'; + "Veuillez autoriser l'accรจs aux fichiers afin de pouvoir les partager avec des amis."; @override String get allowFileAccessMessage => "Autoriser l'accรจs aux fichiers"; @@ -679,4 +678,10 @@ Limite de piรจces jointes dรฉpassรฉe : il n'est pas possible d'ajouter plus de $ @override String get draftLabel => 'Brouillon'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ Position en direct'; + return '๐Ÿ“ Position'; + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 5ba1eb4b26..3302b54f7e 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -1,3 +1,5 @@ +// ignore_for_file: lines_longer_than_80_chars + part of 'stream_chat_localizations.dart'; /// The translations for Hindi (`hi`). @@ -167,8 +169,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get enablePhotoAndVideoAccessMessage => - 'เค•เฅƒเคชเคฏเคพ เค…เคชเคจเฅ‡ เคซเคผเฅ‹เคŸเฅ‹ เค”เคฐ เคตเฅ€เคกเคฟเคฏเฅ‹ เคคเค• เคชเคนเฅเค‚เคš เคธเค•เฅเคทเคฎ เค•เคฐเฅ‡เค‚' - '\nเคคเคพเค•เคฟ เค†เคช เค‰เคจเฅเคนเฅ‡เค‚ เคฎเคฟเคคเฅเคฐเฅ‹เค‚ เค•เฅ‡ เคธเคพเคฅ เคธเคพเคเคพ เค•เคฐ เคธเค•เฅ‡เค‚เฅค'; + 'เค•เฅƒเคชเคฏเคพ เค…เคชเคจเฅ‡ เคซเคผเฅ‹เคŸเฅ‹ เค”เคฐ เคตเฅ€เคกเคฟเคฏเฅ‹ เคคเค• เคชเคนเฅเค‚เคš เคธเค•เฅเคทเคฎ เค•เคฐเฅ‡ เคคเคพเค•เคฟ เค†เคช เค‰เคจเฅเคนเฅ‡เค‚ เคฎเคฟเคคเฅเคฐเฅ‹เค‚ เค•เฅ‡ เคธเคพเคฅ เคธเคพเคเคพ เค•เคฐ เคธเค•เฅ‡เค‚เฅค'; @override String get allowGalleryAccessMessage => 'เค…เคชเคจเฅ€ เค—เฅˆเคฒเคฐเฅ€ เคคเค• เคชเคนเฅเค‚เคš เค•เฅ€ เค…เคจเฅเคฎเคคเคฟ เคฆเฅ‡เค‚'; @@ -177,8 +178,8 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get flagMessageLabel => 'เคซเฅเคฒเฅˆเค— เคธเค‚เคฆเฅ‡เคถ'; @override - String get flagMessageQuestion => 'เค•เฅเคฏเคพ เค†เคช เค†เค—เฅ‡ เค•เฅ€ เคœเคพเค‚เคš เค•เฅ‡ เคฒเคฟเค เค‡เคธ เคธเค‚เคฆเฅ‡เคถ เค•เฅ€' - '\nเคเค• เคชเฅเคฐเคคเคฟ เคฎเฅ‰เคกเคฐเฅ‡เคŸเคฐ เค•เฅ‹ เคญเฅ‡เคœเคจเคพ เคšเคพเคนเคคเฅ‡ เคนเฅˆเค‚?'; + String get flagMessageQuestion => + 'เค•เฅเคฏเคพ เค†เคช เค†เค—เฅ‡ เค•เฅ€ เคœเคพเค‚เคš เค•เฅ‡ เคฒเคฟเค เค‡เคธ เคธเค‚เคฆเฅ‡เคถ เค•เฅ€ เคเค• เคชเฅเคฐเคคเคฟ เคฎเฅ‰เคกเคฐเฅ‡เคŸเคฐ เค•เฅ‹ เคญเฅ‡เคœเคจเคพ เคšเคพเคนเคคเฅ‡ เคนเฅˆเค‚?'; @override String get flagLabel => 'เคซเฅเคฒเฅˆเค—'; @@ -201,7 +202,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get deleteMessageQuestion => - 'เค•เฅเคฏเคพ เค†เคช เคตเคพเค•เคˆ เค‡เคธ เคธเค‚เคฆเฅ‡เคถ เค•เฅ‹ เคธเฅเคฅเคพเคฏเฅ€ เคฐเฅ‚เคช เคธเฅ‡\nเคนเคŸเคพเคจเคพ เคšเคพเคนเคคเฅ‡ เคนเฅˆเค‚?'; + 'เค•เฅเคฏเคพ เค†เคช เคตเคพเค•เคˆ เค‡เคธ เคธเค‚เคฆเฅ‡เคถ เค•เฅ‹ เคธเฅเคฅเคพเคฏเฅ€ เคฐเฅ‚เคช เคธเฅ‡ เคนเคŸเคพเคจเคพ เคšเคพเคนเคคเฅ‡ เคนเฅˆเค‚?'; @override String get operationCouldNotBeCompletedText => @@ -444,8 +445,8 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String unreadMessagesSeparatorText() => 'เคจเค เคธเค‚เคฆเฅ‡เคถเฅค'; @override - String get enableFileAccessMessage => 'เค•เฅƒเคชเคฏเคพ เคซเคผเคพเค‡เคฒเฅ‹เค‚ เคคเค• เคชเคนเฅเค‚เคš เคธเค•เฅเคทเคฎ เค•เคฐเฅ‡เค‚ เคคเคพเค•เคฟ' - '\nเค†เคช เค‰เคจเฅเคนเฅ‡เค‚ เคฎเคฟเคคเฅเคฐเฅ‹เค‚ เค•เฅ‡ เคธเคพเคฅ เคธเคพเคเคพ เค•เคฐ เคธเค•เฅ‡เค‚เฅค'; + String get enableFileAccessMessage => + 'เค•เฅƒเคชเคฏเคพ เคซเคผเคพเค‡เคฒเฅ‹เค‚ เคคเค• เคชเคนเฅเค‚เคš เคธเค•เฅเคทเคฎ เค•เคฐเฅ‡เค‚ เคคเคพเค•เคฟ เค†เคช เค‰เคจเฅเคนเฅ‡เค‚ เคฎเคฟเคคเฅเคฐเฅ‹เค‚ เค•เฅ‡ เคธเคพเคฅ เคธเคพเคเคพ เค•เคฐ เคธเค•เฅ‡เค‚เฅค'; @override String get allowFileAccessMessage => 'เคซเคพเค‡เคฒเฅ‹เค‚ เคคเค• เคชเคนเฅเค‚เคš เค•เฅ€ เค…เคจเฅเคฎเคคเคฟ เคฆเฅ‡เค‚'; @@ -670,4 +671,10 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get draftLabel => 'เคกเฅเคฐเคพเคซเฅเคŸ'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ เคฒเคพเค‡เคต เคฒเฅ‹เค•เฅ‡เคถเคจ'; + return '๐Ÿ“ เคฒเฅ‹เค•เฅ‡เคถเคจ'; + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index 04c3a500db..a32a46ff04 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -1,3 +1,5 @@ +// ignore_for_file: lines_longer_than_80_chars + part of 'stream_chat_localizations.dart'; /// The translations for Italian (`it`). @@ -176,8 +178,7 @@ Il file รจ troppo grande per essere caricato. Il limite รจ di $limitInMB MB.'''; @override String get enablePhotoAndVideoAccessMessage => - "Per favore attiva l'accesso alle foto" - '\ne ai video cosรญ potrai condividerli con i tuoi amici.'; + "Per favore attiva l'accesso alle foto e ai video cosรญ potrai condividerli con i tuoi amici."; @override String get allowGalleryAccessMessage => "Permetti l'accesso alla galleria"; @@ -186,8 +187,8 @@ Il file รจ troppo grande per essere caricato. Il limite รจ di $limitInMB MB.'''; String get flagMessageLabel => 'Segnala messaggio'; @override - String get flagMessageQuestion => 'Vuoi mandare una copia di questo messaggio' - '\nad un moderatore?'; + String get flagMessageQuestion => + 'Vuoi mandare una copia di questo messaggio ad un moderatore?'; @override String get flagLabel => 'SEGNALA'; @@ -210,7 +211,7 @@ Il file รจ troppo grande per essere caricato. Il limite รจ di $limitInMB MB.'''; @override String get deleteMessageQuestion => - 'Sei sicuro di voler definitivamente cancellare questo\nmessaggio?'; + 'Sei sicuro di voler definitivamente cancellare questo messaggio?'; @override String get operationCouldNotBeCompletedText => @@ -453,8 +454,8 @@ Attenzione: il limite massimo di $limit file รจ stato superato. String unreadMessagesSeparatorText() => 'Nouveaux messages'; @override - String get enableFileAccessMessage => "Per favore attiva l'accesso ai file" - '\ncosรญ potrai condividerli con i tuoi amici.'; + String get enableFileAccessMessage => + "Per favore attiva l'accesso ai file cosรญ potrai condividerli con i tuoi amici."; @override String get allowFileAccessMessage => "Consenti l'accesso ai file"; @@ -679,4 +680,10 @@ Attenzione: il limite massimo di $limit file รจ stato superato. @override String get draftLabel => 'Bozza'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ Posizione dal vivo'; + return '๐Ÿ“ Posizione'; + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index d2a55c4c83..34fef9844b 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -163,8 +163,8 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get addMoreFilesLabel => 'ใƒ•ใ‚กใ‚คใƒซใฎ่ฟฝๅŠ '; @override - String get enablePhotoAndVideoAccessMessage => 'ใŠๅ‹้”ใจๅ…ฑๆœ‰ใงใใ‚‹ใ‚ˆใ†ใซใ€ๅ†™็œŸ' - '\nใ‚„ใƒ“ใƒ‡ใ‚ชใธใฎใ‚ขใ‚ฏใ‚ปใ‚นใ‚’ๆœ‰ๅŠนใซใ—ใฆใใ ใ•ใ„ใ€‚'; + String get enablePhotoAndVideoAccessMessage => + 'ใŠๅ‹้”ใจๅ…ฑๆœ‰ใงใใ‚‹ใ‚ˆใ†ใซใ€ๅ†™็œŸใ‚„ใƒ“ใƒ‡ใ‚ชใธใฎใ‚ขใ‚ฏใ‚ปใ‚นใ‚’ๆœ‰ๅŠนใซใ—ใฆใใ ใ•ใ„ใ€‚'; @override String get allowGalleryAccessMessage => 'ใ‚ฎใƒฃใƒฉใƒชใƒผใธใฎใ‚ขใ‚ฏใ‚ปใ‚นใ‚’่จฑๅฏใ™ใ‚‹'; @@ -172,8 +172,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get flagMessageLabel => 'ใƒกใƒƒใ‚ปใƒผใ‚ธใ‚’ใƒ•ใƒฉใ‚ฐใ™ใ‚‹'; @override - String get flagMessageQuestion => 'ใ“ใฎใƒกใƒƒใ‚ปใƒผใ‚ธใฎใ‚ณใƒ”ใƒผใ‚’' - '\nใƒขใƒ‡ใƒฌใƒผใ‚ฟใƒผใซ้€ใฃใฆใ€ใ•ใ‚‰ใซ่ชฟๆŸปใ—ใฆใ‚‚ใ‚‰ใ„ใพใ™ใ‹๏ผŸ'; + String get flagMessageQuestion => 'ใ“ใฎใƒกใƒƒใ‚ปใƒผใ‚ธใฎใ‚ณใƒ”ใƒผใ‚’ใƒขใƒ‡ใƒฌใƒผใ‚ฟใƒผใซ้€ใฃใฆใ€ใ•ใ‚‰ใซ่ชฟๆŸปใ—ใฆใ‚‚ใ‚‰ใ„ใพใ™ใ‹๏ผŸ'; @override String get flagLabel => 'ใƒ•ใƒฉใ‚ฐใ™ใ‚‹'; @@ -194,8 +193,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get deleteMessageLabel => 'ใƒกใƒƒใ‚ปใƒผใ‚ธใ‚’ๅ‰Š้™คใ™ใ‚‹'; @override - String get deleteMessageQuestion => 'ใ“ใฎใƒกใƒƒใ‚ปใƒผใ‚ธ' - '\nใ‚’ๅฎŒๅ…จใซๅ‰Š้™คใ—ใฆใ‚‚ใ‚ˆใ‚ใ—ใ„ใงใ™ใ‹๏ผŸ'; + String get deleteMessageQuestion => 'ใ“ใฎใƒกใƒƒใ‚ปใƒผใ‚ธใ‚’ๅฎŒๅ…จใซๅ‰Š้™คใ—ใฆใ‚‚ใ‚ˆใ‚ใ—ใ„ใงใ™ใ‹๏ผŸ'; @override String get operationCouldNotBeCompletedText => 'ๆ“ไฝœใ‚’ๅฎŒไบ†ใงใใพใ›ใ‚“ใงใ—ใŸใ€‚'; @@ -429,8 +427,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String unreadMessagesSeparatorText() => 'ๆ–ฐใ—ใ„ใƒกใƒƒใ‚ปใƒผใ‚ธใ€‚'; @override - String get enableFileAccessMessage => - 'ๅ‹้”ใจๅ…ฑๆœ‰ใงใใ‚‹ใ‚ˆใ†ใซใ€' '\nใƒ•ใ‚กใ‚คใƒซใธใฎใ‚ขใ‚ฏใ‚ปใ‚นใ‚’ๆœ‰ๅŠนใซใ—ใฆใใ ใ•ใ„ใ€‚'; + String get enableFileAccessMessage => 'ๅ‹้”ใจๅ…ฑๆœ‰ใงใใ‚‹ใ‚ˆใ†ใซใ€ใƒ•ใ‚กใ‚คใƒซใธใฎใ‚ขใ‚ฏใ‚ปใ‚นใ‚’ๆœ‰ๅŠนใซใ—ใฆใใ ใ•ใ„ใ€‚'; @override String get allowFileAccessMessage => 'ใƒ•ใ‚กใ‚คใƒซใธใฎใ‚ขใ‚ฏใ‚ปใ‚นใ‚’่จฑๅฏใ™ใ‚‹'; @@ -648,4 +645,10 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String get draftLabel => 'ไธ‹ๆ›ธใ'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ ใƒฉใ‚คใƒ–ไฝ็ฝฎๆƒ…ๅ ฑ'; + return '๐Ÿ“ ไฝ็ฝฎๆƒ…ๅ ฑ'; + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 816f6d129b..3fec27ffa4 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -163,8 +163,8 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get addMoreFilesLabel => 'ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•จ'; @override - String get enablePhotoAndVideoAccessMessage => '์นœ๊ตฌ์™€ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์‚ฌ์ง„๊ณผ' - '\n๋™์˜์ƒ์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•˜์‹ญ์‹œ์˜ค.'; + String get enablePhotoAndVideoAccessMessage => + '์นœ๊ตฌ์™€ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋„๋ก ์‚ฌ์ง„๊ณผ ๋™์˜์ƒ์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค์ •ํ•˜์‹ญ์‹œ์˜ค.'; @override String get allowGalleryAccessMessage => '๊ฐค๋Ÿฌ๋ฆฌ์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค'; @@ -649,4 +649,10 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String get draftLabel => '์ž„์‹œ๊ธ€'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ ์‹ค์‹œ๊ฐ„ ์œ„์น˜'; + return '๐Ÿ“ ์œ„์น˜'; + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart index cb8b2c80ab..a02387c433 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_no.dart @@ -1,3 +1,5 @@ +// ignore_for_file: lines_longer_than_80_chars + part of 'stream_chat_localizations.dart'; /// The translations for Norwegian (`no`). @@ -165,8 +167,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { @override String get enablePhotoAndVideoAccessMessage => - 'Vennligst gi tillatelse til dine bilder' - '\nog videoer sรฅ du kan dele de med dine venner.'; + 'Vennligst gi tillatelse til dine bilder og videoer sรฅ du kan dele de med dine venner.'; @override String get allowGalleryAccessMessage => 'Tillat tilgang til galleri'; @@ -176,8 +177,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { @override String get flagMessageQuestion => - 'ร˜nsker du รฅ sende en kopi av denne meldingen til en' - '\nmoderator for videre undersรธkelser'; + 'ร˜nsker du รฅ sende en kopi av denne meldingen til en moderator for videre undersรธkelser'; @override String get flagLabel => 'RAPPORTER'; @@ -423,7 +423,6 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { @override String toggleMuteUnmuteUserQuestion({required bool isMuted}) { if (isMuted) { - // ignore: lines_longer_than_80_chars return 'Er du sikker pรฅ at du vil oppheve ignoreringen av denne brukeren?'; } return 'Er du sikker pรฅ at du vil ignorere denne brukeren?'; @@ -437,7 +436,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { @override String get enableFileAccessMessage => - 'Aktiver tilgang til filer slik' '\nat du kan dele dem med venner.'; + 'Aktiver tilgang til filer slik at du kan dele dem med venner.'; @override String get allowFileAccessMessage => 'Gi tilgang til filer'; @@ -660,4 +659,10 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { @override String get draftLabel => 'Utkast'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ Direkte posisjon'; + return '๐Ÿ“ Posisjon'; + } } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart index 2fd941c773..24508b70e1 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart @@ -1,3 +1,5 @@ +// ignore_for_file: lines_longer_than_80_chars + part of 'stream_chat_localizations.dart'; /// The translations for Portuguese (`pt`). @@ -168,8 +170,7 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { @override String get enablePhotoAndVideoAccessMessage => - 'Por favor, permita o acesso ร s suas fotos' - '\ne vรญdeos para que possa compartilhar com sua rede.'; + 'Por favor, permita o acesso ร s suas fotos e vรญdeos para que possa compartilhar com sua rede.'; @override String get allowGalleryAccessMessage => 'Permitir acesso ร  sua galeria'; @@ -178,8 +179,8 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String get flagMessageLabel => 'Denunciar mensagem'; @override - String get flagMessageQuestion => 'Gostaria de enviar esta mensagem ao' - '\nmoderador para maior investigaรงรฃo?'; + String get flagMessageQuestion => + 'Gostaria de enviar esta mensagem ao moderador para maior investigaรงรฃo?'; @override String get flagLabel => 'DENUNCIAR'; @@ -202,7 +203,7 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { @override String get deleteMessageQuestion => - 'Vocรช tem certeza que deseja apagar essa\nmensagem permanentemente?'; + 'Vocรช tem certeza que deseja apagar essa mensagem permanentemente?'; @override String get operationCouldNotBeCompletedText => @@ -450,7 +451,7 @@ Nรฃo รฉ possรญvel adicionar mais de $limit arquivos de uma vez @override String get enableFileAccessMessage => - 'Ative o acesso aos arquivos' '\npara poder compartilhรก-los com amigos.'; + 'Ative o acesso aos arquivos para poder compartilhรก-los com amigos.'; @override String get allowFileAccessMessage => 'Permitir acesso aos arquivos'; @@ -673,4 +674,10 @@ Nรฃo รฉ possรญvel adicionar mais de $limit arquivos de uma vez @override String get draftLabel => 'Rascunho'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '๐Ÿ“ Localizaรงรฃo ao Vivo'; + return '๐Ÿ“ Localizaรงรฃo'; + } } diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index dac768e007..85bd34ec7f 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -1,6 +1,6 @@ name: stream_chat_localizations description: The Official localizations for Stream Chat Flutter, a service for building chat applications -version: 9.16.0 +version: 10.0.0-beta.5 homepage: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -26,7 +26,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - stream_chat_flutter: ^9.16.0 + stream_chat_flutter: ^10.0.0-beta.5 dev_dependencies: flutter_test: diff --git a/packages/stream_chat_localizations/test/translations_test.dart b/packages/stream_chat_localizations/test/translations_test.dart index 4f11894f86..18757b4371 100644 --- a/packages/stream_chat_localizations/test/translations_test.dart +++ b/packages/stream_chat_localizations/test/translations_test.dart @@ -309,6 +309,8 @@ void main() { expect(localizations.pollYouCreatedText, isNotNull); expect(localizations.pollSomeoneCreatedText('TestUser'), isNotNull); expect(localizations.systemMessageLabel, isNotNull); + expect(localizations.draftLabel, isNotNull); + expect(localizations.locationLabel(), isNotNull); }); } diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 599aec84fc..d502d510a3 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,7 +1,18 @@ +## 10.0.0-beta.5 + +- Included the changes from version [`9.16.0`](https://pub.dev/packages/stream_chat_persistence/changelog). + ## 9.16.0 - Updated `stream_chat` dependency to [`9.16.0`](https://pub.dev/packages/stream_chat/changelog). +## 10.0.0-beta.4 + +- Added support for `Location` entity in the database. +- Added support for `emojiCode` and `updatedAt` fields in `Reaction` entity. + +- Included the changes from version [`9.15.0`](https://pub.dev/packages/stream_chat_persistence/changelog). + ## 9.15.0 ๐Ÿž Fixed @@ -13,14 +24,26 @@ - Added support for `User.avgResponseTime` field. +## 10.0.0-beta.3 + +- Included the changes from version [`9.14.0`](https://pub.dev/packages/stream_chat_persistence/changelog). + ## 9.14.0 - Updated `stream_chat` dependency to [`9.14.0`](https://pub.dev/packages/stream_chat/changelog). +## 10.0.0-beta.2 + +- Included the changes from version [`9.13.0`](https://pub.dev/packages/stream_chat_persistence/changelog). + ## 9.13.0 - Updated `stream_chat` dependency to [`9.13.0`](https://pub.dev/packages/stream_chat/changelog). +## 10.0.0-beta.1 + +- Updated `stream_chat` dependency to [`10.0.0-beta.1`](https://pub.dev/packages/stream_chat/changelog). + ## 9.12.0 - Updated `stream_chat` dependency to [`9.12.0`](https://pub.dev/packages/stream_chat/changelog). diff --git a/packages/stream_chat_persistence/example/pubspec.yaml b/packages/stream_chat_persistence/example/pubspec.yaml index 5114d7e411..8f7b003b2c 100644 --- a/packages/stream_chat_persistence/example/pubspec.yaml +++ b/packages/stream_chat_persistence/example/pubspec.yaml @@ -23,8 +23,8 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat: ^9.16.0 - stream_chat_persistence: ^9.16.0 + stream_chat: ^10.0.0-beta.5 + stream_chat_persistence: ^10.0.0-beta.5 flutter: uses-material-design: true \ No newline at end of file diff --git a/packages/stream_chat_persistence/lib/src/dao/dao.dart b/packages/stream_chat_persistence/lib/src/dao/dao.dart index d0aab7b212..d2f088c399 100644 --- a/packages/stream_chat_persistence/lib/src/dao/dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/dao.dart @@ -2,6 +2,7 @@ export 'channel_dao.dart'; export 'channel_query_dao.dart'; export 'connection_event_dao.dart'; export 'draft_message_dao.dart'; +export 'location_dao.dart'; export 'member_dao.dart'; export 'message_dao.dart'; export 'pinned_message_dao.dart'; diff --git a/packages/stream_chat_persistence/lib/src/dao/draft_message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/draft_message_dao.dart index b5a5cb6fc9..5065b3bda4 100644 --- a/packages/stream_chat_persistence/lib/src/dao/draft_message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/draft_message_dao.dart @@ -18,18 +18,27 @@ class DraftMessageDao extends DatabaseAccessor final DriftChatDatabase _db; Future _draftFromEntity(DraftMessageEntity entity) async { - // We do not want to fetch the draft message of the parent and quoted - // message because it will create a circular dependency and will + // We do not want to fetch the draft and shared location of the parent and + // quoted message because it will create a circular dependency and will // result in infinite loop. const fetchDraft = false; + const fetchSharedLocation = false; final parentMessage = await switch (entity.parentId) { - final id? => _db.messageDao.getMessageById(id, fetchDraft: fetchDraft), + final id? => _db.messageDao.getMessageById( + id, + fetchDraft: fetchDraft, + fetchSharedLocation: fetchSharedLocation, + ), _ => null, }; final quotedMessage = await switch (entity.quotedMessageId) { - final id? => _db.messageDao.getMessageById(id, fetchDraft: fetchDraft), + final id? => _db.messageDao.getMessageById( + id, + fetchDraft: fetchDraft, + fetchSharedLocation: fetchSharedLocation, + ), _ => null, }; diff --git a/packages/stream_chat_persistence/lib/src/dao/location_dao.dart b/packages/stream_chat_persistence/lib/src/dao/location_dao.dart new file mode 100644 index 0000000000..6e12421179 --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/dao/location_dao.dart @@ -0,0 +1,83 @@ +// ignore_for_file: join_return_with_assignment + +import 'package:drift/drift.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; +import 'package:stream_chat_persistence/src/entity/locations.dart'; +import 'package:stream_chat_persistence/src/mapper/mapper.dart'; + +part 'location_dao.g.dart'; + +/// The Data Access Object for operations in [Locations] table. +@DriftAccessor(tables: [Locations]) +class LocationDao extends DatabaseAccessor + with _$LocationDaoMixin { + /// Creates a new location dao instance + LocationDao(this._db) : super(_db); + + final DriftChatDatabase _db; + + Future _locationFromEntity(LocationEntity entity) async { + // We do not want to fetch the location of the parent and quoted + // message because it will create a circular dependency and will + // result in infinite loop. + const fetchDraft = false; + const fetchSharedLocation = false; + + final channel = await switch (entity.channelCid) { + final cid? => db.channelDao.getChannelByCid(cid), + _ => null, + }; + + final message = await switch (entity.messageId) { + final id? => _db.messageDao.getMessageById( + id, + fetchDraft: fetchDraft, + fetchSharedLocation: fetchSharedLocation, + ), + _ => null, + }; + + return entity.toLocation( + channel: channel, + message: message, + ); + } + + /// Get all locations for a channel + Future> getLocationsByCid(String cid) async { + final query = select(locations)..where((tbl) => tbl.channelCid.equals(cid)); + + final result = await query.map(_locationFromEntity).get(); + return Future.wait(result); + } + + /// Get location by message ID + Future getLocationByMessageId(String messageId) async { + final query = select(locations) // + ..where((tbl) => tbl.messageId.equals(messageId)); + + final result = await query.getSingleOrNull(); + if (result == null) return null; + + return _locationFromEntity(result); + } + + /// Update multiple locations + Future updateLocations(List locationList) { + return batch( + (it) => it.insertAllOnConflictUpdate( + locations, + locationList.map((it) => it.toEntity()), + ), + ); + } + + /// Delete locations by channel ID + Future deleteLocationsByCid(String cid) => + (delete(locations)..where((tbl) => tbl.channelCid.equals(cid))).go(); + + /// Delete locations by message IDs + Future deleteLocationsByMessageIds(List messageIds) => + (delete(locations)..where((tbl) => tbl.messageId.isIn(messageIds))).go(); +} diff --git a/packages/stream_chat_persistence/lib/src/dao/location_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/location_dao.g.dart new file mode 100644 index 0000000000..240b3b4b83 --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/dao/location_dao.g.dart @@ -0,0 +1,10 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'location_dao.dart'; + +// ignore_for_file: type=lint +mixin _$LocationDaoMixin on DatabaseAccessor { + $ChannelsTable get channels => attachedDatabase.channels; + $MessagesTable get messages => attachedDatabase.messages; + $LocationsTable get locations => attachedDatabase.locations; +} diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart index 77d84dff07..1b7681a02c 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -39,6 +39,7 @@ class MessageDao extends DatabaseAccessor Future _messageFromJoinRow( TypedResult rows, { bool fetchDraft = false, + bool fetchSharedLocation = false, }) async { final userEntity = rows.readTableOrNull(_users); final pinnedByEntity = rows.readTableOrNull(_pinnedByUsers); @@ -67,6 +68,11 @@ class MessageDao extends DatabaseAccessor _ => null, }; + final sharedLocation = await switch (fetchSharedLocation) { + true => _db.locationDao.getLocationByMessageId(msgEntity.id), + _ => null, + }; + return msgEntity.toMessage( user: userEntity?.toUser(), pinnedBy: pinnedByEntity?.toUser(), @@ -75,6 +81,7 @@ class MessageDao extends DatabaseAccessor quotedMessage: quotedMessage, poll: poll, draft: draft, + sharedLocation: sharedLocation, ); } @@ -85,6 +92,7 @@ class MessageDao extends DatabaseAccessor Future getMessageById( String id, { bool fetchDraft = true, + bool fetchSharedLocation = true, }) async { final query = select(messages).join([ leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), @@ -101,6 +109,7 @@ class MessageDao extends DatabaseAccessor return _messageFromJoinRow( result, fetchDraft: fetchDraft, + fetchSharedLocation: fetchSharedLocation, ); } @@ -169,6 +178,7 @@ class MessageDao extends DatabaseAccessor Future> getMessagesByCid( String cid, { bool fetchDraft = true, + bool fetchSharedLocation = true, PaginationParams? messagePagination, }) async { final query = select(messages).join([ @@ -190,6 +200,7 @@ class MessageDao extends DatabaseAccessor (row) => _messageFromJoinRow( row, fetchDraft: fetchDraft, + fetchSharedLocation: fetchSharedLocation, ), ), ); diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart index 7992accf5d..56823f57eb 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart @@ -38,6 +38,7 @@ class PinnedMessageDao extends DatabaseAccessor Future _messageFromJoinRow( TypedResult rows, { bool fetchDraft = false, + bool fetchSharedLocation = false, }) async { final userEntity = rows.readTableOrNull(_users); final pinnedByEntity = rows.readTableOrNull(_pinnedByUsers); @@ -68,6 +69,11 @@ class PinnedMessageDao extends DatabaseAccessor _ => null, }; + final sharedLocation = await switch (fetchSharedLocation) { + true => _db.locationDao.getLocationByMessageId(msgEntity.id), + _ => null, + }; + return msgEntity.toMessage( user: userEntity?.toUser(), pinnedBy: pinnedByEntity?.toUser(), @@ -76,6 +82,7 @@ class PinnedMessageDao extends DatabaseAccessor quotedMessage: quotedMessage, poll: poll, draft: draft, + sharedLocation: sharedLocation, ); } @@ -83,6 +90,7 @@ class PinnedMessageDao extends DatabaseAccessor Future getMessageById( String id, { bool fetchDraft = true, + bool fetchSharedLocation = true, }) async { final query = select(pinnedMessages).join([ leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), @@ -99,6 +107,7 @@ class PinnedMessageDao extends DatabaseAccessor return _messageFromJoinRow( result, fetchDraft: fetchDraft, + fetchSharedLocation: fetchSharedLocation, ); } @@ -166,6 +175,7 @@ class PinnedMessageDao extends DatabaseAccessor Future> getMessagesByCid( String cid, { bool fetchDraft = true, + bool fetchSharedLocation = true, PaginationParams? messagePagination, }) async { final query = select(pinnedMessages).join([ @@ -188,6 +198,7 @@ class PinnedMessageDao extends DatabaseAccessor (row) => _messageFromJoinRow( row, fetchDraft: fetchDraft, + fetchSharedLocation: fetchSharedLocation, ), ), ); diff --git a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart index 5dc89d8a79..aa8cd3ef71 100644 --- a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart @@ -13,6 +13,7 @@ part 'drift_chat_database.g.dart'; tables: [ Channels, DraftMessages, + Locations, Messages, PinnedMessages, Polls, @@ -30,6 +31,7 @@ part 'drift_chat_database.g.dart'; ChannelDao, MessageDao, DraftMessageDao, + LocationDao, PinnedMessageDao, PinnedMessageReactionDao, MemberDao, @@ -55,7 +57,7 @@ class DriftChatDatabase extends _$DriftChatDatabase { // you should bump this number whenever you change or add a table definition. @override - int get schemaVersion => 23; + int get schemaVersion => 1000 + 24; @override MigrationStrategy get migration => MigrationStrategy( diff --git a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart index 15faa9ab6e..2ec585f138 100644 --- a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart @@ -2764,960 +2764,684 @@ class DraftMessagesCompanion extends UpdateCompanion { } } -class $PinnedMessagesTable extends PinnedMessages - with TableInfo<$PinnedMessagesTable, PinnedMessageEntity> { +class $LocationsTable extends Locations + with TableInfo<$LocationsTable, LocationEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $PinnedMessagesTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _messageTextMeta = - const VerificationMeta('messageText'); - @override - late final GeneratedColumn messageText = GeneratedColumn( - 'message_text', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - late final GeneratedColumnWithTypeConverter, String> - attachments = GeneratedColumn('attachments', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>( - $PinnedMessagesTable.$converterattachments); - static const VerificationMeta _stateMeta = const VerificationMeta('state'); - @override - late final GeneratedColumn state = GeneratedColumn( - 'state', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _typeMeta = const VerificationMeta('type'); + $LocationsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _channelCidMeta = + const VerificationMeta('channelCid'); @override - late final GeneratedColumn type = GeneratedColumn( - 'type', aliasedName, false, + late final GeneratedColumn channelCid = GeneratedColumn( + 'channel_cid', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, - defaultValue: const Constant('regular')); - @override - late final GeneratedColumnWithTypeConverter, String> - mentionedUsers = GeneratedColumn( - 'mentioned_users', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>( - $PinnedMessagesTable.$convertermentionedUsers); - @override - late final GeneratedColumnWithTypeConverter?, - String> reactionGroups = GeneratedColumn( - 'reaction_groups', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $PinnedMessagesTable.$converterreactionGroupsn); - static const VerificationMeta _parentIdMeta = - const VerificationMeta('parentId'); - @override - late final GeneratedColumn parentId = GeneratedColumn( - 'parent_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _quotedMessageIdMeta = - const VerificationMeta('quotedMessageId'); - @override - late final GeneratedColumn quotedMessageId = GeneratedColumn( - 'quoted_message_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _pollIdMeta = const VerificationMeta('pollId'); - @override - late final GeneratedColumn pollId = GeneratedColumn( - 'poll_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _replyCountMeta = - const VerificationMeta('replyCount'); - @override - late final GeneratedColumn replyCount = GeneratedColumn( - 'reply_count', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - static const VerificationMeta _showInChannelMeta = - const VerificationMeta('showInChannel'); - @override - late final GeneratedColumn showInChannel = GeneratedColumn( - 'show_in_channel', aliasedName, true, - type: DriftSqlType.bool, - requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("show_in_channel" IN (0, 1))')); - static const VerificationMeta _shadowedMeta = - const VerificationMeta('shadowed'); + 'REFERENCES channels (cid) ON DELETE CASCADE')); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); @override - late final GeneratedColumn shadowed = GeneratedColumn( - 'shadowed', aliasedName, false, - type: DriftSqlType.bool, + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("shadowed" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _commandMeta = - const VerificationMeta('command'); - @override - late final GeneratedColumn command = GeneratedColumn( - 'command', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _localCreatedAtMeta = - const VerificationMeta('localCreatedAt'); - @override - late final GeneratedColumn localCreatedAt = - GeneratedColumn('local_created_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _remoteCreatedAtMeta = - const VerificationMeta('remoteCreatedAt'); - @override - late final GeneratedColumn remoteCreatedAt = - GeneratedColumn('remote_created_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _localUpdatedAtMeta = - const VerificationMeta('localUpdatedAt'); - @override - late final GeneratedColumn localUpdatedAt = - GeneratedColumn('local_updated_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _remoteUpdatedAtMeta = - const VerificationMeta('remoteUpdatedAt'); - @override - late final GeneratedColumn remoteUpdatedAt = - GeneratedColumn('remote_updated_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _localDeletedAtMeta = - const VerificationMeta('localDeletedAt'); - @override - late final GeneratedColumn localDeletedAt = - GeneratedColumn('local_deleted_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _remoteDeletedAtMeta = - const VerificationMeta('remoteDeletedAt'); - @override - late final GeneratedColumn remoteDeletedAt = - GeneratedColumn('remote_deleted_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _messageTextUpdatedAtMeta = - const VerificationMeta('messageTextUpdatedAt'); - @override - late final GeneratedColumn messageTextUpdatedAt = - GeneratedColumn('message_text_updated_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (id) ON DELETE CASCADE')); static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); @override late final GeneratedColumn userId = GeneratedColumn( 'user_id', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); - @override - late final GeneratedColumn pinned = GeneratedColumn( - 'pinned', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _pinnedAtMeta = - const VerificationMeta('pinnedAt'); - @override - late final GeneratedColumn pinnedAt = GeneratedColumn( - 'pinned_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _pinExpiresMeta = - const VerificationMeta('pinExpires'); + static const VerificationMeta _latitudeMeta = + const VerificationMeta('latitude'); + @override + late final GeneratedColumn latitude = GeneratedColumn( + 'latitude', aliasedName, false, + type: DriftSqlType.double, requiredDuringInsert: true); + static const VerificationMeta _longitudeMeta = + const VerificationMeta('longitude'); + @override + late final GeneratedColumn longitude = GeneratedColumn( + 'longitude', aliasedName, false, + type: DriftSqlType.double, requiredDuringInsert: true); + static const VerificationMeta _createdByDeviceIdMeta = + const VerificationMeta('createdByDeviceId'); + @override + late final GeneratedColumn createdByDeviceId = + GeneratedColumn('created_by_device_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _endAtMeta = const VerificationMeta('endAt'); @override - late final GeneratedColumn pinExpires = GeneratedColumn( - 'pin_expires', aliasedName, true, + late final GeneratedColumn endAt = GeneratedColumn( + 'end_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _pinnedByUserIdMeta = - const VerificationMeta('pinnedByUserId'); - @override - late final GeneratedColumn pinnedByUserId = GeneratedColumn( - 'pinned_by_user_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _channelCidMeta = - const VerificationMeta('channelCid'); - @override - late final GeneratedColumn channelCid = GeneratedColumn( - 'channel_cid', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - @override - late final GeneratedColumnWithTypeConverter?, String> - i18n = GeneratedColumn('i18n', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $PinnedMessagesTable.$converteri18n); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); @override - late final GeneratedColumnWithTypeConverter?, String> - restrictedVisibility = GeneratedColumn( - 'restricted_visibility', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $PinnedMessagesTable.$converterrestrictedVisibilityn); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _updatedAtMeta = + const VerificationMeta('updatedAt'); @override - late final GeneratedColumnWithTypeConverter?, String> - extraData = GeneratedColumn('extra_data', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $PinnedMessagesTable.$converterextraDatan); + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); @override List get $columns => [ - id, - messageText, - attachments, - state, - type, - mentionedUsers, - reactionGroups, - parentId, - quotedMessageId, - pollId, - replyCount, - showInChannel, - shadowed, - command, - localCreatedAt, - remoteCreatedAt, - localUpdatedAt, - remoteUpdatedAt, - localDeletedAt, - remoteDeletedAt, - messageTextUpdatedAt, - userId, - pinned, - pinnedAt, - pinExpires, - pinnedByUserId, channelCid, - i18n, - restrictedVisibility, - extraData + messageId, + userId, + latitude, + longitude, + createdByDeviceId, + endAt, + createdAt, + updatedAt ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'pinned_messages'; + static const String $name = 'locations'; @override - VerificationContext validateIntegrity( - Insertable instance, + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } else if (isInserting) { - context.missing(_idMeta); - } - if (data.containsKey('message_text')) { + if (data.containsKey('channel_cid')) { context.handle( - _messageTextMeta, - messageText.isAcceptableOrUnknown( - data['message_text']!, _messageTextMeta)); + _channelCidMeta, + channelCid.isAcceptableOrUnknown( + data['channel_cid']!, _channelCidMeta)); } - if (data.containsKey('state')) { - context.handle( - _stateMeta, state.isAcceptableOrUnknown(data['state']!, _stateMeta)); - } else if (isInserting) { - context.missing(_stateMeta); - } - if (data.containsKey('type')) { - context.handle( - _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); - } - if (data.containsKey('parent_id')) { - context.handle(_parentIdMeta, - parentId.isAcceptableOrUnknown(data['parent_id']!, _parentIdMeta)); - } - if (data.containsKey('quoted_message_id')) { - context.handle( - _quotedMessageIdMeta, - quotedMessageId.isAcceptableOrUnknown( - data['quoted_message_id']!, _quotedMessageIdMeta)); - } - if (data.containsKey('poll_id')) { - context.handle(_pollIdMeta, - pollId.isAcceptableOrUnknown(data['poll_id']!, _pollIdMeta)); - } - if (data.containsKey('reply_count')) { - context.handle( - _replyCountMeta, - replyCount.isAcceptableOrUnknown( - data['reply_count']!, _replyCountMeta)); - } - if (data.containsKey('show_in_channel')) { - context.handle( - _showInChannelMeta, - showInChannel.isAcceptableOrUnknown( - data['show_in_channel']!, _showInChannelMeta)); - } - if (data.containsKey('shadowed')) { - context.handle(_shadowedMeta, - shadowed.isAcceptableOrUnknown(data['shadowed']!, _shadowedMeta)); - } - if (data.containsKey('command')) { - context.handle(_commandMeta, - command.isAcceptableOrUnknown(data['command']!, _commandMeta)); - } - if (data.containsKey('local_created_at')) { - context.handle( - _localCreatedAtMeta, - localCreatedAt.isAcceptableOrUnknown( - data['local_created_at']!, _localCreatedAtMeta)); - } - if (data.containsKey('remote_created_at')) { - context.handle( - _remoteCreatedAtMeta, - remoteCreatedAt.isAcceptableOrUnknown( - data['remote_created_at']!, _remoteCreatedAtMeta)); - } - if (data.containsKey('local_updated_at')) { - context.handle( - _localUpdatedAtMeta, - localUpdatedAt.isAcceptableOrUnknown( - data['local_updated_at']!, _localUpdatedAtMeta)); - } - if (data.containsKey('remote_updated_at')) { - context.handle( - _remoteUpdatedAtMeta, - remoteUpdatedAt.isAcceptableOrUnknown( - data['remote_updated_at']!, _remoteUpdatedAtMeta)); - } - if (data.containsKey('local_deleted_at')) { - context.handle( - _localDeletedAtMeta, - localDeletedAt.isAcceptableOrUnknown( - data['local_deleted_at']!, _localDeletedAtMeta)); - } - if (data.containsKey('remote_deleted_at')) { - context.handle( - _remoteDeletedAtMeta, - remoteDeletedAt.isAcceptableOrUnknown( - data['remote_deleted_at']!, _remoteDeletedAtMeta)); - } - if (data.containsKey('message_text_updated_at')) { - context.handle( - _messageTextUpdatedAtMeta, - messageTextUpdatedAt.isAcceptableOrUnknown( - data['message_text_updated_at']!, _messageTextUpdatedAtMeta)); + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); } if (data.containsKey('user_id')) { context.handle(_userIdMeta, userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); } - if (data.containsKey('pinned')) { - context.handle(_pinnedMeta, - pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); + if (data.containsKey('latitude')) { + context.handle(_latitudeMeta, + latitude.isAcceptableOrUnknown(data['latitude']!, _latitudeMeta)); + } else if (isInserting) { + context.missing(_latitudeMeta); } - if (data.containsKey('pinned_at')) { - context.handle(_pinnedAtMeta, - pinnedAt.isAcceptableOrUnknown(data['pinned_at']!, _pinnedAtMeta)); + if (data.containsKey('longitude')) { + context.handle(_longitudeMeta, + longitude.isAcceptableOrUnknown(data['longitude']!, _longitudeMeta)); + } else if (isInserting) { + context.missing(_longitudeMeta); } - if (data.containsKey('pin_expires')) { + if (data.containsKey('created_by_device_id')) { context.handle( - _pinExpiresMeta, - pinExpires.isAcceptableOrUnknown( - data['pin_expires']!, _pinExpiresMeta)); + _createdByDeviceIdMeta, + createdByDeviceId.isAcceptableOrUnknown( + data['created_by_device_id']!, _createdByDeviceIdMeta)); } - if (data.containsKey('pinned_by_user_id')) { + if (data.containsKey('end_at')) { context.handle( - _pinnedByUserIdMeta, - pinnedByUserId.isAcceptableOrUnknown( - data['pinned_by_user_id']!, _pinnedByUserIdMeta)); + _endAtMeta, endAt.isAcceptableOrUnknown(data['end_at']!, _endAtMeta)); } - if (data.containsKey('channel_cid')) { - context.handle( - _channelCidMeta, - channelCid.isAcceptableOrUnknown( - data['channel_cid']!, _channelCidMeta)); - } else if (isInserting) { - context.missing(_channelCidMeta); + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); } return context; } @override - Set get $primaryKey => {id}; + Set get $primaryKey => {messageId}; @override - PinnedMessageEntity map(Map data, {String? tablePrefix}) { + LocationEntity map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return PinnedMessageEntity( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - messageText: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}message_text']), - attachments: $PinnedMessagesTable.$converterattachments.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}attachments'])!), - state: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}state'])!, - type: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}type'])!, - mentionedUsers: $PinnedMessagesTable.$convertermentionedUsers.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}mentioned_users'])!), - reactionGroups: $PinnedMessagesTable.$converterreactionGroupsn.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}reaction_groups'])), - parentId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}parent_id']), - quotedMessageId: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}quoted_message_id']), - pollId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}poll_id']), - replyCount: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}reply_count']), - showInChannel: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}show_in_channel']), - shadowed: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}shadowed'])!, - command: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}command']), - localCreatedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}local_created_at']), - remoteCreatedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}remote_created_at']), - localUpdatedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}local_updated_at']), - remoteUpdatedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}remote_updated_at']), - localDeletedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}local_deleted_at']), - remoteDeletedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}remote_deleted_at']), - messageTextUpdatedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, - data['${effectivePrefix}message_text_updated_at']), + return LocationEntity( + channelCid: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}channel_cid']), + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id']), userId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}user_id']), - pinned: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, - pinnedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}pinned_at']), - pinExpires: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}pin_expires']), - pinnedByUserId: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}pinned_by_user_id']), - channelCid: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, - i18n: $PinnedMessagesTable.$converteri18n.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}i18n'])), - restrictedVisibility: $PinnedMessagesTable.$converterrestrictedVisibilityn - .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, - data['${effectivePrefix}restricted_visibility'])), - extraData: $PinnedMessagesTable.$converterextraDatan.fromSql( - attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), + latitude: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}latitude'])!, + longitude: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}longitude'])!, + createdByDeviceId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}created_by_device_id']), + endAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}end_at']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, ); } @override - $PinnedMessagesTable createAlias(String alias) { - return $PinnedMessagesTable(attachedDatabase, alias); + $LocationsTable createAlias(String alias) { + return $LocationsTable(attachedDatabase, alias); } - - static TypeConverter, String> $converterattachments = - ListConverter(); - static TypeConverter, String> $convertermentionedUsers = - ListConverter(); - static TypeConverter, String> - $converterreactionGroups = ReactionGroupsConverter(); - static TypeConverter?, String?> - $converterreactionGroupsn = - NullAwareTypeConverter.wrap($converterreactionGroups); - static TypeConverter?, String?> $converteri18n = - NullableMapConverter(); - static TypeConverter, String> $converterrestrictedVisibility = - ListConverter(); - static TypeConverter?, String?> $converterrestrictedVisibilityn = - NullAwareTypeConverter.wrap($converterrestrictedVisibility); - static TypeConverter, String> $converterextraData = - MapConverter(); - static TypeConverter?, String?> $converterextraDatan = - NullAwareTypeConverter.wrap($converterextraData); } -class PinnedMessageEntity extends DataClass - implements Insertable { - /// The message id - final String id; - - /// The text of this message - final String? messageText; - - /// The list of attachments, either provided by the user - /// or generated from a command or as a result of URL scraping. - final List attachments; +class LocationEntity extends DataClass implements Insertable { + /// The channel CID where the location is shared + final String? channelCid; - /// The current state of the message. - final String state; + /// The ID of the message that contains this shared location + final String? messageId; - /// The message type - final String type; + /// The ID of the user who shared the location + final String? userId; - /// The list of user mentioned in the message - final List mentionedUsers; + /// The latitude of the shared location + final double latitude; - /// A map describing the reaction group for every reaction - final Map? reactionGroups; + /// The longitude of the shared location + final double longitude; - /// The ID of the parent message, if the message is a thread reply. - final String? parentId; + /// The ID of the device that created the location + final String? createdByDeviceId; - /// The ID of the quoted message, if the message is a quoted reply. - final String? quotedMessageId; + /// The date at which the shared location will end (for live locations) + /// If null, this is a static location + final DateTime? endAt; - /// The ID of the poll, if the message is a poll. - final String? pollId; + /// The date at which the location was created + final DateTime createdAt; - /// Number of replies for this message. - final int? replyCount; + /// The date at which the location was last updated + final DateTime updatedAt; + const LocationEntity( + {this.channelCid, + this.messageId, + this.userId, + required this.latitude, + required this.longitude, + this.createdByDeviceId, + this.endAt, + required this.createdAt, + required this.updatedAt}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || channelCid != null) { + map['channel_cid'] = Variable(channelCid); + } + if (!nullToAbsent || messageId != null) { + map['message_id'] = Variable(messageId); + } + if (!nullToAbsent || userId != null) { + map['user_id'] = Variable(userId); + } + map['latitude'] = Variable(latitude); + map['longitude'] = Variable(longitude); + if (!nullToAbsent || createdByDeviceId != null) { + map['created_by_device_id'] = Variable(createdByDeviceId); + } + if (!nullToAbsent || endAt != null) { + map['end_at'] = Variable(endAt); + } + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + return map; + } - /// Check if this message needs to show in the channel. - final bool? showInChannel; - - /// If true the message is shadowed - final bool shadowed; - - /// A used command name. - final String? command; - - /// The DateTime on which the message was created on the client. - final DateTime? localCreatedAt; - - /// The DateTime on which the message was created on the server. - final DateTime? remoteCreatedAt; - - /// The DateTime on which the message was updated on the client. - final DateTime? localUpdatedAt; - - /// The DateTime on which the message was updated on the server. - final DateTime? remoteUpdatedAt; - - /// The DateTime on which the message was deleted on the client. - final DateTime? localDeletedAt; - - /// The DateTime on which the message was deleted on the server. - final DateTime? remoteDeletedAt; - - /// The DateTime at which the message text was edited - final DateTime? messageTextUpdatedAt; - - /// Id of the User who sent the message - final String? userId; - - /// Whether the message is pinned or not - final bool pinned; - - /// The DateTime at which the message was pinned - final DateTime? pinnedAt; + factory LocationEntity.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return LocationEntity( + channelCid: serializer.fromJson(json['channelCid']), + messageId: serializer.fromJson(json['messageId']), + userId: serializer.fromJson(json['userId']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + createdByDeviceId: + serializer.fromJson(json['createdByDeviceId']), + endAt: serializer.fromJson(json['endAt']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'channelCid': serializer.toJson(channelCid), + 'messageId': serializer.toJson(messageId), + 'userId': serializer.toJson(userId), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'createdByDeviceId': serializer.toJson(createdByDeviceId), + 'endAt': serializer.toJson(endAt), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + }; + } - /// The DateTime on which the message pin expires - final DateTime? pinExpires; + LocationEntity copyWith( + {Value channelCid = const Value.absent(), + Value messageId = const Value.absent(), + Value userId = const Value.absent(), + double? latitude, + double? longitude, + Value createdByDeviceId = const Value.absent(), + Value endAt = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt}) => + LocationEntity( + channelCid: channelCid.present ? channelCid.value : this.channelCid, + messageId: messageId.present ? messageId.value : this.messageId, + userId: userId.present ? userId.value : this.userId, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + createdByDeviceId: createdByDeviceId.present + ? createdByDeviceId.value + : this.createdByDeviceId, + endAt: endAt.present ? endAt.value : this.endAt, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + LocationEntity copyWithCompanion(LocationsCompanion data) { + return LocationEntity( + channelCid: + data.channelCid.present ? data.channelCid.value : this.channelCid, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + userId: data.userId.present ? data.userId.value : this.userId, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + createdByDeviceId: data.createdByDeviceId.present + ? data.createdByDeviceId.value + : this.createdByDeviceId, + endAt: data.endAt.present ? data.endAt.value : this.endAt, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ); + } - /// Id of the User who pinned the message - final String? pinnedByUserId; + @override + String toString() { + return (StringBuffer('LocationEntity(') + ..write('channelCid: $channelCid, ') + ..write('messageId: $messageId, ') + ..write('userId: $userId, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('createdByDeviceId: $createdByDeviceId, ') + ..write('endAt: $endAt, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt') + ..write(')')) + .toString(); + } - /// The channel cid of which this message is part of - final String channelCid; + @override + int get hashCode => Object.hash(channelCid, messageId, userId, latitude, + longitude, createdByDeviceId, endAt, createdAt, updatedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is LocationEntity && + other.channelCid == this.channelCid && + other.messageId == this.messageId && + other.userId == this.userId && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.createdByDeviceId == this.createdByDeviceId && + other.endAt == this.endAt && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt); +} - /// A Map of [messageText] translations. - final Map? i18n; +class LocationsCompanion extends UpdateCompanion { + final Value channelCid; + final Value messageId; + final Value userId; + final Value latitude; + final Value longitude; + final Value createdByDeviceId; + final Value endAt; + final Value createdAt; + final Value updatedAt; + final Value rowid; + const LocationsCompanion({ + this.channelCid = const Value.absent(), + this.messageId = const Value.absent(), + this.userId = const Value.absent(), + this.latitude = const Value.absent(), + this.longitude = const Value.absent(), + this.createdByDeviceId = const Value.absent(), + this.endAt = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + LocationsCompanion.insert({ + this.channelCid = const Value.absent(), + this.messageId = const Value.absent(), + this.userId = const Value.absent(), + required double latitude, + required double longitude, + this.createdByDeviceId = const Value.absent(), + this.endAt = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.rowid = const Value.absent(), + }) : latitude = Value(latitude), + longitude = Value(longitude); + static Insertable custom({ + Expression? channelCid, + Expression? messageId, + Expression? userId, + Expression? latitude, + Expression? longitude, + Expression? createdByDeviceId, + Expression? endAt, + Expression? createdAt, + Expression? updatedAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (channelCid != null) 'channel_cid': channelCid, + if (messageId != null) 'message_id': messageId, + if (userId != null) 'user_id': userId, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (createdByDeviceId != null) 'created_by_device_id': createdByDeviceId, + if (endAt != null) 'end_at': endAt, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (rowid != null) 'rowid': rowid, + }); + } - /// The list of user ids that should be able to see the message. - final List? restrictedVisibility; + LocationsCompanion copyWith( + {Value? channelCid, + Value? messageId, + Value? userId, + Value? latitude, + Value? longitude, + Value? createdByDeviceId, + Value? endAt, + Value? createdAt, + Value? updatedAt, + Value? rowid}) { + return LocationsCompanion( + channelCid: channelCid ?? this.channelCid, + messageId: messageId ?? this.messageId, + userId: userId ?? this.userId, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + createdByDeviceId: createdByDeviceId ?? this.createdByDeviceId, + endAt: endAt ?? this.endAt, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + rowid: rowid ?? this.rowid, + ); + } - /// Message custom extraData - final Map? extraData; - const PinnedMessageEntity( - {required this.id, - this.messageText, - required this.attachments, - required this.state, - required this.type, - required this.mentionedUsers, - this.reactionGroups, - this.parentId, - this.quotedMessageId, - this.pollId, - this.replyCount, - this.showInChannel, - required this.shadowed, - this.command, - this.localCreatedAt, - this.remoteCreatedAt, - this.localUpdatedAt, - this.remoteUpdatedAt, - this.localDeletedAt, - this.remoteDeletedAt, - this.messageTextUpdatedAt, - this.userId, - required this.pinned, - this.pinnedAt, - this.pinExpires, - this.pinnedByUserId, - required this.channelCid, - this.i18n, - this.restrictedVisibility, - this.extraData}); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['id'] = Variable(id); - if (!nullToAbsent || messageText != null) { - map['message_text'] = Variable(messageText); - } - { - map['attachments'] = Variable( - $PinnedMessagesTable.$converterattachments.toSql(attachments)); - } - map['state'] = Variable(state); - map['type'] = Variable(type); - { - map['mentioned_users'] = Variable( - $PinnedMessagesTable.$convertermentionedUsers.toSql(mentionedUsers)); - } - if (!nullToAbsent || reactionGroups != null) { - map['reaction_groups'] = Variable( - $PinnedMessagesTable.$converterreactionGroupsn.toSql(reactionGroups)); - } - if (!nullToAbsent || parentId != null) { - map['parent_id'] = Variable(parentId); - } - if (!nullToAbsent || quotedMessageId != null) { - map['quoted_message_id'] = Variable(quotedMessageId); - } - if (!nullToAbsent || pollId != null) { - map['poll_id'] = Variable(pollId); - } - if (!nullToAbsent || replyCount != null) { - map['reply_count'] = Variable(replyCount); - } - if (!nullToAbsent || showInChannel != null) { - map['show_in_channel'] = Variable(showInChannel); - } - map['shadowed'] = Variable(shadowed); - if (!nullToAbsent || command != null) { - map['command'] = Variable(command); - } - if (!nullToAbsent || localCreatedAt != null) { - map['local_created_at'] = Variable(localCreatedAt); - } - if (!nullToAbsent || remoteCreatedAt != null) { - map['remote_created_at'] = Variable(remoteCreatedAt); - } - if (!nullToAbsent || localUpdatedAt != null) { - map['local_updated_at'] = Variable(localUpdatedAt); + if (channelCid.present) { + map['channel_cid'] = Variable(channelCid.value); } - if (!nullToAbsent || remoteUpdatedAt != null) { - map['remote_updated_at'] = Variable(remoteUpdatedAt); + if (messageId.present) { + map['message_id'] = Variable(messageId.value); } - if (!nullToAbsent || localDeletedAt != null) { - map['local_deleted_at'] = Variable(localDeletedAt); + if (userId.present) { + map['user_id'] = Variable(userId.value); } - if (!nullToAbsent || remoteDeletedAt != null) { - map['remote_deleted_at'] = Variable(remoteDeletedAt); + if (latitude.present) { + map['latitude'] = Variable(latitude.value); } - if (!nullToAbsent || messageTextUpdatedAt != null) { - map['message_text_updated_at'] = Variable(messageTextUpdatedAt); + if (longitude.present) { + map['longitude'] = Variable(longitude.value); } - if (!nullToAbsent || userId != null) { - map['user_id'] = Variable(userId); + if (createdByDeviceId.present) { + map['created_by_device_id'] = Variable(createdByDeviceId.value); } - map['pinned'] = Variable(pinned); - if (!nullToAbsent || pinnedAt != null) { - map['pinned_at'] = Variable(pinnedAt); + if (endAt.present) { + map['end_at'] = Variable(endAt.value); } - if (!nullToAbsent || pinExpires != null) { - map['pin_expires'] = Variable(pinExpires); + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); } - if (!nullToAbsent || pinnedByUserId != null) { - map['pinned_by_user_id'] = Variable(pinnedByUserId); + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); } - map['channel_cid'] = Variable(channelCid); - if (!nullToAbsent || i18n != null) { - map['i18n'] = - Variable($PinnedMessagesTable.$converteri18n.toSql(i18n)); - } - if (!nullToAbsent || restrictedVisibility != null) { - map['restricted_visibility'] = Variable($PinnedMessagesTable - .$converterrestrictedVisibilityn - .toSql(restrictedVisibility)); - } - if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $PinnedMessagesTable.$converterextraDatan.toSql(extraData)); + if (rowid.present) { + map['rowid'] = Variable(rowid.value); } return map; } - factory PinnedMessageEntity.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return PinnedMessageEntity( - id: serializer.fromJson(json['id']), - messageText: serializer.fromJson(json['messageText']), - attachments: serializer.fromJson>(json['attachments']), - state: serializer.fromJson(json['state']), - type: serializer.fromJson(json['type']), - mentionedUsers: serializer.fromJson>(json['mentionedUsers']), - reactionGroups: serializer - .fromJson?>(json['reactionGroups']), - parentId: serializer.fromJson(json['parentId']), - quotedMessageId: serializer.fromJson(json['quotedMessageId']), - pollId: serializer.fromJson(json['pollId']), - replyCount: serializer.fromJson(json['replyCount']), - showInChannel: serializer.fromJson(json['showInChannel']), - shadowed: serializer.fromJson(json['shadowed']), - command: serializer.fromJson(json['command']), - localCreatedAt: serializer.fromJson(json['localCreatedAt']), - remoteCreatedAt: serializer.fromJson(json['remoteCreatedAt']), - localUpdatedAt: serializer.fromJson(json['localUpdatedAt']), - remoteUpdatedAt: serializer.fromJson(json['remoteUpdatedAt']), - localDeletedAt: serializer.fromJson(json['localDeletedAt']), - remoteDeletedAt: serializer.fromJson(json['remoteDeletedAt']), - messageTextUpdatedAt: - serializer.fromJson(json['messageTextUpdatedAt']), - userId: serializer.fromJson(json['userId']), - pinned: serializer.fromJson(json['pinned']), - pinnedAt: serializer.fromJson(json['pinnedAt']), - pinExpires: serializer.fromJson(json['pinExpires']), - pinnedByUserId: serializer.fromJson(json['pinnedByUserId']), - channelCid: serializer.fromJson(json['channelCid']), - i18n: serializer.fromJson?>(json['i18n']), - restrictedVisibility: - serializer.fromJson?>(json['restrictedVisibility']), - extraData: serializer.fromJson?>(json['extraData']), - ); - } @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'messageText': serializer.toJson(messageText), - 'attachments': serializer.toJson>(attachments), - 'state': serializer.toJson(state), - 'type': serializer.toJson(type), - 'mentionedUsers': serializer.toJson>(mentionedUsers), - 'reactionGroups': - serializer.toJson?>(reactionGroups), - 'parentId': serializer.toJson(parentId), - 'quotedMessageId': serializer.toJson(quotedMessageId), - 'pollId': serializer.toJson(pollId), - 'replyCount': serializer.toJson(replyCount), - 'showInChannel': serializer.toJson(showInChannel), - 'shadowed': serializer.toJson(shadowed), - 'command': serializer.toJson(command), - 'localCreatedAt': serializer.toJson(localCreatedAt), - 'remoteCreatedAt': serializer.toJson(remoteCreatedAt), - 'localUpdatedAt': serializer.toJson(localUpdatedAt), - 'remoteUpdatedAt': serializer.toJson(remoteUpdatedAt), - 'localDeletedAt': serializer.toJson(localDeletedAt), - 'remoteDeletedAt': serializer.toJson(remoteDeletedAt), - 'messageTextUpdatedAt': - serializer.toJson(messageTextUpdatedAt), - 'userId': serializer.toJson(userId), - 'pinned': serializer.toJson(pinned), - 'pinnedAt': serializer.toJson(pinnedAt), - 'pinExpires': serializer.toJson(pinExpires), - 'pinnedByUserId': serializer.toJson(pinnedByUserId), - 'channelCid': serializer.toJson(channelCid), - 'i18n': serializer.toJson?>(i18n), - 'restrictedVisibility': - serializer.toJson?>(restrictedVisibility), - 'extraData': serializer.toJson?>(extraData), - }; - } - - PinnedMessageEntity copyWith( - {String? id, - Value messageText = const Value.absent(), - List? attachments, - String? state, - String? type, - List? mentionedUsers, - Value?> reactionGroups = - const Value.absent(), - Value parentId = const Value.absent(), - Value quotedMessageId = const Value.absent(), - Value pollId = const Value.absent(), - Value replyCount = const Value.absent(), - Value showInChannel = const Value.absent(), - bool? shadowed, - Value command = const Value.absent(), - Value localCreatedAt = const Value.absent(), - Value remoteCreatedAt = const Value.absent(), - Value localUpdatedAt = const Value.absent(), - Value remoteUpdatedAt = const Value.absent(), - Value localDeletedAt = const Value.absent(), - Value remoteDeletedAt = const Value.absent(), - Value messageTextUpdatedAt = const Value.absent(), - Value userId = const Value.absent(), - bool? pinned, - Value pinnedAt = const Value.absent(), - Value pinExpires = const Value.absent(), - Value pinnedByUserId = const Value.absent(), - String? channelCid, - Value?> i18n = const Value.absent(), - Value?> restrictedVisibility = const Value.absent(), - Value?> extraData = const Value.absent()}) => - PinnedMessageEntity( - id: id ?? this.id, - messageText: messageText.present ? messageText.value : this.messageText, - attachments: attachments ?? this.attachments, - state: state ?? this.state, - type: type ?? this.type, - mentionedUsers: mentionedUsers ?? this.mentionedUsers, - reactionGroups: - reactionGroups.present ? reactionGroups.value : this.reactionGroups, - parentId: parentId.present ? parentId.value : this.parentId, - quotedMessageId: quotedMessageId.present - ? quotedMessageId.value - : this.quotedMessageId, - pollId: pollId.present ? pollId.value : this.pollId, - replyCount: replyCount.present ? replyCount.value : this.replyCount, - showInChannel: - showInChannel.present ? showInChannel.value : this.showInChannel, - shadowed: shadowed ?? this.shadowed, - command: command.present ? command.value : this.command, - localCreatedAt: - localCreatedAt.present ? localCreatedAt.value : this.localCreatedAt, - remoteCreatedAt: remoteCreatedAt.present - ? remoteCreatedAt.value - : this.remoteCreatedAt, - localUpdatedAt: - localUpdatedAt.present ? localUpdatedAt.value : this.localUpdatedAt, - remoteUpdatedAt: remoteUpdatedAt.present - ? remoteUpdatedAt.value - : this.remoteUpdatedAt, - localDeletedAt: - localDeletedAt.present ? localDeletedAt.value : this.localDeletedAt, - remoteDeletedAt: remoteDeletedAt.present - ? remoteDeletedAt.value - : this.remoteDeletedAt, - messageTextUpdatedAt: messageTextUpdatedAt.present - ? messageTextUpdatedAt.value - : this.messageTextUpdatedAt, - userId: userId.present ? userId.value : this.userId, - pinned: pinned ?? this.pinned, - pinnedAt: pinnedAt.present ? pinnedAt.value : this.pinnedAt, - pinExpires: pinExpires.present ? pinExpires.value : this.pinExpires, - pinnedByUserId: - pinnedByUserId.present ? pinnedByUserId.value : this.pinnedByUserId, - channelCid: channelCid ?? this.channelCid, - i18n: i18n.present ? i18n.value : this.i18n, - restrictedVisibility: restrictedVisibility.present - ? restrictedVisibility.value - : this.restrictedVisibility, - extraData: extraData.present ? extraData.value : this.extraData, - ); - PinnedMessageEntity copyWithCompanion(PinnedMessagesCompanion data) { - return PinnedMessageEntity( - id: data.id.present ? data.id.value : this.id, - messageText: - data.messageText.present ? data.messageText.value : this.messageText, - attachments: - data.attachments.present ? data.attachments.value : this.attachments, - state: data.state.present ? data.state.value : this.state, - type: data.type.present ? data.type.value : this.type, - mentionedUsers: data.mentionedUsers.present - ? data.mentionedUsers.value - : this.mentionedUsers, - reactionGroups: data.reactionGroups.present - ? data.reactionGroups.value - : this.reactionGroups, - parentId: data.parentId.present ? data.parentId.value : this.parentId, - quotedMessageId: data.quotedMessageId.present - ? data.quotedMessageId.value - : this.quotedMessageId, - pollId: data.pollId.present ? data.pollId.value : this.pollId, - replyCount: - data.replyCount.present ? data.replyCount.value : this.replyCount, - showInChannel: data.showInChannel.present - ? data.showInChannel.value - : this.showInChannel, - shadowed: data.shadowed.present ? data.shadowed.value : this.shadowed, - command: data.command.present ? data.command.value : this.command, - localCreatedAt: data.localCreatedAt.present - ? data.localCreatedAt.value - : this.localCreatedAt, - remoteCreatedAt: data.remoteCreatedAt.present - ? data.remoteCreatedAt.value - : this.remoteCreatedAt, - localUpdatedAt: data.localUpdatedAt.present - ? data.localUpdatedAt.value - : this.localUpdatedAt, - remoteUpdatedAt: data.remoteUpdatedAt.present - ? data.remoteUpdatedAt.value - : this.remoteUpdatedAt, - localDeletedAt: data.localDeletedAt.present - ? data.localDeletedAt.value - : this.localDeletedAt, - remoteDeletedAt: data.remoteDeletedAt.present - ? data.remoteDeletedAt.value - : this.remoteDeletedAt, - messageTextUpdatedAt: data.messageTextUpdatedAt.present - ? data.messageTextUpdatedAt.value - : this.messageTextUpdatedAt, - userId: data.userId.present ? data.userId.value : this.userId, - pinned: data.pinned.present ? data.pinned.value : this.pinned, - pinnedAt: data.pinnedAt.present ? data.pinnedAt.value : this.pinnedAt, - pinExpires: - data.pinExpires.present ? data.pinExpires.value : this.pinExpires, - pinnedByUserId: data.pinnedByUserId.present - ? data.pinnedByUserId.value - : this.pinnedByUserId, - channelCid: - data.channelCid.present ? data.channelCid.value : this.channelCid, - i18n: data.i18n.present ? data.i18n.value : this.i18n, - restrictedVisibility: data.restrictedVisibility.present - ? data.restrictedVisibility.value - : this.restrictedVisibility, - extraData: data.extraData.present ? data.extraData.value : this.extraData, - ); + String toString() { + return (StringBuffer('LocationsCompanion(') + ..write('channelCid: $channelCid, ') + ..write('messageId: $messageId, ') + ..write('userId: $userId, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('createdByDeviceId: $createdByDeviceId, ') + ..write('endAt: $endAt, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); } +} +class $PinnedMessagesTable extends PinnedMessages + with TableInfo<$PinnedMessagesTable, PinnedMessageEntity> { @override - String toString() { - return (StringBuffer('PinnedMessageEntity(') - ..write('id: $id, ') - ..write('messageText: $messageText, ') - ..write('attachments: $attachments, ') - ..write('state: $state, ') - ..write('type: $type, ') - ..write('mentionedUsers: $mentionedUsers, ') - ..write('reactionGroups: $reactionGroups, ') - ..write('parentId: $parentId, ') - ..write('quotedMessageId: $quotedMessageId, ') - ..write('pollId: $pollId, ') - ..write('replyCount: $replyCount, ') - ..write('showInChannel: $showInChannel, ') - ..write('shadowed: $shadowed, ') - ..write('command: $command, ') - ..write('localCreatedAt: $localCreatedAt, ') - ..write('remoteCreatedAt: $remoteCreatedAt, ') - ..write('localUpdatedAt: $localUpdatedAt, ') - ..write('remoteUpdatedAt: $remoteUpdatedAt, ') - ..write('localDeletedAt: $localDeletedAt, ') - ..write('remoteDeletedAt: $remoteDeletedAt, ') - ..write('messageTextUpdatedAt: $messageTextUpdatedAt, ') - ..write('userId: $userId, ') - ..write('pinned: $pinned, ') - ..write('pinnedAt: $pinnedAt, ') - ..write('pinExpires: $pinExpires, ') - ..write('pinnedByUserId: $pinnedByUserId, ') - ..write('channelCid: $channelCid, ') - ..write('i18n: $i18n, ') - ..write('restrictedVisibility: $restrictedVisibility, ') - ..write('extraData: $extraData') - ..write(')')) - .toString(); - } - + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PinnedMessagesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); @override - int get hashCode => Object.hashAll([ + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _messageTextMeta = + const VerificationMeta('messageText'); + @override + late final GeneratedColumn messageText = GeneratedColumn( + 'message_text', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + late final GeneratedColumnWithTypeConverter, String> + attachments = GeneratedColumn('attachments', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter>( + $PinnedMessagesTable.$converterattachments); + static const VerificationMeta _stateMeta = const VerificationMeta('state'); + @override + late final GeneratedColumn state = GeneratedColumn( + 'state', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('regular')); + @override + late final GeneratedColumnWithTypeConverter, String> + mentionedUsers = GeneratedColumn( + 'mentioned_users', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter>( + $PinnedMessagesTable.$convertermentionedUsers); + @override + late final GeneratedColumnWithTypeConverter?, + String> reactionGroups = GeneratedColumn( + 'reaction_groups', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $PinnedMessagesTable.$converterreactionGroupsn); + static const VerificationMeta _parentIdMeta = + const VerificationMeta('parentId'); + @override + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _quotedMessageIdMeta = + const VerificationMeta('quotedMessageId'); + @override + late final GeneratedColumn quotedMessageId = GeneratedColumn( + 'quoted_message_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _pollIdMeta = const VerificationMeta('pollId'); + @override + late final GeneratedColumn pollId = GeneratedColumn( + 'poll_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _replyCountMeta = + const VerificationMeta('replyCount'); + @override + late final GeneratedColumn replyCount = GeneratedColumn( + 'reply_count', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _showInChannelMeta = + const VerificationMeta('showInChannel'); + @override + late final GeneratedColumn showInChannel = GeneratedColumn( + 'show_in_channel', aliasedName, true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("show_in_channel" IN (0, 1))')); + static const VerificationMeta _shadowedMeta = + const VerificationMeta('shadowed'); + @override + late final GeneratedColumn shadowed = GeneratedColumn( + 'shadowed', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("shadowed" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _commandMeta = + const VerificationMeta('command'); + @override + late final GeneratedColumn command = GeneratedColumn( + 'command', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _localCreatedAtMeta = + const VerificationMeta('localCreatedAt'); + @override + late final GeneratedColumn localCreatedAt = + GeneratedColumn('local_created_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _remoteCreatedAtMeta = + const VerificationMeta('remoteCreatedAt'); + @override + late final GeneratedColumn remoteCreatedAt = + GeneratedColumn('remote_created_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _localUpdatedAtMeta = + const VerificationMeta('localUpdatedAt'); + @override + late final GeneratedColumn localUpdatedAt = + GeneratedColumn('local_updated_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _remoteUpdatedAtMeta = + const VerificationMeta('remoteUpdatedAt'); + @override + late final GeneratedColumn remoteUpdatedAt = + GeneratedColumn('remote_updated_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _localDeletedAtMeta = + const VerificationMeta('localDeletedAt'); + @override + late final GeneratedColumn localDeletedAt = + GeneratedColumn('local_deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _remoteDeletedAtMeta = + const VerificationMeta('remoteDeletedAt'); + @override + late final GeneratedColumn remoteDeletedAt = + GeneratedColumn('remote_deleted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _messageTextUpdatedAtMeta = + const VerificationMeta('messageTextUpdatedAt'); + @override + late final GeneratedColumn messageTextUpdatedAt = + GeneratedColumn('message_text_updated_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + @override + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _pinnedMeta = const VerificationMeta('pinned'); + @override + late final GeneratedColumn pinned = GeneratedColumn( + 'pinned', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("pinned" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _pinnedAtMeta = + const VerificationMeta('pinnedAt'); + @override + late final GeneratedColumn pinnedAt = GeneratedColumn( + 'pinned_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _pinExpiresMeta = + const VerificationMeta('pinExpires'); + @override + late final GeneratedColumn pinExpires = GeneratedColumn( + 'pin_expires', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _pinnedByUserIdMeta = + const VerificationMeta('pinnedByUserId'); + @override + late final GeneratedColumn pinnedByUserId = GeneratedColumn( + 'pinned_by_user_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _channelCidMeta = + const VerificationMeta('channelCid'); + @override + late final GeneratedColumn channelCid = GeneratedColumn( + 'channel_cid', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + late final GeneratedColumnWithTypeConverter?, String> + i18n = GeneratedColumn('i18n', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $PinnedMessagesTable.$converteri18n); + @override + late final GeneratedColumnWithTypeConverter?, String> + restrictedVisibility = GeneratedColumn( + 'restricted_visibility', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $PinnedMessagesTable.$converterrestrictedVisibilityn); + @override + late final GeneratedColumnWithTypeConverter?, String> + extraData = GeneratedColumn('extra_data', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $PinnedMessagesTable.$converterextraDatan); + @override + List get $columns => [ id, messageText, attachments, @@ -3748,278 +3472,2380 @@ class PinnedMessageEntity extends DataClass i18n, restrictedVisibility, extraData - ]); + ]; @override - bool operator ==(Object other) => - identical(this, other) || - (other is PinnedMessageEntity && - other.id == this.id && - other.messageText == this.messageText && - other.attachments == this.attachments && - other.state == this.state && - other.type == this.type && - other.mentionedUsers == this.mentionedUsers && - other.reactionGroups == this.reactionGroups && - other.parentId == this.parentId && - other.quotedMessageId == this.quotedMessageId && - other.pollId == this.pollId && - other.replyCount == this.replyCount && - other.showInChannel == this.showInChannel && - other.shadowed == this.shadowed && - other.command == this.command && - other.localCreatedAt == this.localCreatedAt && - other.remoteCreatedAt == this.remoteCreatedAt && - other.localUpdatedAt == this.localUpdatedAt && - other.remoteUpdatedAt == this.remoteUpdatedAt && - other.localDeletedAt == this.localDeletedAt && - other.remoteDeletedAt == this.remoteDeletedAt && - other.messageTextUpdatedAt == this.messageTextUpdatedAt && - other.userId == this.userId && - other.pinned == this.pinned && - other.pinnedAt == this.pinnedAt && - other.pinExpires == this.pinExpires && - other.pinnedByUserId == this.pinnedByUserId && - other.channelCid == this.channelCid && - other.i18n == this.i18n && - other.restrictedVisibility == this.restrictedVisibility && - other.extraData == this.extraData); -} - + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'pinned_messages'; + @override + VerificationContext validateIntegrity( + Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('message_text')) { + context.handle( + _messageTextMeta, + messageText.isAcceptableOrUnknown( + data['message_text']!, _messageTextMeta)); + } + if (data.containsKey('state')) { + context.handle( + _stateMeta, state.isAcceptableOrUnknown(data['state']!, _stateMeta)); + } else if (isInserting) { + context.missing(_stateMeta); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + } + if (data.containsKey('parent_id')) { + context.handle(_parentIdMeta, + parentId.isAcceptableOrUnknown(data['parent_id']!, _parentIdMeta)); + } + if (data.containsKey('quoted_message_id')) { + context.handle( + _quotedMessageIdMeta, + quotedMessageId.isAcceptableOrUnknown( + data['quoted_message_id']!, _quotedMessageIdMeta)); + } + if (data.containsKey('poll_id')) { + context.handle(_pollIdMeta, + pollId.isAcceptableOrUnknown(data['poll_id']!, _pollIdMeta)); + } + if (data.containsKey('reply_count')) { + context.handle( + _replyCountMeta, + replyCount.isAcceptableOrUnknown( + data['reply_count']!, _replyCountMeta)); + } + if (data.containsKey('show_in_channel')) { + context.handle( + _showInChannelMeta, + showInChannel.isAcceptableOrUnknown( + data['show_in_channel']!, _showInChannelMeta)); + } + if (data.containsKey('shadowed')) { + context.handle(_shadowedMeta, + shadowed.isAcceptableOrUnknown(data['shadowed']!, _shadowedMeta)); + } + if (data.containsKey('command')) { + context.handle(_commandMeta, + command.isAcceptableOrUnknown(data['command']!, _commandMeta)); + } + if (data.containsKey('local_created_at')) { + context.handle( + _localCreatedAtMeta, + localCreatedAt.isAcceptableOrUnknown( + data['local_created_at']!, _localCreatedAtMeta)); + } + if (data.containsKey('remote_created_at')) { + context.handle( + _remoteCreatedAtMeta, + remoteCreatedAt.isAcceptableOrUnknown( + data['remote_created_at']!, _remoteCreatedAtMeta)); + } + if (data.containsKey('local_updated_at')) { + context.handle( + _localUpdatedAtMeta, + localUpdatedAt.isAcceptableOrUnknown( + data['local_updated_at']!, _localUpdatedAtMeta)); + } + if (data.containsKey('remote_updated_at')) { + context.handle( + _remoteUpdatedAtMeta, + remoteUpdatedAt.isAcceptableOrUnknown( + data['remote_updated_at']!, _remoteUpdatedAtMeta)); + } + if (data.containsKey('local_deleted_at')) { + context.handle( + _localDeletedAtMeta, + localDeletedAt.isAcceptableOrUnknown( + data['local_deleted_at']!, _localDeletedAtMeta)); + } + if (data.containsKey('remote_deleted_at')) { + context.handle( + _remoteDeletedAtMeta, + remoteDeletedAt.isAcceptableOrUnknown( + data['remote_deleted_at']!, _remoteDeletedAtMeta)); + } + if (data.containsKey('message_text_updated_at')) { + context.handle( + _messageTextUpdatedAtMeta, + messageTextUpdatedAt.isAcceptableOrUnknown( + data['message_text_updated_at']!, _messageTextUpdatedAtMeta)); + } + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + } + if (data.containsKey('pinned')) { + context.handle(_pinnedMeta, + pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); + } + if (data.containsKey('pinned_at')) { + context.handle(_pinnedAtMeta, + pinnedAt.isAcceptableOrUnknown(data['pinned_at']!, _pinnedAtMeta)); + } + if (data.containsKey('pin_expires')) { + context.handle( + _pinExpiresMeta, + pinExpires.isAcceptableOrUnknown( + data['pin_expires']!, _pinExpiresMeta)); + } + if (data.containsKey('pinned_by_user_id')) { + context.handle( + _pinnedByUserIdMeta, + pinnedByUserId.isAcceptableOrUnknown( + data['pinned_by_user_id']!, _pinnedByUserIdMeta)); + } + if (data.containsKey('channel_cid')) { + context.handle( + _channelCidMeta, + channelCid.isAcceptableOrUnknown( + data['channel_cid']!, _channelCidMeta)); + } else if (isInserting) { + context.missing(_channelCidMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PinnedMessageEntity map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PinnedMessageEntity( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + messageText: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_text']), + attachments: $PinnedMessagesTable.$converterattachments.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}attachments'])!), + state: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}state'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + mentionedUsers: $PinnedMessagesTable.$convertermentionedUsers.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}mentioned_users'])!), + reactionGroups: $PinnedMessagesTable.$converterreactionGroupsn.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}reaction_groups'])), + parentId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}parent_id']), + quotedMessageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}quoted_message_id']), + pollId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}poll_id']), + replyCount: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}reply_count']), + showInChannel: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}show_in_channel']), + shadowed: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}shadowed'])!, + command: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}command']), + localCreatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}local_created_at']), + remoteCreatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}remote_created_at']), + localUpdatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}local_updated_at']), + remoteUpdatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}remote_updated_at']), + localDeletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}local_deleted_at']), + remoteDeletedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}remote_deleted_at']), + messageTextUpdatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}message_text_updated_at']), + userId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_id']), + pinned: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, + pinnedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}pinned_at']), + pinExpires: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}pin_expires']), + pinnedByUserId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}pinned_by_user_id']), + channelCid: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, + i18n: $PinnedMessagesTable.$converteri18n.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}i18n'])), + restrictedVisibility: $PinnedMessagesTable.$converterrestrictedVisibilityn + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}restricted_visibility'])), + extraData: $PinnedMessagesTable.$converterextraDatan.fromSql( + attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), + ); + } + + @override + $PinnedMessagesTable createAlias(String alias) { + return $PinnedMessagesTable(attachedDatabase, alias); + } + + static TypeConverter, String> $converterattachments = + ListConverter(); + static TypeConverter, String> $convertermentionedUsers = + ListConverter(); + static TypeConverter, String> + $converterreactionGroups = ReactionGroupsConverter(); + static TypeConverter?, String?> + $converterreactionGroupsn = + NullAwareTypeConverter.wrap($converterreactionGroups); + static TypeConverter?, String?> $converteri18n = + NullableMapConverter(); + static TypeConverter, String> $converterrestrictedVisibility = + ListConverter(); + static TypeConverter?, String?> $converterrestrictedVisibilityn = + NullAwareTypeConverter.wrap($converterrestrictedVisibility); + static TypeConverter, String> $converterextraData = + MapConverter(); + static TypeConverter?, String?> $converterextraDatan = + NullAwareTypeConverter.wrap($converterextraData); +} + +class PinnedMessageEntity extends DataClass + implements Insertable { + /// The message id + final String id; + + /// The text of this message + final String? messageText; + + /// The list of attachments, either provided by the user + /// or generated from a command or as a result of URL scraping. + final List attachments; + + /// The current state of the message. + final String state; + + /// The message type + final String type; + + /// The list of user mentioned in the message + final List mentionedUsers; + + /// A map describing the reaction group for every reaction + final Map? reactionGroups; + + /// The ID of the parent message, if the message is a thread reply. + final String? parentId; + + /// The ID of the quoted message, if the message is a quoted reply. + final String? quotedMessageId; + + /// The ID of the poll, if the message is a poll. + final String? pollId; + + /// Number of replies for this message. + final int? replyCount; + + /// Check if this message needs to show in the channel. + final bool? showInChannel; + + /// If true the message is shadowed + final bool shadowed; + + /// A used command name. + final String? command; + + /// The DateTime on which the message was created on the client. + final DateTime? localCreatedAt; + + /// The DateTime on which the message was created on the server. + final DateTime? remoteCreatedAt; + + /// The DateTime on which the message was updated on the client. + final DateTime? localUpdatedAt; + + /// The DateTime on which the message was updated on the server. + final DateTime? remoteUpdatedAt; + + /// The DateTime on which the message was deleted on the client. + final DateTime? localDeletedAt; + + /// The DateTime on which the message was deleted on the server. + final DateTime? remoteDeletedAt; + + /// The DateTime at which the message text was edited + final DateTime? messageTextUpdatedAt; + + /// Id of the User who sent the message + final String? userId; + + /// Whether the message is pinned or not + final bool pinned; + + /// The DateTime at which the message was pinned + final DateTime? pinnedAt; + + /// The DateTime on which the message pin expires + final DateTime? pinExpires; + + /// Id of the User who pinned the message + final String? pinnedByUserId; + + /// The channel cid of which this message is part of + final String channelCid; + + /// A Map of [messageText] translations. + final Map? i18n; + + /// The list of user ids that should be able to see the message. + final List? restrictedVisibility; + + /// Message custom extraData + final Map? extraData; + const PinnedMessageEntity( + {required this.id, + this.messageText, + required this.attachments, + required this.state, + required this.type, + required this.mentionedUsers, + this.reactionGroups, + this.parentId, + this.quotedMessageId, + this.pollId, + this.replyCount, + this.showInChannel, + required this.shadowed, + this.command, + this.localCreatedAt, + this.remoteCreatedAt, + this.localUpdatedAt, + this.remoteUpdatedAt, + this.localDeletedAt, + this.remoteDeletedAt, + this.messageTextUpdatedAt, + this.userId, + required this.pinned, + this.pinnedAt, + this.pinExpires, + this.pinnedByUserId, + required this.channelCid, + this.i18n, + this.restrictedVisibility, + this.extraData}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || messageText != null) { + map['message_text'] = Variable(messageText); + } + { + map['attachments'] = Variable( + $PinnedMessagesTable.$converterattachments.toSql(attachments)); + } + map['state'] = Variable(state); + map['type'] = Variable(type); + { + map['mentioned_users'] = Variable( + $PinnedMessagesTable.$convertermentionedUsers.toSql(mentionedUsers)); + } + if (!nullToAbsent || reactionGroups != null) { + map['reaction_groups'] = Variable( + $PinnedMessagesTable.$converterreactionGroupsn.toSql(reactionGroups)); + } + if (!nullToAbsent || parentId != null) { + map['parent_id'] = Variable(parentId); + } + if (!nullToAbsent || quotedMessageId != null) { + map['quoted_message_id'] = Variable(quotedMessageId); + } + if (!nullToAbsent || pollId != null) { + map['poll_id'] = Variable(pollId); + } + if (!nullToAbsent || replyCount != null) { + map['reply_count'] = Variable(replyCount); + } + if (!nullToAbsent || showInChannel != null) { + map['show_in_channel'] = Variable(showInChannel); + } + map['shadowed'] = Variable(shadowed); + if (!nullToAbsent || command != null) { + map['command'] = Variable(command); + } + if (!nullToAbsent || localCreatedAt != null) { + map['local_created_at'] = Variable(localCreatedAt); + } + if (!nullToAbsent || remoteCreatedAt != null) { + map['remote_created_at'] = Variable(remoteCreatedAt); + } + if (!nullToAbsent || localUpdatedAt != null) { + map['local_updated_at'] = Variable(localUpdatedAt); + } + if (!nullToAbsent || remoteUpdatedAt != null) { + map['remote_updated_at'] = Variable(remoteUpdatedAt); + } + if (!nullToAbsent || localDeletedAt != null) { + map['local_deleted_at'] = Variable(localDeletedAt); + } + if (!nullToAbsent || remoteDeletedAt != null) { + map['remote_deleted_at'] = Variable(remoteDeletedAt); + } + if (!nullToAbsent || messageTextUpdatedAt != null) { + map['message_text_updated_at'] = Variable(messageTextUpdatedAt); + } + if (!nullToAbsent || userId != null) { + map['user_id'] = Variable(userId); + } + map['pinned'] = Variable(pinned); + if (!nullToAbsent || pinnedAt != null) { + map['pinned_at'] = Variable(pinnedAt); + } + if (!nullToAbsent || pinExpires != null) { + map['pin_expires'] = Variable(pinExpires); + } + if (!nullToAbsent || pinnedByUserId != null) { + map['pinned_by_user_id'] = Variable(pinnedByUserId); + } + map['channel_cid'] = Variable(channelCid); + if (!nullToAbsent || i18n != null) { + map['i18n'] = + Variable($PinnedMessagesTable.$converteri18n.toSql(i18n)); + } + if (!nullToAbsent || restrictedVisibility != null) { + map['restricted_visibility'] = Variable($PinnedMessagesTable + .$converterrestrictedVisibilityn + .toSql(restrictedVisibility)); + } + if (!nullToAbsent || extraData != null) { + map['extra_data'] = Variable( + $PinnedMessagesTable.$converterextraDatan.toSql(extraData)); + } + return map; + } + + factory PinnedMessageEntity.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PinnedMessageEntity( + id: serializer.fromJson(json['id']), + messageText: serializer.fromJson(json['messageText']), + attachments: serializer.fromJson>(json['attachments']), + state: serializer.fromJson(json['state']), + type: serializer.fromJson(json['type']), + mentionedUsers: serializer.fromJson>(json['mentionedUsers']), + reactionGroups: serializer + .fromJson?>(json['reactionGroups']), + parentId: serializer.fromJson(json['parentId']), + quotedMessageId: serializer.fromJson(json['quotedMessageId']), + pollId: serializer.fromJson(json['pollId']), + replyCount: serializer.fromJson(json['replyCount']), + showInChannel: serializer.fromJson(json['showInChannel']), + shadowed: serializer.fromJson(json['shadowed']), + command: serializer.fromJson(json['command']), + localCreatedAt: serializer.fromJson(json['localCreatedAt']), + remoteCreatedAt: serializer.fromJson(json['remoteCreatedAt']), + localUpdatedAt: serializer.fromJson(json['localUpdatedAt']), + remoteUpdatedAt: serializer.fromJson(json['remoteUpdatedAt']), + localDeletedAt: serializer.fromJson(json['localDeletedAt']), + remoteDeletedAt: serializer.fromJson(json['remoteDeletedAt']), + messageTextUpdatedAt: + serializer.fromJson(json['messageTextUpdatedAt']), + userId: serializer.fromJson(json['userId']), + pinned: serializer.fromJson(json['pinned']), + pinnedAt: serializer.fromJson(json['pinnedAt']), + pinExpires: serializer.fromJson(json['pinExpires']), + pinnedByUserId: serializer.fromJson(json['pinnedByUserId']), + channelCid: serializer.fromJson(json['channelCid']), + i18n: serializer.fromJson?>(json['i18n']), + restrictedVisibility: + serializer.fromJson?>(json['restrictedVisibility']), + extraData: serializer.fromJson?>(json['extraData']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'messageText': serializer.toJson(messageText), + 'attachments': serializer.toJson>(attachments), + 'state': serializer.toJson(state), + 'type': serializer.toJson(type), + 'mentionedUsers': serializer.toJson>(mentionedUsers), + 'reactionGroups': + serializer.toJson?>(reactionGroups), + 'parentId': serializer.toJson(parentId), + 'quotedMessageId': serializer.toJson(quotedMessageId), + 'pollId': serializer.toJson(pollId), + 'replyCount': serializer.toJson(replyCount), + 'showInChannel': serializer.toJson(showInChannel), + 'shadowed': serializer.toJson(shadowed), + 'command': serializer.toJson(command), + 'localCreatedAt': serializer.toJson(localCreatedAt), + 'remoteCreatedAt': serializer.toJson(remoteCreatedAt), + 'localUpdatedAt': serializer.toJson(localUpdatedAt), + 'remoteUpdatedAt': serializer.toJson(remoteUpdatedAt), + 'localDeletedAt': serializer.toJson(localDeletedAt), + 'remoteDeletedAt': serializer.toJson(remoteDeletedAt), + 'messageTextUpdatedAt': + serializer.toJson(messageTextUpdatedAt), + 'userId': serializer.toJson(userId), + 'pinned': serializer.toJson(pinned), + 'pinnedAt': serializer.toJson(pinnedAt), + 'pinExpires': serializer.toJson(pinExpires), + 'pinnedByUserId': serializer.toJson(pinnedByUserId), + 'channelCid': serializer.toJson(channelCid), + 'i18n': serializer.toJson?>(i18n), + 'restrictedVisibility': + serializer.toJson?>(restrictedVisibility), + 'extraData': serializer.toJson?>(extraData), + }; + } + + PinnedMessageEntity copyWith( + {String? id, + Value messageText = const Value.absent(), + List? attachments, + String? state, + String? type, + List? mentionedUsers, + Value?> reactionGroups = + const Value.absent(), + Value parentId = const Value.absent(), + Value quotedMessageId = const Value.absent(), + Value pollId = const Value.absent(), + Value replyCount = const Value.absent(), + Value showInChannel = const Value.absent(), + bool? shadowed, + Value command = const Value.absent(), + Value localCreatedAt = const Value.absent(), + Value remoteCreatedAt = const Value.absent(), + Value localUpdatedAt = const Value.absent(), + Value remoteUpdatedAt = const Value.absent(), + Value localDeletedAt = const Value.absent(), + Value remoteDeletedAt = const Value.absent(), + Value messageTextUpdatedAt = const Value.absent(), + Value userId = const Value.absent(), + bool? pinned, + Value pinnedAt = const Value.absent(), + Value pinExpires = const Value.absent(), + Value pinnedByUserId = const Value.absent(), + String? channelCid, + Value?> i18n = const Value.absent(), + Value?> restrictedVisibility = const Value.absent(), + Value?> extraData = const Value.absent()}) => + PinnedMessageEntity( + id: id ?? this.id, + messageText: messageText.present ? messageText.value : this.messageText, + attachments: attachments ?? this.attachments, + state: state ?? this.state, + type: type ?? this.type, + mentionedUsers: mentionedUsers ?? this.mentionedUsers, + reactionGroups: + reactionGroups.present ? reactionGroups.value : this.reactionGroups, + parentId: parentId.present ? parentId.value : this.parentId, + quotedMessageId: quotedMessageId.present + ? quotedMessageId.value + : this.quotedMessageId, + pollId: pollId.present ? pollId.value : this.pollId, + replyCount: replyCount.present ? replyCount.value : this.replyCount, + showInChannel: + showInChannel.present ? showInChannel.value : this.showInChannel, + shadowed: shadowed ?? this.shadowed, + command: command.present ? command.value : this.command, + localCreatedAt: + localCreatedAt.present ? localCreatedAt.value : this.localCreatedAt, + remoteCreatedAt: remoteCreatedAt.present + ? remoteCreatedAt.value + : this.remoteCreatedAt, + localUpdatedAt: + localUpdatedAt.present ? localUpdatedAt.value : this.localUpdatedAt, + remoteUpdatedAt: remoteUpdatedAt.present + ? remoteUpdatedAt.value + : this.remoteUpdatedAt, + localDeletedAt: + localDeletedAt.present ? localDeletedAt.value : this.localDeletedAt, + remoteDeletedAt: remoteDeletedAt.present + ? remoteDeletedAt.value + : this.remoteDeletedAt, + messageTextUpdatedAt: messageTextUpdatedAt.present + ? messageTextUpdatedAt.value + : this.messageTextUpdatedAt, + userId: userId.present ? userId.value : this.userId, + pinned: pinned ?? this.pinned, + pinnedAt: pinnedAt.present ? pinnedAt.value : this.pinnedAt, + pinExpires: pinExpires.present ? pinExpires.value : this.pinExpires, + pinnedByUserId: + pinnedByUserId.present ? pinnedByUserId.value : this.pinnedByUserId, + channelCid: channelCid ?? this.channelCid, + i18n: i18n.present ? i18n.value : this.i18n, + restrictedVisibility: restrictedVisibility.present + ? restrictedVisibility.value + : this.restrictedVisibility, + extraData: extraData.present ? extraData.value : this.extraData, + ); + PinnedMessageEntity copyWithCompanion(PinnedMessagesCompanion data) { + return PinnedMessageEntity( + id: data.id.present ? data.id.value : this.id, + messageText: + data.messageText.present ? data.messageText.value : this.messageText, + attachments: + data.attachments.present ? data.attachments.value : this.attachments, + state: data.state.present ? data.state.value : this.state, + type: data.type.present ? data.type.value : this.type, + mentionedUsers: data.mentionedUsers.present + ? data.mentionedUsers.value + : this.mentionedUsers, + reactionGroups: data.reactionGroups.present + ? data.reactionGroups.value + : this.reactionGroups, + parentId: data.parentId.present ? data.parentId.value : this.parentId, + quotedMessageId: data.quotedMessageId.present + ? data.quotedMessageId.value + : this.quotedMessageId, + pollId: data.pollId.present ? data.pollId.value : this.pollId, + replyCount: + data.replyCount.present ? data.replyCount.value : this.replyCount, + showInChannel: data.showInChannel.present + ? data.showInChannel.value + : this.showInChannel, + shadowed: data.shadowed.present ? data.shadowed.value : this.shadowed, + command: data.command.present ? data.command.value : this.command, + localCreatedAt: data.localCreatedAt.present + ? data.localCreatedAt.value + : this.localCreatedAt, + remoteCreatedAt: data.remoteCreatedAt.present + ? data.remoteCreatedAt.value + : this.remoteCreatedAt, + localUpdatedAt: data.localUpdatedAt.present + ? data.localUpdatedAt.value + : this.localUpdatedAt, + remoteUpdatedAt: data.remoteUpdatedAt.present + ? data.remoteUpdatedAt.value + : this.remoteUpdatedAt, + localDeletedAt: data.localDeletedAt.present + ? data.localDeletedAt.value + : this.localDeletedAt, + remoteDeletedAt: data.remoteDeletedAt.present + ? data.remoteDeletedAt.value + : this.remoteDeletedAt, + messageTextUpdatedAt: data.messageTextUpdatedAt.present + ? data.messageTextUpdatedAt.value + : this.messageTextUpdatedAt, + userId: data.userId.present ? data.userId.value : this.userId, + pinned: data.pinned.present ? data.pinned.value : this.pinned, + pinnedAt: data.pinnedAt.present ? data.pinnedAt.value : this.pinnedAt, + pinExpires: + data.pinExpires.present ? data.pinExpires.value : this.pinExpires, + pinnedByUserId: data.pinnedByUserId.present + ? data.pinnedByUserId.value + : this.pinnedByUserId, + channelCid: + data.channelCid.present ? data.channelCid.value : this.channelCid, + i18n: data.i18n.present ? data.i18n.value : this.i18n, + restrictedVisibility: data.restrictedVisibility.present + ? data.restrictedVisibility.value + : this.restrictedVisibility, + extraData: data.extraData.present ? data.extraData.value : this.extraData, + ); + } + + @override + String toString() { + return (StringBuffer('PinnedMessageEntity(') + ..write('id: $id, ') + ..write('messageText: $messageText, ') + ..write('attachments: $attachments, ') + ..write('state: $state, ') + ..write('type: $type, ') + ..write('mentionedUsers: $mentionedUsers, ') + ..write('reactionGroups: $reactionGroups, ') + ..write('parentId: $parentId, ') + ..write('quotedMessageId: $quotedMessageId, ') + ..write('pollId: $pollId, ') + ..write('replyCount: $replyCount, ') + ..write('showInChannel: $showInChannel, ') + ..write('shadowed: $shadowed, ') + ..write('command: $command, ') + ..write('localCreatedAt: $localCreatedAt, ') + ..write('remoteCreatedAt: $remoteCreatedAt, ') + ..write('localUpdatedAt: $localUpdatedAt, ') + ..write('remoteUpdatedAt: $remoteUpdatedAt, ') + ..write('localDeletedAt: $localDeletedAt, ') + ..write('remoteDeletedAt: $remoteDeletedAt, ') + ..write('messageTextUpdatedAt: $messageTextUpdatedAt, ') + ..write('userId: $userId, ') + ..write('pinned: $pinned, ') + ..write('pinnedAt: $pinnedAt, ') + ..write('pinExpires: $pinExpires, ') + ..write('pinnedByUserId: $pinnedByUserId, ') + ..write('channelCid: $channelCid, ') + ..write('i18n: $i18n, ') + ..write('restrictedVisibility: $restrictedVisibility, ') + ..write('extraData: $extraData') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + id, + messageText, + attachments, + state, + type, + mentionedUsers, + reactionGroups, + parentId, + quotedMessageId, + pollId, + replyCount, + showInChannel, + shadowed, + command, + localCreatedAt, + remoteCreatedAt, + localUpdatedAt, + remoteUpdatedAt, + localDeletedAt, + remoteDeletedAt, + messageTextUpdatedAt, + userId, + pinned, + pinnedAt, + pinExpires, + pinnedByUserId, + channelCid, + i18n, + restrictedVisibility, + extraData + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PinnedMessageEntity && + other.id == this.id && + other.messageText == this.messageText && + other.attachments == this.attachments && + other.state == this.state && + other.type == this.type && + other.mentionedUsers == this.mentionedUsers && + other.reactionGroups == this.reactionGroups && + other.parentId == this.parentId && + other.quotedMessageId == this.quotedMessageId && + other.pollId == this.pollId && + other.replyCount == this.replyCount && + other.showInChannel == this.showInChannel && + other.shadowed == this.shadowed && + other.command == this.command && + other.localCreatedAt == this.localCreatedAt && + other.remoteCreatedAt == this.remoteCreatedAt && + other.localUpdatedAt == this.localUpdatedAt && + other.remoteUpdatedAt == this.remoteUpdatedAt && + other.localDeletedAt == this.localDeletedAt && + other.remoteDeletedAt == this.remoteDeletedAt && + other.messageTextUpdatedAt == this.messageTextUpdatedAt && + other.userId == this.userId && + other.pinned == this.pinned && + other.pinnedAt == this.pinnedAt && + other.pinExpires == this.pinExpires && + other.pinnedByUserId == this.pinnedByUserId && + other.channelCid == this.channelCid && + other.i18n == this.i18n && + other.restrictedVisibility == this.restrictedVisibility && + other.extraData == this.extraData); +} + class PinnedMessagesCompanion extends UpdateCompanion { final Value id; - final Value messageText; - final Value> attachments; - final Value state; - final Value type; - final Value> mentionedUsers; - final Value?> reactionGroups; - final Value parentId; - final Value quotedMessageId; + final Value messageText; + final Value> attachments; + final Value state; + final Value type; + final Value> mentionedUsers; + final Value?> reactionGroups; + final Value parentId; + final Value quotedMessageId; + final Value pollId; + final Value replyCount; + final Value showInChannel; + final Value shadowed; + final Value command; + final Value localCreatedAt; + final Value remoteCreatedAt; + final Value localUpdatedAt; + final Value remoteUpdatedAt; + final Value localDeletedAt; + final Value remoteDeletedAt; + final Value messageTextUpdatedAt; + final Value userId; + final Value pinned; + final Value pinnedAt; + final Value pinExpires; + final Value pinnedByUserId; + final Value channelCid; + final Value?> i18n; + final Value?> restrictedVisibility; + final Value?> extraData; + final Value rowid; + const PinnedMessagesCompanion({ + this.id = const Value.absent(), + this.messageText = const Value.absent(), + this.attachments = const Value.absent(), + this.state = const Value.absent(), + this.type = const Value.absent(), + this.mentionedUsers = const Value.absent(), + this.reactionGroups = const Value.absent(), + this.parentId = const Value.absent(), + this.quotedMessageId = const Value.absent(), + this.pollId = const Value.absent(), + this.replyCount = const Value.absent(), + this.showInChannel = const Value.absent(), + this.shadowed = const Value.absent(), + this.command = const Value.absent(), + this.localCreatedAt = const Value.absent(), + this.remoteCreatedAt = const Value.absent(), + this.localUpdatedAt = const Value.absent(), + this.remoteUpdatedAt = const Value.absent(), + this.localDeletedAt = const Value.absent(), + this.remoteDeletedAt = const Value.absent(), + this.messageTextUpdatedAt = const Value.absent(), + this.userId = const Value.absent(), + this.pinned = const Value.absent(), + this.pinnedAt = const Value.absent(), + this.pinExpires = const Value.absent(), + this.pinnedByUserId = const Value.absent(), + this.channelCid = const Value.absent(), + this.i18n = const Value.absent(), + this.restrictedVisibility = const Value.absent(), + this.extraData = const Value.absent(), + this.rowid = const Value.absent(), + }); + PinnedMessagesCompanion.insert({ + required String id, + this.messageText = const Value.absent(), + required List attachments, + required String state, + this.type = const Value.absent(), + required List mentionedUsers, + this.reactionGroups = const Value.absent(), + this.parentId = const Value.absent(), + this.quotedMessageId = const Value.absent(), + this.pollId = const Value.absent(), + this.replyCount = const Value.absent(), + this.showInChannel = const Value.absent(), + this.shadowed = const Value.absent(), + this.command = const Value.absent(), + this.localCreatedAt = const Value.absent(), + this.remoteCreatedAt = const Value.absent(), + this.localUpdatedAt = const Value.absent(), + this.remoteUpdatedAt = const Value.absent(), + this.localDeletedAt = const Value.absent(), + this.remoteDeletedAt = const Value.absent(), + this.messageTextUpdatedAt = const Value.absent(), + this.userId = const Value.absent(), + this.pinned = const Value.absent(), + this.pinnedAt = const Value.absent(), + this.pinExpires = const Value.absent(), + this.pinnedByUserId = const Value.absent(), + required String channelCid, + this.i18n = const Value.absent(), + this.restrictedVisibility = const Value.absent(), + this.extraData = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + attachments = Value(attachments), + state = Value(state), + mentionedUsers = Value(mentionedUsers), + channelCid = Value(channelCid); + static Insertable custom({ + Expression? id, + Expression? messageText, + Expression? attachments, + Expression? state, + Expression? type, + Expression? mentionedUsers, + Expression? reactionGroups, + Expression? parentId, + Expression? quotedMessageId, + Expression? pollId, + Expression? replyCount, + Expression? showInChannel, + Expression? shadowed, + Expression? command, + Expression? localCreatedAt, + Expression? remoteCreatedAt, + Expression? localUpdatedAt, + Expression? remoteUpdatedAt, + Expression? localDeletedAt, + Expression? remoteDeletedAt, + Expression? messageTextUpdatedAt, + Expression? userId, + Expression? pinned, + Expression? pinnedAt, + Expression? pinExpires, + Expression? pinnedByUserId, + Expression? channelCid, + Expression? i18n, + Expression? restrictedVisibility, + Expression? extraData, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (messageText != null) 'message_text': messageText, + if (attachments != null) 'attachments': attachments, + if (state != null) 'state': state, + if (type != null) 'type': type, + if (mentionedUsers != null) 'mentioned_users': mentionedUsers, + if (reactionGroups != null) 'reaction_groups': reactionGroups, + if (parentId != null) 'parent_id': parentId, + if (quotedMessageId != null) 'quoted_message_id': quotedMessageId, + if (pollId != null) 'poll_id': pollId, + if (replyCount != null) 'reply_count': replyCount, + if (showInChannel != null) 'show_in_channel': showInChannel, + if (shadowed != null) 'shadowed': shadowed, + if (command != null) 'command': command, + if (localCreatedAt != null) 'local_created_at': localCreatedAt, + if (remoteCreatedAt != null) 'remote_created_at': remoteCreatedAt, + if (localUpdatedAt != null) 'local_updated_at': localUpdatedAt, + if (remoteUpdatedAt != null) 'remote_updated_at': remoteUpdatedAt, + if (localDeletedAt != null) 'local_deleted_at': localDeletedAt, + if (remoteDeletedAt != null) 'remote_deleted_at': remoteDeletedAt, + if (messageTextUpdatedAt != null) + 'message_text_updated_at': messageTextUpdatedAt, + if (userId != null) 'user_id': userId, + if (pinned != null) 'pinned': pinned, + if (pinnedAt != null) 'pinned_at': pinnedAt, + if (pinExpires != null) 'pin_expires': pinExpires, + if (pinnedByUserId != null) 'pinned_by_user_id': pinnedByUserId, + if (channelCid != null) 'channel_cid': channelCid, + if (i18n != null) 'i18n': i18n, + if (restrictedVisibility != null) + 'restricted_visibility': restrictedVisibility, + if (extraData != null) 'extra_data': extraData, + if (rowid != null) 'rowid': rowid, + }); + } + + PinnedMessagesCompanion copyWith( + {Value? id, + Value? messageText, + Value>? attachments, + Value? state, + Value? type, + Value>? mentionedUsers, + Value?>? reactionGroups, + Value? parentId, + Value? quotedMessageId, + Value? pollId, + Value? replyCount, + Value? showInChannel, + Value? shadowed, + Value? command, + Value? localCreatedAt, + Value? remoteCreatedAt, + Value? localUpdatedAt, + Value? remoteUpdatedAt, + Value? localDeletedAt, + Value? remoteDeletedAt, + Value? messageTextUpdatedAt, + Value? userId, + Value? pinned, + Value? pinnedAt, + Value? pinExpires, + Value? pinnedByUserId, + Value? channelCid, + Value?>? i18n, + Value?>? restrictedVisibility, + Value?>? extraData, + Value? rowid}) { + return PinnedMessagesCompanion( + id: id ?? this.id, + messageText: messageText ?? this.messageText, + attachments: attachments ?? this.attachments, + state: state ?? this.state, + type: type ?? this.type, + mentionedUsers: mentionedUsers ?? this.mentionedUsers, + reactionGroups: reactionGroups ?? this.reactionGroups, + parentId: parentId ?? this.parentId, + quotedMessageId: quotedMessageId ?? this.quotedMessageId, + pollId: pollId ?? this.pollId, + replyCount: replyCount ?? this.replyCount, + showInChannel: showInChannel ?? this.showInChannel, + shadowed: shadowed ?? this.shadowed, + command: command ?? this.command, + localCreatedAt: localCreatedAt ?? this.localCreatedAt, + remoteCreatedAt: remoteCreatedAt ?? this.remoteCreatedAt, + localUpdatedAt: localUpdatedAt ?? this.localUpdatedAt, + remoteUpdatedAt: remoteUpdatedAt ?? this.remoteUpdatedAt, + localDeletedAt: localDeletedAt ?? this.localDeletedAt, + remoteDeletedAt: remoteDeletedAt ?? this.remoteDeletedAt, + messageTextUpdatedAt: messageTextUpdatedAt ?? this.messageTextUpdatedAt, + userId: userId ?? this.userId, + pinned: pinned ?? this.pinned, + pinnedAt: pinnedAt ?? this.pinnedAt, + pinExpires: pinExpires ?? this.pinExpires, + pinnedByUserId: pinnedByUserId ?? this.pinnedByUserId, + channelCid: channelCid ?? this.channelCid, + i18n: i18n ?? this.i18n, + restrictedVisibility: restrictedVisibility ?? this.restrictedVisibility, + extraData: extraData ?? this.extraData, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (messageText.present) { + map['message_text'] = Variable(messageText.value); + } + if (attachments.present) { + map['attachments'] = Variable( + $PinnedMessagesTable.$converterattachments.toSql(attachments.value)); + } + if (state.present) { + map['state'] = Variable(state.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (mentionedUsers.present) { + map['mentioned_users'] = Variable($PinnedMessagesTable + .$convertermentionedUsers + .toSql(mentionedUsers.value)); + } + if (reactionGroups.present) { + map['reaction_groups'] = Variable($PinnedMessagesTable + .$converterreactionGroupsn + .toSql(reactionGroups.value)); + } + if (parentId.present) { + map['parent_id'] = Variable(parentId.value); + } + if (quotedMessageId.present) { + map['quoted_message_id'] = Variable(quotedMessageId.value); + } + if (pollId.present) { + map['poll_id'] = Variable(pollId.value); + } + if (replyCount.present) { + map['reply_count'] = Variable(replyCount.value); + } + if (showInChannel.present) { + map['show_in_channel'] = Variable(showInChannel.value); + } + if (shadowed.present) { + map['shadowed'] = Variable(shadowed.value); + } + if (command.present) { + map['command'] = Variable(command.value); + } + if (localCreatedAt.present) { + map['local_created_at'] = Variable(localCreatedAt.value); + } + if (remoteCreatedAt.present) { + map['remote_created_at'] = Variable(remoteCreatedAt.value); + } + if (localUpdatedAt.present) { + map['local_updated_at'] = Variable(localUpdatedAt.value); + } + if (remoteUpdatedAt.present) { + map['remote_updated_at'] = Variable(remoteUpdatedAt.value); + } + if (localDeletedAt.present) { + map['local_deleted_at'] = Variable(localDeletedAt.value); + } + if (remoteDeletedAt.present) { + map['remote_deleted_at'] = Variable(remoteDeletedAt.value); + } + if (messageTextUpdatedAt.present) { + map['message_text_updated_at'] = + Variable(messageTextUpdatedAt.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (pinned.present) { + map['pinned'] = Variable(pinned.value); + } + if (pinnedAt.present) { + map['pinned_at'] = Variable(pinnedAt.value); + } + if (pinExpires.present) { + map['pin_expires'] = Variable(pinExpires.value); + } + if (pinnedByUserId.present) { + map['pinned_by_user_id'] = Variable(pinnedByUserId.value); + } + if (channelCid.present) { + map['channel_cid'] = Variable(channelCid.value); + } + if (i18n.present) { + map['i18n'] = Variable( + $PinnedMessagesTable.$converteri18n.toSql(i18n.value)); + } + if (restrictedVisibility.present) { + map['restricted_visibility'] = Variable($PinnedMessagesTable + .$converterrestrictedVisibilityn + .toSql(restrictedVisibility.value)); + } + if (extraData.present) { + map['extra_data'] = Variable( + $PinnedMessagesTable.$converterextraDatan.toSql(extraData.value)); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PinnedMessagesCompanion(') + ..write('id: $id, ') + ..write('messageText: $messageText, ') + ..write('attachments: $attachments, ') + ..write('state: $state, ') + ..write('type: $type, ') + ..write('mentionedUsers: $mentionedUsers, ') + ..write('reactionGroups: $reactionGroups, ') + ..write('parentId: $parentId, ') + ..write('quotedMessageId: $quotedMessageId, ') + ..write('pollId: $pollId, ') + ..write('replyCount: $replyCount, ') + ..write('showInChannel: $showInChannel, ') + ..write('shadowed: $shadowed, ') + ..write('command: $command, ') + ..write('localCreatedAt: $localCreatedAt, ') + ..write('remoteCreatedAt: $remoteCreatedAt, ') + ..write('localUpdatedAt: $localUpdatedAt, ') + ..write('remoteUpdatedAt: $remoteUpdatedAt, ') + ..write('localDeletedAt: $localDeletedAt, ') + ..write('remoteDeletedAt: $remoteDeletedAt, ') + ..write('messageTextUpdatedAt: $messageTextUpdatedAt, ') + ..write('userId: $userId, ') + ..write('pinned: $pinned, ') + ..write('pinnedAt: $pinnedAt, ') + ..write('pinExpires: $pinExpires, ') + ..write('pinnedByUserId: $pinnedByUserId, ') + ..write('channelCid: $channelCid, ') + ..write('i18n: $i18n, ') + ..write('restrictedVisibility: $restrictedVisibility, ') + ..write('extraData: $extraData, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $PollsTable extends Polls with TableInfo<$PollsTable, PollEntity> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PollsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _descriptionMeta = + const VerificationMeta('description'); + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + late final GeneratedColumnWithTypeConverter, String> options = + GeneratedColumn('options', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter>($PollsTable.$converteroptions); + @override + late final GeneratedColumnWithTypeConverter + votingVisibility = GeneratedColumn( + 'voting_visibility', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('public')) + .withConverter( + $PollsTable.$convertervotingVisibility); + static const VerificationMeta _enforceUniqueVoteMeta = + const VerificationMeta('enforceUniqueVote'); + @override + late final GeneratedColumn enforceUniqueVote = GeneratedColumn( + 'enforce_unique_vote', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("enforce_unique_vote" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _maxVotesAllowedMeta = + const VerificationMeta('maxVotesAllowed'); + @override + late final GeneratedColumn maxVotesAllowed = GeneratedColumn( + 'max_votes_allowed', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _allowUserSuggestedOptionsMeta = + const VerificationMeta('allowUserSuggestedOptions'); + @override + late final GeneratedColumn allowUserSuggestedOptions = + GeneratedColumn('allow_user_suggested_options', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("allow_user_suggested_options" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _allowAnswersMeta = + const VerificationMeta('allowAnswers'); + @override + late final GeneratedColumn allowAnswers = GeneratedColumn( + 'allow_answers', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("allow_answers" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _isClosedMeta = + const VerificationMeta('isClosed'); + @override + late final GeneratedColumn isClosed = GeneratedColumn( + 'is_closed', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("is_closed" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _answersCountMeta = + const VerificationMeta('answersCount'); + @override + late final GeneratedColumn answersCount = GeneratedColumn( + 'answers_count', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + @override + late final GeneratedColumnWithTypeConverter, String> + voteCountsByOption = GeneratedColumn( + 'vote_counts_by_option', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter>( + $PollsTable.$convertervoteCountsByOption); + static const VerificationMeta _voteCountMeta = + const VerificationMeta('voteCount'); + @override + late final GeneratedColumn voteCount = GeneratedColumn( + 'vote_count', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); + static const VerificationMeta _createdByIdMeta = + const VerificationMeta('createdById'); + @override + late final GeneratedColumn createdById = GeneratedColumn( + 'created_by_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _updatedAtMeta = + const VerificationMeta('updatedAt'); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + @override + late final GeneratedColumnWithTypeConverter?, String> + extraData = GeneratedColumn('extra_data', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $PollsTable.$converterextraDatan); + @override + List get $columns => [ + id, + name, + description, + options, + votingVisibility, + enforceUniqueVote, + maxVotesAllowed, + allowUserSuggestedOptions, + allowAnswers, + isClosed, + answersCount, + voteCountsByOption, + voteCount, + createdById, + createdAt, + updatedAt, + extraData + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'polls'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } + if (data.containsKey('enforce_unique_vote')) { + context.handle( + _enforceUniqueVoteMeta, + enforceUniqueVote.isAcceptableOrUnknown( + data['enforce_unique_vote']!, _enforceUniqueVoteMeta)); + } + if (data.containsKey('max_votes_allowed')) { + context.handle( + _maxVotesAllowedMeta, + maxVotesAllowed.isAcceptableOrUnknown( + data['max_votes_allowed']!, _maxVotesAllowedMeta)); + } + if (data.containsKey('allow_user_suggested_options')) { + context.handle( + _allowUserSuggestedOptionsMeta, + allowUserSuggestedOptions.isAcceptableOrUnknown( + data['allow_user_suggested_options']!, + _allowUserSuggestedOptionsMeta)); + } + if (data.containsKey('allow_answers')) { + context.handle( + _allowAnswersMeta, + allowAnswers.isAcceptableOrUnknown( + data['allow_answers']!, _allowAnswersMeta)); + } + if (data.containsKey('is_closed')) { + context.handle(_isClosedMeta, + isClosed.isAcceptableOrUnknown(data['is_closed']!, _isClosedMeta)); + } + if (data.containsKey('answers_count')) { + context.handle( + _answersCountMeta, + answersCount.isAcceptableOrUnknown( + data['answers_count']!, _answersCountMeta)); + } + if (data.containsKey('vote_count')) { + context.handle(_voteCountMeta, + voteCount.isAcceptableOrUnknown(data['vote_count']!, _voteCountMeta)); + } + if (data.containsKey('created_by_id')) { + context.handle( + _createdByIdMeta, + createdById.isAcceptableOrUnknown( + data['created_by_id']!, _createdByIdMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PollEntity map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PollEntity( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description']), + options: $PollsTable.$converteroptions.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}options'])!), + votingVisibility: $PollsTable.$convertervotingVisibility.fromSql( + attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}voting_visibility'])!), + enforceUniqueVote: attachedDatabase.typeMapping.read( + DriftSqlType.bool, data['${effectivePrefix}enforce_unique_vote'])!, + maxVotesAllowed: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}max_votes_allowed']), + allowUserSuggestedOptions: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}allow_user_suggested_options'])!, + allowAnswers: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}allow_answers'])!, + isClosed: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_closed'])!, + answersCount: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}answers_count'])!, + voteCountsByOption: $PollsTable.$convertervoteCountsByOption.fromSql( + attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}vote_counts_by_option'])!), + voteCount: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}vote_count'])!, + createdById: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}created_by_id']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + extraData: $PollsTable.$converterextraDatan.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), + ); + } + + @override + $PollsTable createAlias(String alias) { + return $PollsTable(attachedDatabase, alias); + } + + static TypeConverter, String> $converteroptions = + ListConverter(); + static TypeConverter $convertervotingVisibility = + const VotingVisibilityConverter(); + static TypeConverter, String> $convertervoteCountsByOption = + MapConverter(); + static TypeConverter, String> $converterextraData = + MapConverter(); + static TypeConverter?, String?> $converterextraDatan = + NullAwareTypeConverter.wrap($converterextraData); +} + +class PollEntity extends DataClass implements Insertable { + /// The unique identifier of the poll. + final String id; + + /// The name of the poll. + final String name; + + /// The description of the poll. + final String? description; + + /// The list of options available for the poll. + final List options; + + /// Represents the visibility of the voting process. + /// + /// Defaults to 'public'. + final VotingVisibility votingVisibility; + + /// If true, only unique votes are allowed. + /// + /// Defaults to false. + final bool enforceUniqueVote; + + /// The maximum number of votes allowed per user. + final int? maxVotesAllowed; + + /// If true, users can suggest their own options. + /// + /// Defaults to false. + final bool allowUserSuggestedOptions; + + /// If true, users can provide their own answers/comments. + /// + /// Defaults to false. + final bool allowAnswers; + + /// Indicates if the poll is closed. + final bool isClosed; + + /// The total number of answers received by the poll. + final int answersCount; + + /// Map of vote counts by option. + final Map voteCountsByOption; + + /// The total number of votes received by the poll. + final int voteCount; + + /// The id of the user who created the poll. + final String? createdById; + + /// The date when the poll was created. + final DateTime createdAt; + + /// The date when the poll was last updated. + final DateTime updatedAt; + + /// Map of custom poll extraData + final Map? extraData; + const PollEntity( + {required this.id, + required this.name, + this.description, + required this.options, + required this.votingVisibility, + required this.enforceUniqueVote, + this.maxVotesAllowed, + required this.allowUserSuggestedOptions, + required this.allowAnswers, + required this.isClosed, + required this.answersCount, + required this.voteCountsByOption, + required this.voteCount, + this.createdById, + required this.createdAt, + required this.updatedAt, + this.extraData}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + { + map['options'] = + Variable($PollsTable.$converteroptions.toSql(options)); + } + { + map['voting_visibility'] = Variable( + $PollsTable.$convertervotingVisibility.toSql(votingVisibility)); + } + map['enforce_unique_vote'] = Variable(enforceUniqueVote); + if (!nullToAbsent || maxVotesAllowed != null) { + map['max_votes_allowed'] = Variable(maxVotesAllowed); + } + map['allow_user_suggested_options'] = + Variable(allowUserSuggestedOptions); + map['allow_answers'] = Variable(allowAnswers); + map['is_closed'] = Variable(isClosed); + map['answers_count'] = Variable(answersCount); + { + map['vote_counts_by_option'] = Variable( + $PollsTable.$convertervoteCountsByOption.toSql(voteCountsByOption)); + } + map['vote_count'] = Variable(voteCount); + if (!nullToAbsent || createdById != null) { + map['created_by_id'] = Variable(createdById); + } + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || extraData != null) { + map['extra_data'] = + Variable($PollsTable.$converterextraDatan.toSql(extraData)); + } + return map; + } + + factory PollEntity.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PollEntity( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + options: serializer.fromJson>(json['options']), + votingVisibility: + serializer.fromJson(json['votingVisibility']), + enforceUniqueVote: serializer.fromJson(json['enforceUniqueVote']), + maxVotesAllowed: serializer.fromJson(json['maxVotesAllowed']), + allowUserSuggestedOptions: + serializer.fromJson(json['allowUserSuggestedOptions']), + allowAnswers: serializer.fromJson(json['allowAnswers']), + isClosed: serializer.fromJson(json['isClosed']), + answersCount: serializer.fromJson(json['answersCount']), + voteCountsByOption: + serializer.fromJson>(json['voteCountsByOption']), + voteCount: serializer.fromJson(json['voteCount']), + createdById: serializer.fromJson(json['createdById']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + extraData: serializer.fromJson?>(json['extraData']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'options': serializer.toJson>(options), + 'votingVisibility': serializer.toJson(votingVisibility), + 'enforceUniqueVote': serializer.toJson(enforceUniqueVote), + 'maxVotesAllowed': serializer.toJson(maxVotesAllowed), + 'allowUserSuggestedOptions': + serializer.toJson(allowUserSuggestedOptions), + 'allowAnswers': serializer.toJson(allowAnswers), + 'isClosed': serializer.toJson(isClosed), + 'answersCount': serializer.toJson(answersCount), + 'voteCountsByOption': + serializer.toJson>(voteCountsByOption), + 'voteCount': serializer.toJson(voteCount), + 'createdById': serializer.toJson(createdById), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'extraData': serializer.toJson?>(extraData), + }; + } + + PollEntity copyWith( + {String? id, + String? name, + Value description = const Value.absent(), + List? options, + VotingVisibility? votingVisibility, + bool? enforceUniqueVote, + Value maxVotesAllowed = const Value.absent(), + bool? allowUserSuggestedOptions, + bool? allowAnswers, + bool? isClosed, + int? answersCount, + Map? voteCountsByOption, + int? voteCount, + Value createdById = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value?> extraData = const Value.absent()}) => + PollEntity( + id: id ?? this.id, + name: name ?? this.name, + description: description.present ? description.value : this.description, + options: options ?? this.options, + votingVisibility: votingVisibility ?? this.votingVisibility, + enforceUniqueVote: enforceUniqueVote ?? this.enforceUniqueVote, + maxVotesAllowed: maxVotesAllowed.present + ? maxVotesAllowed.value + : this.maxVotesAllowed, + allowUserSuggestedOptions: + allowUserSuggestedOptions ?? this.allowUserSuggestedOptions, + allowAnswers: allowAnswers ?? this.allowAnswers, + isClosed: isClosed ?? this.isClosed, + answersCount: answersCount ?? this.answersCount, + voteCountsByOption: voteCountsByOption ?? this.voteCountsByOption, + voteCount: voteCount ?? this.voteCount, + createdById: createdById.present ? createdById.value : this.createdById, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + extraData: extraData.present ? extraData.value : this.extraData, + ); + PollEntity copyWithCompanion(PollsCompanion data) { + return PollEntity( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: + data.description.present ? data.description.value : this.description, + options: data.options.present ? data.options.value : this.options, + votingVisibility: data.votingVisibility.present + ? data.votingVisibility.value + : this.votingVisibility, + enforceUniqueVote: data.enforceUniqueVote.present + ? data.enforceUniqueVote.value + : this.enforceUniqueVote, + maxVotesAllowed: data.maxVotesAllowed.present + ? data.maxVotesAllowed.value + : this.maxVotesAllowed, + allowUserSuggestedOptions: data.allowUserSuggestedOptions.present + ? data.allowUserSuggestedOptions.value + : this.allowUserSuggestedOptions, + allowAnswers: data.allowAnswers.present + ? data.allowAnswers.value + : this.allowAnswers, + isClosed: data.isClosed.present ? data.isClosed.value : this.isClosed, + answersCount: data.answersCount.present + ? data.answersCount.value + : this.answersCount, + voteCountsByOption: data.voteCountsByOption.present + ? data.voteCountsByOption.value + : this.voteCountsByOption, + voteCount: data.voteCount.present ? data.voteCount.value : this.voteCount, + createdById: + data.createdById.present ? data.createdById.value : this.createdById, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + extraData: data.extraData.present ? data.extraData.value : this.extraData, + ); + } + + @override + String toString() { + return (StringBuffer('PollEntity(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('options: $options, ') + ..write('votingVisibility: $votingVisibility, ') + ..write('enforceUniqueVote: $enforceUniqueVote, ') + ..write('maxVotesAllowed: $maxVotesAllowed, ') + ..write('allowUserSuggestedOptions: $allowUserSuggestedOptions, ') + ..write('allowAnswers: $allowAnswers, ') + ..write('isClosed: $isClosed, ') + ..write('answersCount: $answersCount, ') + ..write('voteCountsByOption: $voteCountsByOption, ') + ..write('voteCount: $voteCount, ') + ..write('createdById: $createdById, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('extraData: $extraData') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + name, + description, + options, + votingVisibility, + enforceUniqueVote, + maxVotesAllowed, + allowUserSuggestedOptions, + allowAnswers, + isClosed, + answersCount, + voteCountsByOption, + voteCount, + createdById, + createdAt, + updatedAt, + extraData); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PollEntity && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.options == this.options && + other.votingVisibility == this.votingVisibility && + other.enforceUniqueVote == this.enforceUniqueVote && + other.maxVotesAllowed == this.maxVotesAllowed && + other.allowUserSuggestedOptions == this.allowUserSuggestedOptions && + other.allowAnswers == this.allowAnswers && + other.isClosed == this.isClosed && + other.answersCount == this.answersCount && + other.voteCountsByOption == this.voteCountsByOption && + other.voteCount == this.voteCount && + other.createdById == this.createdById && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.extraData == this.extraData); +} + +class PollsCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value description; + final Value> options; + final Value votingVisibility; + final Value enforceUniqueVote; + final Value maxVotesAllowed; + final Value allowUserSuggestedOptions; + final Value allowAnswers; + final Value isClosed; + final Value answersCount; + final Value> voteCountsByOption; + final Value voteCount; + final Value createdById; + final Value createdAt; + final Value updatedAt; + final Value?> extraData; + final Value rowid; + const PollsCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.options = const Value.absent(), + this.votingVisibility = const Value.absent(), + this.enforceUniqueVote = const Value.absent(), + this.maxVotesAllowed = const Value.absent(), + this.allowUserSuggestedOptions = const Value.absent(), + this.allowAnswers = const Value.absent(), + this.isClosed = const Value.absent(), + this.answersCount = const Value.absent(), + this.voteCountsByOption = const Value.absent(), + this.voteCount = const Value.absent(), + this.createdById = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.extraData = const Value.absent(), + this.rowid = const Value.absent(), + }); + PollsCompanion.insert({ + required String id, + required String name, + this.description = const Value.absent(), + required List options, + this.votingVisibility = const Value.absent(), + this.enforceUniqueVote = const Value.absent(), + this.maxVotesAllowed = const Value.absent(), + this.allowUserSuggestedOptions = const Value.absent(), + this.allowAnswers = const Value.absent(), + this.isClosed = const Value.absent(), + this.answersCount = const Value.absent(), + required Map voteCountsByOption, + this.voteCount = const Value.absent(), + this.createdById = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), + this.extraData = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + options = Value(options), + voteCountsByOption = Value(voteCountsByOption); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? description, + Expression? options, + Expression? votingVisibility, + Expression? enforceUniqueVote, + Expression? maxVotesAllowed, + Expression? allowUserSuggestedOptions, + Expression? allowAnswers, + Expression? isClosed, + Expression? answersCount, + Expression? voteCountsByOption, + Expression? voteCount, + Expression? createdById, + Expression? createdAt, + Expression? updatedAt, + Expression? extraData, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (options != null) 'options': options, + if (votingVisibility != null) 'voting_visibility': votingVisibility, + if (enforceUniqueVote != null) 'enforce_unique_vote': enforceUniqueVote, + if (maxVotesAllowed != null) 'max_votes_allowed': maxVotesAllowed, + if (allowUserSuggestedOptions != null) + 'allow_user_suggested_options': allowUserSuggestedOptions, + if (allowAnswers != null) 'allow_answers': allowAnswers, + if (isClosed != null) 'is_closed': isClosed, + if (answersCount != null) 'answers_count': answersCount, + if (voteCountsByOption != null) + 'vote_counts_by_option': voteCountsByOption, + if (voteCount != null) 'vote_count': voteCount, + if (createdById != null) 'created_by_id': createdById, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (extraData != null) 'extra_data': extraData, + if (rowid != null) 'rowid': rowid, + }); + } + + PollsCompanion copyWith( + {Value? id, + Value? name, + Value? description, + Value>? options, + Value? votingVisibility, + Value? enforceUniqueVote, + Value? maxVotesAllowed, + Value? allowUserSuggestedOptions, + Value? allowAnswers, + Value? isClosed, + Value? answersCount, + Value>? voteCountsByOption, + Value? voteCount, + Value? createdById, + Value? createdAt, + Value? updatedAt, + Value?>? extraData, + Value? rowid}) { + return PollsCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + options: options ?? this.options, + votingVisibility: votingVisibility ?? this.votingVisibility, + enforceUniqueVote: enforceUniqueVote ?? this.enforceUniqueVote, + maxVotesAllowed: maxVotesAllowed ?? this.maxVotesAllowed, + allowUserSuggestedOptions: + allowUserSuggestedOptions ?? this.allowUserSuggestedOptions, + allowAnswers: allowAnswers ?? this.allowAnswers, + isClosed: isClosed ?? this.isClosed, + answersCount: answersCount ?? this.answersCount, + voteCountsByOption: voteCountsByOption ?? this.voteCountsByOption, + voteCount: voteCount ?? this.voteCount, + createdById: createdById ?? this.createdById, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + extraData: extraData ?? this.extraData, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (options.present) { + map['options'] = + Variable($PollsTable.$converteroptions.toSql(options.value)); + } + if (votingVisibility.present) { + map['voting_visibility'] = Variable( + $PollsTable.$convertervotingVisibility.toSql(votingVisibility.value)); + } + if (enforceUniqueVote.present) { + map['enforce_unique_vote'] = Variable(enforceUniqueVote.value); + } + if (maxVotesAllowed.present) { + map['max_votes_allowed'] = Variable(maxVotesAllowed.value); + } + if (allowUserSuggestedOptions.present) { + map['allow_user_suggested_options'] = + Variable(allowUserSuggestedOptions.value); + } + if (allowAnswers.present) { + map['allow_answers'] = Variable(allowAnswers.value); + } + if (isClosed.present) { + map['is_closed'] = Variable(isClosed.value); + } + if (answersCount.present) { + map['answers_count'] = Variable(answersCount.value); + } + if (voteCountsByOption.present) { + map['vote_counts_by_option'] = Variable($PollsTable + .$convertervoteCountsByOption + .toSql(voteCountsByOption.value)); + } + if (voteCount.present) { + map['vote_count'] = Variable(voteCount.value); + } + if (createdById.present) { + map['created_by_id'] = Variable(createdById.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (extraData.present) { + map['extra_data'] = Variable( + $PollsTable.$converterextraDatan.toSql(extraData.value)); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PollsCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('options: $options, ') + ..write('votingVisibility: $votingVisibility, ') + ..write('enforceUniqueVote: $enforceUniqueVote, ') + ..write('maxVotesAllowed: $maxVotesAllowed, ') + ..write('allowUserSuggestedOptions: $allowUserSuggestedOptions, ') + ..write('allowAnswers: $allowAnswers, ') + ..write('isClosed: $isClosed, ') + ..write('answersCount: $answersCount, ') + ..write('voteCountsByOption: $voteCountsByOption, ') + ..write('voteCount: $voteCount, ') + ..write('createdById: $createdById, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('extraData: $extraData, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $PollVotesTable extends PollVotes + with TableInfo<$PollVotesTable, PollVoteEntity> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PollVotesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _pollIdMeta = const VerificationMeta('pollId'); + @override + late final GeneratedColumn pollId = GeneratedColumn( + 'poll_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES polls (id) ON DELETE CASCADE')); + static const VerificationMeta _optionIdMeta = + const VerificationMeta('optionId'); + @override + late final GeneratedColumn optionId = GeneratedColumn( + 'option_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _answerTextMeta = + const VerificationMeta('answerText'); + @override + late final GeneratedColumn answerText = GeneratedColumn( + 'answer_text', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _updatedAtMeta = + const VerificationMeta('updatedAt'); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + @override + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => + [id, pollId, optionId, answerText, createdAt, updatedAt, userId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'poll_votes'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('poll_id')) { + context.handle(_pollIdMeta, + pollId.isAcceptableOrUnknown(data['poll_id']!, _pollIdMeta)); + } + if (data.containsKey('option_id')) { + context.handle(_optionIdMeta, + optionId.isAcceptableOrUnknown(data['option_id']!, _optionIdMeta)); + } + if (data.containsKey('answer_text')) { + context.handle( + _answerTextMeta, + answerText.isAcceptableOrUnknown( + data['answer_text']!, _answerTextMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id, pollId}; + @override + PollVoteEntity map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PollVoteEntity( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id']), + pollId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}poll_id']), + optionId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}option_id']), + answerText: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}answer_text']), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + userId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_id']), + ); + } + + @override + $PollVotesTable createAlias(String alias) { + return $PollVotesTable(attachedDatabase, alias); + } +} + +class PollVoteEntity extends DataClass implements Insertable { + /// The unique identifier of the poll vote. + final String? id; + + /// The unique identifier of the poll the vote belongs to. + final String? pollId; + + /// The unique identifier of the option selected in the poll. + /// + /// Nullable if the user provided an answer. + final String? optionId; + + /// The text of the answer provided in the poll. + /// + /// Nullable if the user selected an option. + final String? answerText; + + /// The date when the poll vote was created. + final DateTime createdAt; + + /// The date when the poll vote was last updated. + final DateTime updatedAt; + + /// The unique identifier of the user who voted. + /// + /// Nullable if the poll is anonymous. + final String? userId; + const PollVoteEntity( + {this.id, + this.pollId, + this.optionId, + this.answerText, + required this.createdAt, + required this.updatedAt, + this.userId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (!nullToAbsent || id != null) { + map['id'] = Variable(id); + } + if (!nullToAbsent || pollId != null) { + map['poll_id'] = Variable(pollId); + } + if (!nullToAbsent || optionId != null) { + map['option_id'] = Variable(optionId); + } + if (!nullToAbsent || answerText != null) { + map['answer_text'] = Variable(answerText); + } + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); + if (!nullToAbsent || userId != null) { + map['user_id'] = Variable(userId); + } + return map; + } + + factory PollVoteEntity.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PollVoteEntity( + id: serializer.fromJson(json['id']), + pollId: serializer.fromJson(json['pollId']), + optionId: serializer.fromJson(json['optionId']), + answerText: serializer.fromJson(json['answerText']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + userId: serializer.fromJson(json['userId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'pollId': serializer.toJson(pollId), + 'optionId': serializer.toJson(optionId), + 'answerText': serializer.toJson(answerText), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'userId': serializer.toJson(userId), + }; + } + + PollVoteEntity copyWith( + {Value id = const Value.absent(), + Value pollId = const Value.absent(), + Value optionId = const Value.absent(), + Value answerText = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value userId = const Value.absent()}) => + PollVoteEntity( + id: id.present ? id.value : this.id, + pollId: pollId.present ? pollId.value : this.pollId, + optionId: optionId.present ? optionId.value : this.optionId, + answerText: answerText.present ? answerText.value : this.answerText, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + userId: userId.present ? userId.value : this.userId, + ); + PollVoteEntity copyWithCompanion(PollVotesCompanion data) { + return PollVoteEntity( + id: data.id.present ? data.id.value : this.id, + pollId: data.pollId.present ? data.pollId.value : this.pollId, + optionId: data.optionId.present ? data.optionId.value : this.optionId, + answerText: + data.answerText.present ? data.answerText.value : this.answerText, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + userId: data.userId.present ? data.userId.value : this.userId, + ); + } + + @override + String toString() { + return (StringBuffer('PollVoteEntity(') + ..write('id: $id, ') + ..write('pollId: $pollId, ') + ..write('optionId: $optionId, ') + ..write('answerText: $answerText, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('userId: $userId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, pollId, optionId, answerText, createdAt, updatedAt, userId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PollVoteEntity && + other.id == this.id && + other.pollId == this.pollId && + other.optionId == this.optionId && + other.answerText == this.answerText && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.userId == this.userId); +} + +class PollVotesCompanion extends UpdateCompanion { + final Value id; final Value pollId; - final Value replyCount; - final Value showInChannel; - final Value shadowed; - final Value command; - final Value localCreatedAt; - final Value remoteCreatedAt; - final Value localUpdatedAt; - final Value remoteUpdatedAt; - final Value localDeletedAt; - final Value remoteDeletedAt; - final Value messageTextUpdatedAt; + final Value optionId; + final Value answerText; + final Value createdAt; + final Value updatedAt; final Value userId; - final Value pinned; - final Value pinnedAt; - final Value pinExpires; - final Value pinnedByUserId; - final Value channelCid; - final Value?> i18n; - final Value?> restrictedVisibility; - final Value?> extraData; final Value rowid; - const PinnedMessagesCompanion({ + const PollVotesCompanion({ this.id = const Value.absent(), - this.messageText = const Value.absent(), - this.attachments = const Value.absent(), - this.state = const Value.absent(), - this.type = const Value.absent(), - this.mentionedUsers = const Value.absent(), - this.reactionGroups = const Value.absent(), - this.parentId = const Value.absent(), - this.quotedMessageId = const Value.absent(), this.pollId = const Value.absent(), - this.replyCount = const Value.absent(), - this.showInChannel = const Value.absent(), - this.shadowed = const Value.absent(), - this.command = const Value.absent(), - this.localCreatedAt = const Value.absent(), - this.remoteCreatedAt = const Value.absent(), - this.localUpdatedAt = const Value.absent(), - this.remoteUpdatedAt = const Value.absent(), - this.localDeletedAt = const Value.absent(), - this.remoteDeletedAt = const Value.absent(), - this.messageTextUpdatedAt = const Value.absent(), + this.optionId = const Value.absent(), + this.answerText = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), this.userId = const Value.absent(), - this.pinned = const Value.absent(), - this.pinnedAt = const Value.absent(), - this.pinExpires = const Value.absent(), - this.pinnedByUserId = const Value.absent(), - this.channelCid = const Value.absent(), - this.i18n = const Value.absent(), - this.restrictedVisibility = const Value.absent(), - this.extraData = const Value.absent(), this.rowid = const Value.absent(), }); - PinnedMessagesCompanion.insert({ - required String id, - this.messageText = const Value.absent(), - required List attachments, - required String state, - this.type = const Value.absent(), - required List mentionedUsers, - this.reactionGroups = const Value.absent(), - this.parentId = const Value.absent(), - this.quotedMessageId = const Value.absent(), + PollVotesCompanion.insert({ + this.id = const Value.absent(), this.pollId = const Value.absent(), - this.replyCount = const Value.absent(), - this.showInChannel = const Value.absent(), - this.shadowed = const Value.absent(), - this.command = const Value.absent(), - this.localCreatedAt = const Value.absent(), - this.remoteCreatedAt = const Value.absent(), - this.localUpdatedAt = const Value.absent(), - this.remoteUpdatedAt = const Value.absent(), - this.localDeletedAt = const Value.absent(), - this.remoteDeletedAt = const Value.absent(), - this.messageTextUpdatedAt = const Value.absent(), + this.optionId = const Value.absent(), + this.answerText = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), this.userId = const Value.absent(), - this.pinned = const Value.absent(), - this.pinnedAt = const Value.absent(), - this.pinExpires = const Value.absent(), - this.pinnedByUserId = const Value.absent(), - required String channelCid, - this.i18n = const Value.absent(), - this.restrictedVisibility = const Value.absent(), - this.extraData = const Value.absent(), this.rowid = const Value.absent(), - }) : id = Value(id), - attachments = Value(attachments), - state = Value(state), - mentionedUsers = Value(mentionedUsers), - channelCid = Value(channelCid); - static Insertable custom({ + }); + static Insertable custom({ Expression? id, - Expression? messageText, - Expression? attachments, - Expression? state, - Expression? type, - Expression? mentionedUsers, - Expression? reactionGroups, - Expression? parentId, - Expression? quotedMessageId, Expression? pollId, - Expression? replyCount, - Expression? showInChannel, - Expression? shadowed, - Expression? command, - Expression? localCreatedAt, - Expression? remoteCreatedAt, - Expression? localUpdatedAt, - Expression? remoteUpdatedAt, - Expression? localDeletedAt, - Expression? remoteDeletedAt, - Expression? messageTextUpdatedAt, + Expression? optionId, + Expression? answerText, + Expression? createdAt, + Expression? updatedAt, Expression? userId, - Expression? pinned, - Expression? pinnedAt, - Expression? pinExpires, - Expression? pinnedByUserId, - Expression? channelCid, - Expression? i18n, - Expression? restrictedVisibility, - Expression? extraData, Expression? rowid, }) { return RawValuesInsertable({ if (id != null) 'id': id, - if (messageText != null) 'message_text': messageText, - if (attachments != null) 'attachments': attachments, - if (state != null) 'state': state, - if (type != null) 'type': type, - if (mentionedUsers != null) 'mentioned_users': mentionedUsers, - if (reactionGroups != null) 'reaction_groups': reactionGroups, - if (parentId != null) 'parent_id': parentId, - if (quotedMessageId != null) 'quoted_message_id': quotedMessageId, if (pollId != null) 'poll_id': pollId, - if (replyCount != null) 'reply_count': replyCount, - if (showInChannel != null) 'show_in_channel': showInChannel, - if (shadowed != null) 'shadowed': shadowed, - if (command != null) 'command': command, - if (localCreatedAt != null) 'local_created_at': localCreatedAt, - if (remoteCreatedAt != null) 'remote_created_at': remoteCreatedAt, - if (localUpdatedAt != null) 'local_updated_at': localUpdatedAt, - if (remoteUpdatedAt != null) 'remote_updated_at': remoteUpdatedAt, - if (localDeletedAt != null) 'local_deleted_at': localDeletedAt, - if (remoteDeletedAt != null) 'remote_deleted_at': remoteDeletedAt, - if (messageTextUpdatedAt != null) - 'message_text_updated_at': messageTextUpdatedAt, + if (optionId != null) 'option_id': optionId, + if (answerText != null) 'answer_text': answerText, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, if (userId != null) 'user_id': userId, - if (pinned != null) 'pinned': pinned, - if (pinnedAt != null) 'pinned_at': pinnedAt, - if (pinExpires != null) 'pin_expires': pinExpires, - if (pinnedByUserId != null) 'pinned_by_user_id': pinnedByUserId, - if (channelCid != null) 'channel_cid': channelCid, - if (i18n != null) 'i18n': i18n, - if (restrictedVisibility != null) - 'restricted_visibility': restrictedVisibility, - if (extraData != null) 'extra_data': extraData, if (rowid != null) 'rowid': rowid, }); } - PinnedMessagesCompanion copyWith( - {Value? id, - Value? messageText, - Value>? attachments, - Value? state, - Value? type, - Value>? mentionedUsers, - Value?>? reactionGroups, - Value? parentId, - Value? quotedMessageId, + PollVotesCompanion copyWith( + {Value? id, Value? pollId, - Value? replyCount, - Value? showInChannel, - Value? shadowed, - Value? command, - Value? localCreatedAt, - Value? remoteCreatedAt, - Value? localUpdatedAt, - Value? remoteUpdatedAt, - Value? localDeletedAt, - Value? remoteDeletedAt, - Value? messageTextUpdatedAt, + Value? optionId, + Value? answerText, + Value? createdAt, + Value? updatedAt, Value? userId, - Value? pinned, - Value? pinnedAt, - Value? pinExpires, - Value? pinnedByUserId, - Value? channelCid, - Value?>? i18n, - Value?>? restrictedVisibility, - Value?>? extraData, Value? rowid}) { - return PinnedMessagesCompanion( + return PollVotesCompanion( id: id ?? this.id, - messageText: messageText ?? this.messageText, - attachments: attachments ?? this.attachments, - state: state ?? this.state, - type: type ?? this.type, - mentionedUsers: mentionedUsers ?? this.mentionedUsers, - reactionGroups: reactionGroups ?? this.reactionGroups, - parentId: parentId ?? this.parentId, - quotedMessageId: quotedMessageId ?? this.quotedMessageId, pollId: pollId ?? this.pollId, - replyCount: replyCount ?? this.replyCount, - showInChannel: showInChannel ?? this.showInChannel, - shadowed: shadowed ?? this.shadowed, - command: command ?? this.command, - localCreatedAt: localCreatedAt ?? this.localCreatedAt, - remoteCreatedAt: remoteCreatedAt ?? this.remoteCreatedAt, - localUpdatedAt: localUpdatedAt ?? this.localUpdatedAt, - remoteUpdatedAt: remoteUpdatedAt ?? this.remoteUpdatedAt, - localDeletedAt: localDeletedAt ?? this.localDeletedAt, - remoteDeletedAt: remoteDeletedAt ?? this.remoteDeletedAt, - messageTextUpdatedAt: messageTextUpdatedAt ?? this.messageTextUpdatedAt, + optionId: optionId ?? this.optionId, + answerText: answerText ?? this.answerText, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, userId: userId ?? this.userId, - pinned: pinned ?? this.pinned, - pinnedAt: pinnedAt ?? this.pinnedAt, - pinExpires: pinExpires ?? this.pinExpires, - pinnedByUserId: pinnedByUserId ?? this.pinnedByUserId, - channelCid: channelCid ?? this.channelCid, - i18n: i18n ?? this.i18n, - restrictedVisibility: restrictedVisibility ?? this.restrictedVisibility, - extraData: extraData ?? this.extraData, rowid: rowid ?? this.rowid, ); } @@ -4030,103 +5856,24 @@ class PinnedMessagesCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } - if (messageText.present) { - map['message_text'] = Variable(messageText.value); - } - if (attachments.present) { - map['attachments'] = Variable( - $PinnedMessagesTable.$converterattachments.toSql(attachments.value)); - } - if (state.present) { - map['state'] = Variable(state.value); - } - if (type.present) { - map['type'] = Variable(type.value); - } - if (mentionedUsers.present) { - map['mentioned_users'] = Variable($PinnedMessagesTable - .$convertermentionedUsers - .toSql(mentionedUsers.value)); - } - if (reactionGroups.present) { - map['reaction_groups'] = Variable($PinnedMessagesTable - .$converterreactionGroupsn - .toSql(reactionGroups.value)); - } - if (parentId.present) { - map['parent_id'] = Variable(parentId.value); - } - if (quotedMessageId.present) { - map['quoted_message_id'] = Variable(quotedMessageId.value); - } if (pollId.present) { map['poll_id'] = Variable(pollId.value); } - if (replyCount.present) { - map['reply_count'] = Variable(replyCount.value); - } - if (showInChannel.present) { - map['show_in_channel'] = Variable(showInChannel.value); - } - if (shadowed.present) { - map['shadowed'] = Variable(shadowed.value); - } - if (command.present) { - map['command'] = Variable(command.value); - } - if (localCreatedAt.present) { - map['local_created_at'] = Variable(localCreatedAt.value); - } - if (remoteCreatedAt.present) { - map['remote_created_at'] = Variable(remoteCreatedAt.value); - } - if (localUpdatedAt.present) { - map['local_updated_at'] = Variable(localUpdatedAt.value); - } - if (remoteUpdatedAt.present) { - map['remote_updated_at'] = Variable(remoteUpdatedAt.value); + if (optionId.present) { + map['option_id'] = Variable(optionId.value); } - if (localDeletedAt.present) { - map['local_deleted_at'] = Variable(localDeletedAt.value); + if (answerText.present) { + map['answer_text'] = Variable(answerText.value); } - if (remoteDeletedAt.present) { - map['remote_deleted_at'] = Variable(remoteDeletedAt.value); + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); } - if (messageTextUpdatedAt.present) { - map['message_text_updated_at'] = - Variable(messageTextUpdatedAt.value); + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); } if (userId.present) { map['user_id'] = Variable(userId.value); } - if (pinned.present) { - map['pinned'] = Variable(pinned.value); - } - if (pinnedAt.present) { - map['pinned_at'] = Variable(pinnedAt.value); - } - if (pinExpires.present) { - map['pin_expires'] = Variable(pinExpires.value); - } - if (pinnedByUserId.present) { - map['pinned_by_user_id'] = Variable(pinnedByUserId.value); - } - if (channelCid.present) { - map['channel_cid'] = Variable(channelCid.value); - } - if (i18n.present) { - map['i18n'] = Variable( - $PinnedMessagesTable.$converteri18n.toSql(i18n.value)); - } - if (restrictedVisibility.present) { - map['restricted_visibility'] = Variable($PinnedMessagesTable - .$converterrestrictedVisibilityn - .toSql(restrictedVisibility.value)); - } - if (extraData.present) { - map['extra_data'] = Variable( - $PinnedMessagesTable.$converterextraDatan.toSql(extraData.value)); - } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -4135,152 +5882,50 @@ class PinnedMessagesCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('PinnedMessagesCompanion(') + return (StringBuffer('PollVotesCompanion(') ..write('id: $id, ') - ..write('messageText: $messageText, ') - ..write('attachments: $attachments, ') - ..write('state: $state, ') - ..write('type: $type, ') - ..write('mentionedUsers: $mentionedUsers, ') - ..write('reactionGroups: $reactionGroups, ') - ..write('parentId: $parentId, ') - ..write('quotedMessageId: $quotedMessageId, ') ..write('pollId: $pollId, ') - ..write('replyCount: $replyCount, ') - ..write('showInChannel: $showInChannel, ') - ..write('shadowed: $shadowed, ') - ..write('command: $command, ') - ..write('localCreatedAt: $localCreatedAt, ') - ..write('remoteCreatedAt: $remoteCreatedAt, ') - ..write('localUpdatedAt: $localUpdatedAt, ') - ..write('remoteUpdatedAt: $remoteUpdatedAt, ') - ..write('localDeletedAt: $localDeletedAt, ') - ..write('remoteDeletedAt: $remoteDeletedAt, ') - ..write('messageTextUpdatedAt: $messageTextUpdatedAt, ') - ..write('userId: $userId, ') - ..write('pinned: $pinned, ') - ..write('pinnedAt: $pinnedAt, ') - ..write('pinExpires: $pinExpires, ') - ..write('pinnedByUserId: $pinnedByUserId, ') - ..write('channelCid: $channelCid, ') - ..write('i18n: $i18n, ') - ..write('restrictedVisibility: $restrictedVisibility, ') - ..write('extraData: $extraData, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); - } -} - -class $PollsTable extends Polls with TableInfo<$PollsTable, PollEntity> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $PollsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _nameMeta = const VerificationMeta('name'); - @override - late final GeneratedColumn name = GeneratedColumn( - 'name', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _descriptionMeta = - const VerificationMeta('description'); - @override - late final GeneratedColumn description = GeneratedColumn( - 'description', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - @override - late final GeneratedColumnWithTypeConverter, String> options = - GeneratedColumn('options', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>($PollsTable.$converteroptions); - @override - late final GeneratedColumnWithTypeConverter - votingVisibility = GeneratedColumn( - 'voting_visibility', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultValue: const Constant('public')) - .withConverter( - $PollsTable.$convertervotingVisibility); - static const VerificationMeta _enforceUniqueVoteMeta = - const VerificationMeta('enforceUniqueVote'); - @override - late final GeneratedColumn enforceUniqueVote = GeneratedColumn( - 'enforce_unique_vote', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("enforce_unique_vote" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _maxVotesAllowedMeta = - const VerificationMeta('maxVotesAllowed'); - @override - late final GeneratedColumn maxVotesAllowed = GeneratedColumn( - 'max_votes_allowed', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - static const VerificationMeta _allowUserSuggestedOptionsMeta = - const VerificationMeta('allowUserSuggestedOptions'); - @override - late final GeneratedColumn allowUserSuggestedOptions = - GeneratedColumn('allow_user_suggested_options', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("allow_user_suggested_options" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _allowAnswersMeta = - const VerificationMeta('allowAnswers'); - @override - late final GeneratedColumn allowAnswers = GeneratedColumn( - 'allow_answers', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("allow_answers" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _isClosedMeta = - const VerificationMeta('isClosed'); - @override - late final GeneratedColumn isClosed = GeneratedColumn( - 'is_closed', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("is_closed" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _answersCountMeta = - const VerificationMeta('answersCount'); + ..write('optionId: $optionId, ') + ..write('answerText: $answerText, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('userId: $userId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $PinnedMessageReactionsTable extends PinnedMessageReactions + with TableInfo<$PinnedMessageReactionsTable, PinnedMessageReactionEntity> { @override - late final GeneratedColumn answersCount = GeneratedColumn( - 'answers_count', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const Constant(0)); + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PinnedMessageReactionsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); @override - late final GeneratedColumnWithTypeConverter, String> - voteCountsByOption = GeneratedColumn( - 'vote_counts_by_option', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>( - $PollsTable.$convertervoteCountsByOption); - static const VerificationMeta _voteCountMeta = - const VerificationMeta('voteCount'); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); @override - late final GeneratedColumn voteCount = GeneratedColumn( - 'vote_count', aliasedName, false, - type: DriftSqlType.int, + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false, - defaultValue: const Constant(0)); - static const VerificationMeta _createdByIdMeta = - const VerificationMeta('createdById'); + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES pinned_messages (id) ON DELETE CASCADE')); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); @override - late final GeneratedColumn createdById = GeneratedColumn( - 'created_by_id', aliasedName, true, + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _emojiCodeMeta = + const VerificationMeta('emojiCode'); + @override + late final GeneratedColumn emojiCode = GeneratedColumn( + 'emoji_code', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @@ -4298,103 +5943,58 @@ class $PollsTable extends Polls with TableInfo<$PollsTable, PollEntity> { type: DriftSqlType.dateTime, requiredDuringInsert: false, defaultValue: currentDateAndTime); + static const VerificationMeta _scoreMeta = const VerificationMeta('score'); + @override + late final GeneratedColumn score = GeneratedColumn( + 'score', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); @override late final GeneratedColumnWithTypeConverter?, String> extraData = GeneratedColumn('extra_data', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false) .withConverter?>( - $PollsTable.$converterextraDatan); + $PinnedMessageReactionsTable.$converterextraDatan); @override List get $columns => [ - id, - name, - description, - options, - votingVisibility, - enforceUniqueVote, - maxVotesAllowed, - allowUserSuggestedOptions, - allowAnswers, - isClosed, - answersCount, - voteCountsByOption, - voteCount, - createdById, + userId, + messageId, + type, + emojiCode, createdAt, updatedAt, + score, extraData ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'polls'; + static const String $name = 'pinned_message_reactions'; @override - VerificationContext validateIntegrity(Insertable instance, + VerificationContext validateIntegrity( + Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } else if (isInserting) { - context.missing(_idMeta); - } - if (data.containsKey('name')) { - context.handle( - _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); - } else if (isInserting) { - context.missing(_nameMeta); - } - if (data.containsKey('description')) { - context.handle( - _descriptionMeta, - description.isAcceptableOrUnknown( - data['description']!, _descriptionMeta)); - } - if (data.containsKey('enforce_unique_vote')) { - context.handle( - _enforceUniqueVoteMeta, - enforceUniqueVote.isAcceptableOrUnknown( - data['enforce_unique_vote']!, _enforceUniqueVoteMeta)); - } - if (data.containsKey('max_votes_allowed')) { - context.handle( - _maxVotesAllowedMeta, - maxVotesAllowed.isAcceptableOrUnknown( - data['max_votes_allowed']!, _maxVotesAllowedMeta)); - } - if (data.containsKey('allow_user_suggested_options')) { - context.handle( - _allowUserSuggestedOptionsMeta, - allowUserSuggestedOptions.isAcceptableOrUnknown( - data['allow_user_suggested_options']!, - _allowUserSuggestedOptionsMeta)); - } - if (data.containsKey('allow_answers')) { - context.handle( - _allowAnswersMeta, - allowAnswers.isAcceptableOrUnknown( - data['allow_answers']!, _allowAnswersMeta)); + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); } - if (data.containsKey('is_closed')) { - context.handle(_isClosedMeta, - isClosed.isAcceptableOrUnknown(data['is_closed']!, _isClosedMeta)); + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); } - if (data.containsKey('answers_count')) { + if (data.containsKey('type')) { context.handle( - _answersCountMeta, - answersCount.isAcceptableOrUnknown( - data['answers_count']!, _answersCountMeta)); - } - if (data.containsKey('vote_count')) { - context.handle(_voteCountMeta, - voteCount.isAcceptableOrUnknown(data['vote_count']!, _voteCountMeta)); + _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + } else if (isInserting) { + context.missing(_typeMeta); } - if (data.containsKey('created_by_id')) { - context.handle( - _createdByIdMeta, - createdById.isAcceptableOrUnknown( - data['created_by_id']!, _createdByIdMeta)); + if (data.containsKey('emoji_code')) { + context.handle(_emojiCodeMeta, + emojiCode.isAcceptableOrUnknown(data['emoji_code']!, _emojiCodeMeta)); } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, @@ -4404,216 +6004,119 @@ class $PollsTable extends Polls with TableInfo<$PollsTable, PollEntity> { context.handle(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); } + if (data.containsKey('score')) { + context.handle( + _scoreMeta, score.isAcceptableOrUnknown(data['score']!, _scoreMeta)); + } return context; } @override - Set get $primaryKey => {id}; + Set get $primaryKey => {messageId, type, userId}; @override - PollEntity map(Map data, {String? tablePrefix}) { + PinnedMessageReactionEntity map(Map data, + {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return PollEntity( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - name: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}name'])!, - description: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}description']), - options: $PollsTable.$converteroptions.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}options'])!), - votingVisibility: $PollsTable.$convertervotingVisibility.fromSql( - attachedDatabase.typeMapping.read(DriftSqlType.string, - data['${effectivePrefix}voting_visibility'])!), - enforceUniqueVote: attachedDatabase.typeMapping.read( - DriftSqlType.bool, data['${effectivePrefix}enforce_unique_vote'])!, - maxVotesAllowed: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}max_votes_allowed']), - allowUserSuggestedOptions: attachedDatabase.typeMapping.read( - DriftSqlType.bool, - data['${effectivePrefix}allow_user_suggested_options'])!, - allowAnswers: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}allow_answers'])!, - isClosed: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}is_closed'])!, - answersCount: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}answers_count'])!, - voteCountsByOption: $PollsTable.$convertervoteCountsByOption.fromSql( - attachedDatabase.typeMapping.read(DriftSqlType.string, - data['${effectivePrefix}vote_counts_by_option'])!), - voteCount: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}vote_count'])!, - createdById: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}created_by_id']), + return PinnedMessageReactionEntity( + userId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_id']), + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id']), + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + emojiCode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}emoji_code']), createdAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, updatedAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, - extraData: $PollsTable.$converterextraDatan.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), + score: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}score'])!, + extraData: $PinnedMessageReactionsTable.$converterextraDatan.fromSql( + attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), ); } @override - $PollsTable createAlias(String alias) { - return $PollsTable(attachedDatabase, alias); + $PinnedMessageReactionsTable createAlias(String alias) { + return $PinnedMessageReactionsTable(attachedDatabase, alias); } - static TypeConverter, String> $converteroptions = - ListConverter(); - static TypeConverter $convertervotingVisibility = - const VotingVisibilityConverter(); - static TypeConverter, String> $convertervoteCountsByOption = - MapConverter(); static TypeConverter, String> $converterextraData = MapConverter(); static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap($converterextraData); } -class PollEntity extends DataClass implements Insertable { - /// The unique identifier of the poll. - final String id; - - /// The name of the poll. - final String name; - - /// The description of the poll. - final String? description; - - /// The list of options available for the poll. - final List options; - - /// Represents the visibility of the voting process. - /// - /// Defaults to 'public'. - final VotingVisibility votingVisibility; - - /// If true, only unique votes are allowed. - /// - /// Defaults to false. - final bool enforceUniqueVote; - - /// The maximum number of votes allowed per user. - final int? maxVotesAllowed; - - /// If true, users can suggest their own options. - /// - /// Defaults to false. - final bool allowUserSuggestedOptions; - - /// If true, users can provide their own answers/comments. - /// - /// Defaults to false. - final bool allowAnswers; - - /// Indicates if the poll is closed. - final bool isClosed; - - /// The total number of answers received by the poll. - final int answersCount; +class PinnedMessageReactionEntity extends DataClass + implements Insertable { + /// The id of the user that sent the reaction + final String? userId; - /// Map of vote counts by option. - final Map voteCountsByOption; + /// The messageId to which the reaction belongs + final String? messageId; - /// The total number of votes received by the poll. - final int voteCount; + /// The type of the reaction + final String type; - /// The id of the user who created the poll. - final String? createdById; + /// The emoji code for the reaction + final String? emojiCode; - /// The date when the poll was created. + /// The DateTime on which the reaction is created final DateTime createdAt; - /// The date when the poll was last updated. + /// The DateTime on which the reaction was last updated final DateTime updatedAt; - /// Map of custom poll extraData + /// The score of the reaction (ie. number of reactions sent) + final int score; + + /// Reaction custom extraData final Map? extraData; - const PollEntity( - {required this.id, - required this.name, - this.description, - required this.options, - required this.votingVisibility, - required this.enforceUniqueVote, - this.maxVotesAllowed, - required this.allowUserSuggestedOptions, - required this.allowAnswers, - required this.isClosed, - required this.answersCount, - required this.voteCountsByOption, - required this.voteCount, - this.createdById, + const PinnedMessageReactionEntity( + {this.userId, + this.messageId, + required this.type, + this.emojiCode, required this.createdAt, required this.updatedAt, + required this.score, this.extraData}); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['id'] = Variable(id); - map['name'] = Variable(name); - if (!nullToAbsent || description != null) { - map['description'] = Variable(description); - } - { - map['options'] = - Variable($PollsTable.$converteroptions.toSql(options)); - } - { - map['voting_visibility'] = Variable( - $PollsTable.$convertervotingVisibility.toSql(votingVisibility)); - } - map['enforce_unique_vote'] = Variable(enforceUniqueVote); - if (!nullToAbsent || maxVotesAllowed != null) { - map['max_votes_allowed'] = Variable(maxVotesAllowed); + if (!nullToAbsent || userId != null) { + map['user_id'] = Variable(userId); } - map['allow_user_suggested_options'] = - Variable(allowUserSuggestedOptions); - map['allow_answers'] = Variable(allowAnswers); - map['is_closed'] = Variable(isClosed); - map['answers_count'] = Variable(answersCount); - { - map['vote_counts_by_option'] = Variable( - $PollsTable.$convertervoteCountsByOption.toSql(voteCountsByOption)); + if (!nullToAbsent || messageId != null) { + map['message_id'] = Variable(messageId); } - map['vote_count'] = Variable(voteCount); - if (!nullToAbsent || createdById != null) { - map['created_by_id'] = Variable(createdById); + map['type'] = Variable(type); + if (!nullToAbsent || emojiCode != null) { + map['emoji_code'] = Variable(emojiCode); } map['created_at'] = Variable(createdAt); map['updated_at'] = Variable(updatedAt); + map['score'] = Variable(score); if (!nullToAbsent || extraData != null) { - map['extra_data'] = - Variable($PollsTable.$converterextraDatan.toSql(extraData)); + map['extra_data'] = Variable( + $PinnedMessageReactionsTable.$converterextraDatan.toSql(extraData)); } return map; } - factory PollEntity.fromJson(Map json, + factory PinnedMessageReactionEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return PollEntity( - id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - description: serializer.fromJson(json['description']), - options: serializer.fromJson>(json['options']), - votingVisibility: - serializer.fromJson(json['votingVisibility']), - enforceUniqueVote: serializer.fromJson(json['enforceUniqueVote']), - maxVotesAllowed: serializer.fromJson(json['maxVotesAllowed']), - allowUserSuggestedOptions: - serializer.fromJson(json['allowUserSuggestedOptions']), - allowAnswers: serializer.fromJson(json['allowAnswers']), - isClosed: serializer.fromJson(json['isClosed']), - answersCount: serializer.fromJson(json['answersCount']), - voteCountsByOption: - serializer.fromJson>(json['voteCountsByOption']), - voteCount: serializer.fromJson(json['voteCount']), - createdById: serializer.fromJson(json['createdById']), + return PinnedMessageReactionEntity( + userId: serializer.fromJson(json['userId']), + messageId: serializer.fromJson(json['messageId']), + type: serializer.fromJson(json['type']), + emojiCode: serializer.fromJson(json['emojiCode']), createdAt: serializer.fromJson(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), + score: serializer.fromJson(json['score']), extraData: serializer.fromJson?>(json['extraData']), ); } @@ -4621,315 +6124,157 @@ class PollEntity extends DataClass implements Insertable { Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'description': serializer.toJson(description), - 'options': serializer.toJson>(options), - 'votingVisibility': serializer.toJson(votingVisibility), - 'enforceUniqueVote': serializer.toJson(enforceUniqueVote), - 'maxVotesAllowed': serializer.toJson(maxVotesAllowed), - 'allowUserSuggestedOptions': - serializer.toJson(allowUserSuggestedOptions), - 'allowAnswers': serializer.toJson(allowAnswers), - 'isClosed': serializer.toJson(isClosed), - 'answersCount': serializer.toJson(answersCount), - 'voteCountsByOption': - serializer.toJson>(voteCountsByOption), - 'voteCount': serializer.toJson(voteCount), - 'createdById': serializer.toJson(createdById), + 'userId': serializer.toJson(userId), + 'messageId': serializer.toJson(messageId), + 'type': serializer.toJson(type), + 'emojiCode': serializer.toJson(emojiCode), 'createdAt': serializer.toJson(createdAt), 'updatedAt': serializer.toJson(updatedAt), + 'score': serializer.toJson(score), 'extraData': serializer.toJson?>(extraData), }; } - PollEntity copyWith( - {String? id, - String? name, - Value description = const Value.absent(), - List? options, - VotingVisibility? votingVisibility, - bool? enforceUniqueVote, - Value maxVotesAllowed = const Value.absent(), - bool? allowUserSuggestedOptions, - bool? allowAnswers, - bool? isClosed, - int? answersCount, - Map? voteCountsByOption, - int? voteCount, - Value createdById = const Value.absent(), + PinnedMessageReactionEntity copyWith( + {Value userId = const Value.absent(), + Value messageId = const Value.absent(), + String? type, + Value emojiCode = const Value.absent(), DateTime? createdAt, DateTime? updatedAt, + int? score, Value?> extraData = const Value.absent()}) => - PollEntity( - id: id ?? this.id, - name: name ?? this.name, - description: description.present ? description.value : this.description, - options: options ?? this.options, - votingVisibility: votingVisibility ?? this.votingVisibility, - enforceUniqueVote: enforceUniqueVote ?? this.enforceUniqueVote, - maxVotesAllowed: maxVotesAllowed.present - ? maxVotesAllowed.value - : this.maxVotesAllowed, - allowUserSuggestedOptions: - allowUserSuggestedOptions ?? this.allowUserSuggestedOptions, - allowAnswers: allowAnswers ?? this.allowAnswers, - isClosed: isClosed ?? this.isClosed, - answersCount: answersCount ?? this.answersCount, - voteCountsByOption: voteCountsByOption ?? this.voteCountsByOption, - voteCount: voteCount ?? this.voteCount, - createdById: createdById.present ? createdById.value : this.createdById, + PinnedMessageReactionEntity( + userId: userId.present ? userId.value : this.userId, + messageId: messageId.present ? messageId.value : this.messageId, + type: type ?? this.type, + emojiCode: emojiCode.present ? emojiCode.value : this.emojiCode, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + score: score ?? this.score, extraData: extraData.present ? extraData.value : this.extraData, ); - PollEntity copyWithCompanion(PollsCompanion data) { - return PollEntity( - id: data.id.present ? data.id.value : this.id, - name: data.name.present ? data.name.value : this.name, - description: - data.description.present ? data.description.value : this.description, - options: data.options.present ? data.options.value : this.options, - votingVisibility: data.votingVisibility.present - ? data.votingVisibility.value - : this.votingVisibility, - enforceUniqueVote: data.enforceUniqueVote.present - ? data.enforceUniqueVote.value - : this.enforceUniqueVote, - maxVotesAllowed: data.maxVotesAllowed.present - ? data.maxVotesAllowed.value - : this.maxVotesAllowed, - allowUserSuggestedOptions: data.allowUserSuggestedOptions.present - ? data.allowUserSuggestedOptions.value - : this.allowUserSuggestedOptions, - allowAnswers: data.allowAnswers.present - ? data.allowAnswers.value - : this.allowAnswers, - isClosed: data.isClosed.present ? data.isClosed.value : this.isClosed, - answersCount: data.answersCount.present - ? data.answersCount.value - : this.answersCount, - voteCountsByOption: data.voteCountsByOption.present - ? data.voteCountsByOption.value - : this.voteCountsByOption, - voteCount: data.voteCount.present ? data.voteCount.value : this.voteCount, - createdById: - data.createdById.present ? data.createdById.value : this.createdById, + PinnedMessageReactionEntity copyWithCompanion( + PinnedMessageReactionsCompanion data) { + return PinnedMessageReactionEntity( + userId: data.userId.present ? data.userId.value : this.userId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + type: data.type.present ? data.type.value : this.type, + emojiCode: data.emojiCode.present ? data.emojiCode.value : this.emojiCode, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + score: data.score.present ? data.score.value : this.score, extraData: data.extraData.present ? data.extraData.value : this.extraData, ); } @override String toString() { - return (StringBuffer('PollEntity(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('description: $description, ') - ..write('options: $options, ') - ..write('votingVisibility: $votingVisibility, ') - ..write('enforceUniqueVote: $enforceUniqueVote, ') - ..write('maxVotesAllowed: $maxVotesAllowed, ') - ..write('allowUserSuggestedOptions: $allowUserSuggestedOptions, ') - ..write('allowAnswers: $allowAnswers, ') - ..write('isClosed: $isClosed, ') - ..write('answersCount: $answersCount, ') - ..write('voteCountsByOption: $voteCountsByOption, ') - ..write('voteCount: $voteCount, ') - ..write('createdById: $createdById, ') + return (StringBuffer('PinnedMessageReactionEntity(') + ..write('userId: $userId, ') + ..write('messageId: $messageId, ') + ..write('type: $type, ') + ..write('emojiCode: $emojiCode, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') + ..write('score: $score, ') ..write('extraData: $extraData') - ..write(')')) - .toString(); - } - - @override - int get hashCode => Object.hash( - id, - name, - description, - options, - votingVisibility, - enforceUniqueVote, - maxVotesAllowed, - allowUserSuggestedOptions, - allowAnswers, - isClosed, - answersCount, - voteCountsByOption, - voteCount, - createdById, - createdAt, - updatedAt, - extraData); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is PollEntity && - other.id == this.id && - other.name == this.name && - other.description == this.description && - other.options == this.options && - other.votingVisibility == this.votingVisibility && - other.enforceUniqueVote == this.enforceUniqueVote && - other.maxVotesAllowed == this.maxVotesAllowed && - other.allowUserSuggestedOptions == this.allowUserSuggestedOptions && - other.allowAnswers == this.allowAnswers && - other.isClosed == this.isClosed && - other.answersCount == this.answersCount && - other.voteCountsByOption == this.voteCountsByOption && - other.voteCount == this.voteCount && - other.createdById == this.createdById && + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userId, messageId, type, emojiCode, createdAt, + updatedAt, score, extraData); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PinnedMessageReactionEntity && + other.userId == this.userId && + other.messageId == this.messageId && + other.type == this.type && + other.emojiCode == this.emojiCode && other.createdAt == this.createdAt && other.updatedAt == this.updatedAt && + other.score == this.score && other.extraData == this.extraData); } -class PollsCompanion extends UpdateCompanion { - final Value id; - final Value name; - final Value description; - final Value> options; - final Value votingVisibility; - final Value enforceUniqueVote; - final Value maxVotesAllowed; - final Value allowUserSuggestedOptions; - final Value allowAnswers; - final Value isClosed; - final Value answersCount; - final Value> voteCountsByOption; - final Value voteCount; - final Value createdById; +class PinnedMessageReactionsCompanion + extends UpdateCompanion { + final Value userId; + final Value messageId; + final Value type; + final Value emojiCode; final Value createdAt; final Value updatedAt; + final Value score; final Value?> extraData; final Value rowid; - const PollsCompanion({ - this.id = const Value.absent(), - this.name = const Value.absent(), - this.description = const Value.absent(), - this.options = const Value.absent(), - this.votingVisibility = const Value.absent(), - this.enforceUniqueVote = const Value.absent(), - this.maxVotesAllowed = const Value.absent(), - this.allowUserSuggestedOptions = const Value.absent(), - this.allowAnswers = const Value.absent(), - this.isClosed = const Value.absent(), - this.answersCount = const Value.absent(), - this.voteCountsByOption = const Value.absent(), - this.voteCount = const Value.absent(), - this.createdById = const Value.absent(), + const PinnedMessageReactionsCompanion({ + this.userId = const Value.absent(), + this.messageId = const Value.absent(), + this.type = const Value.absent(), + this.emojiCode = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), + this.score = const Value.absent(), this.extraData = const Value.absent(), this.rowid = const Value.absent(), }); - PollsCompanion.insert({ - required String id, - required String name, - this.description = const Value.absent(), - required List options, - this.votingVisibility = const Value.absent(), - this.enforceUniqueVote = const Value.absent(), - this.maxVotesAllowed = const Value.absent(), - this.allowUserSuggestedOptions = const Value.absent(), - this.allowAnswers = const Value.absent(), - this.isClosed = const Value.absent(), - this.answersCount = const Value.absent(), - required Map voteCountsByOption, - this.voteCount = const Value.absent(), - this.createdById = const Value.absent(), + PinnedMessageReactionsCompanion.insert({ + this.userId = const Value.absent(), + this.messageId = const Value.absent(), + required String type, + this.emojiCode = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), + this.score = const Value.absent(), this.extraData = const Value.absent(), this.rowid = const Value.absent(), - }) : id = Value(id), - name = Value(name), - options = Value(options), - voteCountsByOption = Value(voteCountsByOption); - static Insertable custom({ - Expression? id, - Expression? name, - Expression? description, - Expression? options, - Expression? votingVisibility, - Expression? enforceUniqueVote, - Expression? maxVotesAllowed, - Expression? allowUserSuggestedOptions, - Expression? allowAnswers, - Expression? isClosed, - Expression? answersCount, - Expression? voteCountsByOption, - Expression? voteCount, - Expression? createdById, + }) : type = Value(type); + static Insertable custom({ + Expression? userId, + Expression? messageId, + Expression? type, + Expression? emojiCode, Expression? createdAt, Expression? updatedAt, + Expression? score, Expression? extraData, Expression? rowid, }) { return RawValuesInsertable({ - if (id != null) 'id': id, - if (name != null) 'name': name, - if (description != null) 'description': description, - if (options != null) 'options': options, - if (votingVisibility != null) 'voting_visibility': votingVisibility, - if (enforceUniqueVote != null) 'enforce_unique_vote': enforceUniqueVote, - if (maxVotesAllowed != null) 'max_votes_allowed': maxVotesAllowed, - if (allowUserSuggestedOptions != null) - 'allow_user_suggested_options': allowUserSuggestedOptions, - if (allowAnswers != null) 'allow_answers': allowAnswers, - if (isClosed != null) 'is_closed': isClosed, - if (answersCount != null) 'answers_count': answersCount, - if (voteCountsByOption != null) - 'vote_counts_by_option': voteCountsByOption, - if (voteCount != null) 'vote_count': voteCount, - if (createdById != null) 'created_by_id': createdById, + if (userId != null) 'user_id': userId, + if (messageId != null) 'message_id': messageId, + if (type != null) 'type': type, + if (emojiCode != null) 'emoji_code': emojiCode, if (createdAt != null) 'created_at': createdAt, if (updatedAt != null) 'updated_at': updatedAt, + if (score != null) 'score': score, if (extraData != null) 'extra_data': extraData, if (rowid != null) 'rowid': rowid, }); } - PollsCompanion copyWith( - {Value? id, - Value? name, - Value? description, - Value>? options, - Value? votingVisibility, - Value? enforceUniqueVote, - Value? maxVotesAllowed, - Value? allowUserSuggestedOptions, - Value? allowAnswers, - Value? isClosed, - Value? answersCount, - Value>? voteCountsByOption, - Value? voteCount, - Value? createdById, + PinnedMessageReactionsCompanion copyWith( + {Value? userId, + Value? messageId, + Value? type, + Value? emojiCode, Value? createdAt, Value? updatedAt, + Value? score, Value?>? extraData, Value? rowid}) { - return PollsCompanion( - id: id ?? this.id, - name: name ?? this.name, - description: description ?? this.description, - options: options ?? this.options, - votingVisibility: votingVisibility ?? this.votingVisibility, - enforceUniqueVote: enforceUniqueVote ?? this.enforceUniqueVote, - maxVotesAllowed: maxVotesAllowed ?? this.maxVotesAllowed, - allowUserSuggestedOptions: - allowUserSuggestedOptions ?? this.allowUserSuggestedOptions, - allowAnswers: allowAnswers ?? this.allowAnswers, - isClosed: isClosed ?? this.isClosed, - answersCount: answersCount ?? this.answersCount, - voteCountsByOption: voteCountsByOption ?? this.voteCountsByOption, - voteCount: voteCount ?? this.voteCount, - createdById: createdById ?? this.createdById, + return PinnedMessageReactionsCompanion( + userId: userId ?? this.userId, + messageId: messageId ?? this.messageId, + type: type ?? this.type, + emojiCode: emojiCode ?? this.emojiCode, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + score: score ?? this.score, extraData: extraData ?? this.extraData, rowid: rowid ?? this.rowid, ); @@ -4938,52 +6283,17 @@ class PollsCompanion extends UpdateCompanion { @override Map toColumns(bool nullToAbsent) { final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (name.present) { - map['name'] = Variable(name.value); - } - if (description.present) { - map['description'] = Variable(description.value); - } - if (options.present) { - map['options'] = - Variable($PollsTable.$converteroptions.toSql(options.value)); - } - if (votingVisibility.present) { - map['voting_visibility'] = Variable( - $PollsTable.$convertervotingVisibility.toSql(votingVisibility.value)); - } - if (enforceUniqueVote.present) { - map['enforce_unique_vote'] = Variable(enforceUniqueVote.value); - } - if (maxVotesAllowed.present) { - map['max_votes_allowed'] = Variable(maxVotesAllowed.value); - } - if (allowUserSuggestedOptions.present) { - map['allow_user_suggested_options'] = - Variable(allowUserSuggestedOptions.value); - } - if (allowAnswers.present) { - map['allow_answers'] = Variable(allowAnswers.value); - } - if (isClosed.present) { - map['is_closed'] = Variable(isClosed.value); - } - if (answersCount.present) { - map['answers_count'] = Variable(answersCount.value); + if (userId.present) { + map['user_id'] = Variable(userId.value); } - if (voteCountsByOption.present) { - map['vote_counts_by_option'] = Variable($PollsTable - .$convertervoteCountsByOption - .toSql(voteCountsByOption.value)); + if (messageId.present) { + map['message_id'] = Variable(messageId.value); } - if (voteCount.present) { - map['vote_count'] = Variable(voteCount.value); + if (type.present) { + map['type'] = Variable(type.value); } - if (createdById.present) { - map['created_by_id'] = Variable(createdById.value); + if (emojiCode.present) { + map['emoji_code'] = Variable(emojiCode.value); } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); @@ -4991,9 +6301,13 @@ class PollsCompanion extends UpdateCompanion { if (updatedAt.present) { map['updated_at'] = Variable(updatedAt.value); } + if (score.present) { + map['score'] = Variable(score.value); + } if (extraData.present) { - map['extra_data'] = Variable( - $PollsTable.$converterextraDatan.toSql(extraData.value)); + map['extra_data'] = Variable($PinnedMessageReactionsTable + .$converterextraDatan + .toSql(extraData.value)); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -5003,23 +6317,14 @@ class PollsCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('PollsCompanion(') - ..write('id: $id, ') - ..write('name: $name, ') - ..write('description: $description, ') - ..write('options: $options, ') - ..write('votingVisibility: $votingVisibility, ') - ..write('enforceUniqueVote: $enforceUniqueVote, ') - ..write('maxVotesAllowed: $maxVotesAllowed, ') - ..write('allowUserSuggestedOptions: $allowUserSuggestedOptions, ') - ..write('allowAnswers: $allowAnswers, ') - ..write('isClosed: $isClosed, ') - ..write('answersCount: $answersCount, ') - ..write('voteCountsByOption: $voteCountsByOption, ') - ..write('voteCount: $voteCount, ') - ..write('createdById: $createdById, ') + return (StringBuffer('PinnedMessageReactionsCompanion(') + ..write('userId: $userId, ') + ..write('messageId: $messageId, ') + ..write('type: $type, ') + ..write('emojiCode: $emojiCode, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') + ..write('score: $score, ') ..write('extraData: $extraData, ') ..write('rowid: $rowid') ..write(')')) @@ -5027,36 +6332,36 @@ class PollsCompanion extends UpdateCompanion { } } -class $PollVotesTable extends PollVotes - with TableInfo<$PollVotesTable, PollVoteEntity> { +class $ReactionsTable extends Reactions + with TableInfo<$ReactionsTable, ReactionEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $PollVotesTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); + $ReactionsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, true, + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _pollIdMeta = const VerificationMeta('pollId'); + static const VerificationMeta _messageIdMeta = + const VerificationMeta('messageId'); @override - late final GeneratedColumn pollId = GeneratedColumn( - 'poll_id', aliasedName, true, + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES polls (id) ON DELETE CASCADE')); - static const VerificationMeta _optionIdMeta = - const VerificationMeta('optionId'); + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES messages (id) ON DELETE CASCADE')); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); @override - late final GeneratedColumn optionId = GeneratedColumn( - 'option_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _answerTextMeta = - const VerificationMeta('answerText'); + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _emojiCodeMeta = + const VerificationMeta('emojiCode'); @override - late final GeneratedColumn answerText = GeneratedColumn( - 'answer_text', aliasedName, true, + late final GeneratedColumn emojiCode = GeneratedColumn( + 'emoji_code', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @@ -5074,40 +6379,57 @@ class $PollVotesTable extends PollVotes type: DriftSqlType.dateTime, requiredDuringInsert: false, defaultValue: currentDateAndTime); - static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + static const VerificationMeta _scoreMeta = const VerificationMeta('score'); @override - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn score = GeneratedColumn( + 'score', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0)); @override - List get $columns => - [id, pollId, optionId, answerText, createdAt, updatedAt, userId]; + late final GeneratedColumnWithTypeConverter?, String> + extraData = GeneratedColumn('extra_data', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $ReactionsTable.$converterextraDatan); + @override + List get $columns => [ + userId, + messageId, + type, + emojiCode, + createdAt, + updatedAt, + score, + extraData + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'poll_votes'; + static const String $name = 'reactions'; @override - VerificationContext validateIntegrity(Insertable instance, + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } - if (data.containsKey('poll_id')) { - context.handle(_pollIdMeta, - pollId.isAcceptableOrUnknown(data['poll_id']!, _pollIdMeta)); + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); } - if (data.containsKey('option_id')) { - context.handle(_optionIdMeta, - optionId.isAcceptableOrUnknown(data['option_id']!, _optionIdMeta)); + if (data.containsKey('message_id')) { + context.handle(_messageIdMeta, + messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); } - if (data.containsKey('answer_text')) { + if (data.containsKey('type')) { context.handle( - _answerTextMeta, - answerText.isAcceptableOrUnknown( - data['answer_text']!, _answerTextMeta)); + _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + } else if (isInserting) { + context.missing(_typeMeta); + } + if (data.containsKey('emoji_code')) { + context.handle(_emojiCodeMeta, + emojiCode.isAcceptableOrUnknown(data['emoji_code']!, _emojiCodeMeta)); } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, @@ -5117,255 +6439,274 @@ class $PollVotesTable extends PollVotes context.handle(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); } - if (data.containsKey('user_id')) { - context.handle(_userIdMeta, - userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + if (data.containsKey('score')) { + context.handle( + _scoreMeta, score.isAcceptableOrUnknown(data['score']!, _scoreMeta)); } return context; } @override - Set get $primaryKey => {id, pollId}; + Set get $primaryKey => {messageId, type, userId}; @override - PollVoteEntity map(Map data, {String? tablePrefix}) { + ReactionEntity map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return PollVoteEntity( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id']), - pollId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}poll_id']), - optionId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}option_id']), - answerText: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}answer_text']), + return ReactionEntity( + userId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_id']), + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id']), + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + emojiCode: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}emoji_code']), createdAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, updatedAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, - userId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}user_id']), + score: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}score'])!, + extraData: $ReactionsTable.$converterextraDatan.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), ); } @override - $PollVotesTable createAlias(String alias) { - return $PollVotesTable(attachedDatabase, alias); + $ReactionsTable createAlias(String alias) { + return $ReactionsTable(attachedDatabase, alias); } + + static TypeConverter, String> $converterextraData = + MapConverter(); + static TypeConverter?, String?> $converterextraDatan = + NullAwareTypeConverter.wrap($converterextraData); } -class PollVoteEntity extends DataClass implements Insertable { - /// The unique identifier of the poll vote. - final String? id; +class ReactionEntity extends DataClass implements Insertable { + /// The id of the user that sent the reaction + final String? userId; - /// The unique identifier of the poll the vote belongs to. - final String? pollId; + /// The messageId to which the reaction belongs + final String? messageId; - /// The unique identifier of the option selected in the poll. - /// - /// Nullable if the user provided an answer. - final String? optionId; + /// The type of the reaction + final String type; - /// The text of the answer provided in the poll. - /// - /// Nullable if the user selected an option. - final String? answerText; + /// The emoji code for the reaction + final String? emojiCode; - /// The date when the poll vote was created. + /// The DateTime on which the reaction is created final DateTime createdAt; - /// The date when the poll vote was last updated. + /// The DateTime on which the reaction was last updated final DateTime updatedAt; - /// The unique identifier of the user who voted. - /// - /// Nullable if the poll is anonymous. - final String? userId; - const PollVoteEntity( - {this.id, - this.pollId, - this.optionId, - this.answerText, + /// The score of the reaction (ie. number of reactions sent) + final int score; + + /// Reaction custom extraData + final Map? extraData; + const ReactionEntity( + {this.userId, + this.messageId, + required this.type, + this.emojiCode, required this.createdAt, required this.updatedAt, - this.userId}); + required this.score, + this.extraData}); @override Map toColumns(bool nullToAbsent) { final map = {}; - if (!nullToAbsent || id != null) { - map['id'] = Variable(id); - } - if (!nullToAbsent || pollId != null) { - map['poll_id'] = Variable(pollId); + if (!nullToAbsent || userId != null) { + map['user_id'] = Variable(userId); } - if (!nullToAbsent || optionId != null) { - map['option_id'] = Variable(optionId); + if (!nullToAbsent || messageId != null) { + map['message_id'] = Variable(messageId); } - if (!nullToAbsent || answerText != null) { - map['answer_text'] = Variable(answerText); + map['type'] = Variable(type); + if (!nullToAbsent || emojiCode != null) { + map['emoji_code'] = Variable(emojiCode); } map['created_at'] = Variable(createdAt); map['updated_at'] = Variable(updatedAt); - if (!nullToAbsent || userId != null) { - map['user_id'] = Variable(userId); + map['score'] = Variable(score); + if (!nullToAbsent || extraData != null) { + map['extra_data'] = Variable( + $ReactionsTable.$converterextraDatan.toSql(extraData)); } return map; } - factory PollVoteEntity.fromJson(Map json, + factory ReactionEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return PollVoteEntity( - id: serializer.fromJson(json['id']), - pollId: serializer.fromJson(json['pollId']), - optionId: serializer.fromJson(json['optionId']), - answerText: serializer.fromJson(json['answerText']), + return ReactionEntity( + userId: serializer.fromJson(json['userId']), + messageId: serializer.fromJson(json['messageId']), + type: serializer.fromJson(json['type']), + emojiCode: serializer.fromJson(json['emojiCode']), createdAt: serializer.fromJson(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), - userId: serializer.fromJson(json['userId']), + score: serializer.fromJson(json['score']), + extraData: serializer.fromJson?>(json['extraData']), ); } @override Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'id': serializer.toJson(id), - 'pollId': serializer.toJson(pollId), - 'optionId': serializer.toJson(optionId), - 'answerText': serializer.toJson(answerText), + 'userId': serializer.toJson(userId), + 'messageId': serializer.toJson(messageId), + 'type': serializer.toJson(type), + 'emojiCode': serializer.toJson(emojiCode), 'createdAt': serializer.toJson(createdAt), 'updatedAt': serializer.toJson(updatedAt), - 'userId': serializer.toJson(userId), + 'score': serializer.toJson(score), + 'extraData': serializer.toJson?>(extraData), }; } - PollVoteEntity copyWith( - {Value id = const Value.absent(), - Value pollId = const Value.absent(), - Value optionId = const Value.absent(), - Value answerText = const Value.absent(), + ReactionEntity copyWith( + {Value userId = const Value.absent(), + Value messageId = const Value.absent(), + String? type, + Value emojiCode = const Value.absent(), DateTime? createdAt, DateTime? updatedAt, - Value userId = const Value.absent()}) => - PollVoteEntity( - id: id.present ? id.value : this.id, - pollId: pollId.present ? pollId.value : this.pollId, - optionId: optionId.present ? optionId.value : this.optionId, - answerText: answerText.present ? answerText.value : this.answerText, + int? score, + Value?> extraData = const Value.absent()}) => + ReactionEntity( + userId: userId.present ? userId.value : this.userId, + messageId: messageId.present ? messageId.value : this.messageId, + type: type ?? this.type, + emojiCode: emojiCode.present ? emojiCode.value : this.emojiCode, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, - userId: userId.present ? userId.value : this.userId, + score: score ?? this.score, + extraData: extraData.present ? extraData.value : this.extraData, ); - PollVoteEntity copyWithCompanion(PollVotesCompanion data) { - return PollVoteEntity( - id: data.id.present ? data.id.value : this.id, - pollId: data.pollId.present ? data.pollId.value : this.pollId, - optionId: data.optionId.present ? data.optionId.value : this.optionId, - answerText: - data.answerText.present ? data.answerText.value : this.answerText, + ReactionEntity copyWithCompanion(ReactionsCompanion data) { + return ReactionEntity( + userId: data.userId.present ? data.userId.value : this.userId, + messageId: data.messageId.present ? data.messageId.value : this.messageId, + type: data.type.present ? data.type.value : this.type, + emojiCode: data.emojiCode.present ? data.emojiCode.value : this.emojiCode, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, - userId: data.userId.present ? data.userId.value : this.userId, + score: data.score.present ? data.score.value : this.score, + extraData: data.extraData.present ? data.extraData.value : this.extraData, ); } @override String toString() { - return (StringBuffer('PollVoteEntity(') - ..write('id: $id, ') - ..write('pollId: $pollId, ') - ..write('optionId: $optionId, ') - ..write('answerText: $answerText, ') + return (StringBuffer('ReactionEntity(') + ..write('userId: $userId, ') + ..write('messageId: $messageId, ') + ..write('type: $type, ') + ..write('emojiCode: $emojiCode, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') - ..write('userId: $userId') + ..write('score: $score, ') + ..write('extraData: $extraData') ..write(')')) .toString(); } @override - int get hashCode => Object.hash( - id, pollId, optionId, answerText, createdAt, updatedAt, userId); + int get hashCode => Object.hash(userId, messageId, type, emojiCode, createdAt, + updatedAt, score, extraData); @override bool operator ==(Object other) => identical(this, other) || - (other is PollVoteEntity && - other.id == this.id && - other.pollId == this.pollId && - other.optionId == this.optionId && - other.answerText == this.answerText && + (other is ReactionEntity && + other.userId == this.userId && + other.messageId == this.messageId && + other.type == this.type && + other.emojiCode == this.emojiCode && other.createdAt == this.createdAt && other.updatedAt == this.updatedAt && - other.userId == this.userId); -} - -class PollVotesCompanion extends UpdateCompanion { - final Value id; - final Value pollId; - final Value optionId; - final Value answerText; + other.score == this.score && + other.extraData == this.extraData); +} + +class ReactionsCompanion extends UpdateCompanion { + final Value userId; + final Value messageId; + final Value type; + final Value emojiCode; final Value createdAt; final Value updatedAt; - final Value userId; + final Value score; + final Value?> extraData; final Value rowid; - const PollVotesCompanion({ - this.id = const Value.absent(), - this.pollId = const Value.absent(), - this.optionId = const Value.absent(), - this.answerText = const Value.absent(), + const ReactionsCompanion({ + this.userId = const Value.absent(), + this.messageId = const Value.absent(), + this.type = const Value.absent(), + this.emojiCode = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), - this.userId = const Value.absent(), + this.score = const Value.absent(), + this.extraData = const Value.absent(), this.rowid = const Value.absent(), }); - PollVotesCompanion.insert({ - this.id = const Value.absent(), - this.pollId = const Value.absent(), - this.optionId = const Value.absent(), - this.answerText = const Value.absent(), + ReactionsCompanion.insert({ + this.userId = const Value.absent(), + this.messageId = const Value.absent(), + required String type, + this.emojiCode = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), - this.userId = const Value.absent(), + this.score = const Value.absent(), + this.extraData = const Value.absent(), this.rowid = const Value.absent(), - }); - static Insertable custom({ - Expression? id, - Expression? pollId, - Expression? optionId, - Expression? answerText, + }) : type = Value(type); + static Insertable custom({ + Expression? userId, + Expression? messageId, + Expression? type, + Expression? emojiCode, Expression? createdAt, Expression? updatedAt, - Expression? userId, + Expression? score, + Expression? extraData, Expression? rowid, }) { return RawValuesInsertable({ - if (id != null) 'id': id, - if (pollId != null) 'poll_id': pollId, - if (optionId != null) 'option_id': optionId, - if (answerText != null) 'answer_text': answerText, + if (userId != null) 'user_id': userId, + if (messageId != null) 'message_id': messageId, + if (type != null) 'type': type, + if (emojiCode != null) 'emoji_code': emojiCode, if (createdAt != null) 'created_at': createdAt, if (updatedAt != null) 'updated_at': updatedAt, - if (userId != null) 'user_id': userId, + if (score != null) 'score': score, + if (extraData != null) 'extra_data': extraData, if (rowid != null) 'rowid': rowid, }); } - PollVotesCompanion copyWith( - {Value? id, - Value? pollId, - Value? optionId, - Value? answerText, + ReactionsCompanion copyWith( + {Value? userId, + Value? messageId, + Value? type, + Value? emojiCode, Value? createdAt, Value? updatedAt, - Value? userId, + Value? score, + Value?>? extraData, Value? rowid}) { - return PollVotesCompanion( - id: id ?? this.id, - pollId: pollId ?? this.pollId, - optionId: optionId ?? this.optionId, - answerText: answerText ?? this.answerText, + return ReactionsCompanion( + userId: userId ?? this.userId, + messageId: messageId ?? this.messageId, + type: type ?? this.type, + emojiCode: emojiCode ?? this.emojiCode, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, - userId: userId ?? this.userId, + score: score ?? this.score, + extraData: extraData ?? this.extraData, rowid: rowid ?? this.rowid, ); } @@ -5373,17 +6714,17 @@ class PollVotesCompanion extends UpdateCompanion { @override Map toColumns(bool nullToAbsent) { final map = {}; - if (id.present) { - map['id'] = Variable(id.value); + if (userId.present) { + map['user_id'] = Variable(userId.value); } - if (pollId.present) { - map['poll_id'] = Variable(pollId.value); + if (messageId.present) { + map['message_id'] = Variable(messageId.value); } - if (optionId.present) { - map['option_id'] = Variable(optionId.value); + if (type.present) { + map['type'] = Variable(type.value); } - if (answerText.present) { - map['answer_text'] = Variable(answerText.value); + if (emojiCode.present) { + map['emoji_code'] = Variable(emojiCode.value); } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); @@ -5391,8 +6732,12 @@ class PollVotesCompanion extends UpdateCompanion { if (updatedAt.present) { map['updated_at'] = Variable(updatedAt.value); } - if (userId.present) { - map['user_id'] = Variable(userId.value); + if (score.present) { + map['score'] = Variable(score.value); + } + if (extraData.present) { + map['extra_data'] = Variable( + $ReactionsTable.$converterextraDatan.toSql(extraData.value)); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -5402,327 +6747,506 @@ class PollVotesCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('PollVotesCompanion(') - ..write('id: $id, ') - ..write('pollId: $pollId, ') - ..write('optionId: $optionId, ') - ..write('answerText: $answerText, ') + return (StringBuffer('ReactionsCompanion(') + ..write('userId: $userId, ') + ..write('messageId: $messageId, ') + ..write('type: $type, ') + ..write('emojiCode: $emojiCode, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') - ..write('userId: $userId, ') + ..write('score: $score, ') + ..write('extraData: $extraData, ') ..write('rowid: $rowid') ..write(')')) .toString(); } } -class $PinnedMessageReactionsTable extends PinnedMessageReactions - with TableInfo<$PinnedMessageReactionsTable, PinnedMessageReactionEntity> { +class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $PinnedMessageReactionsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + $UsersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); @override - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, false, + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _messageIdMeta = - const VerificationMeta('messageId'); + static const VerificationMeta _roleMeta = const VerificationMeta('role'); @override - late final GeneratedColumn messageId = GeneratedColumn( - 'message_id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES pinned_messages (id) ON DELETE CASCADE')); - static const VerificationMeta _typeMeta = const VerificationMeta('type'); + late final GeneratedColumn role = GeneratedColumn( + 'role', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _languageMeta = + const VerificationMeta('language'); @override - late final GeneratedColumn type = GeneratedColumn( - 'type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn language = GeneratedColumn( + 'language', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.dateTime, + 'created_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _updatedAtMeta = + const VerificationMeta('updatedAt'); + @override + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _lastActiveMeta = + const VerificationMeta('lastActive'); + @override + late final GeneratedColumn lastActive = GeneratedColumn( + 'last_active', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _onlineMeta = const VerificationMeta('online'); + @override + late final GeneratedColumn online = GeneratedColumn( + 'online', aliasedName, false, + type: DriftSqlType.bool, requiredDuringInsert: false, - defaultValue: currentDateAndTime); - static const VerificationMeta _scoreMeta = const VerificationMeta('score'); + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("online" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _bannedMeta = const VerificationMeta('banned'); @override - late final GeneratedColumn score = GeneratedColumn( - 'score', aliasedName, false, - type: DriftSqlType.int, + late final GeneratedColumn banned = GeneratedColumn( + 'banned', aliasedName, false, + type: DriftSqlType.bool, requiredDuringInsert: false, - defaultValue: const Constant(0)); + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("banned" IN (0, 1))'), + defaultValue: const Constant(false)); @override - late final GeneratedColumnWithTypeConverter?, String> - extraData = GeneratedColumn('extra_data', aliasedName, true, + late final GeneratedColumnWithTypeConverter?, String> + teamsRole = GeneratedColumn('teams_role', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $PinnedMessageReactionsTable.$converterextraDatan); + .withConverter?>( + $UsersTable.$converterteamsRolen); + static const VerificationMeta _avgResponseTimeMeta = + const VerificationMeta('avgResponseTime'); @override - List get $columns => - [userId, messageId, type, createdAt, score, extraData]; + late final GeneratedColumn avgResponseTime = GeneratedColumn( + 'avg_response_time', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + @override + late final GeneratedColumnWithTypeConverter, String> + extraData = GeneratedColumn('extra_data', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter>($UsersTable.$converterextraData); + @override + List get $columns => [ + id, + role, + language, + createdAt, + updatedAt, + lastActive, + online, + banned, + teamsRole, + avgResponseTime, + extraData + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'pinned_message_reactions'; + static const String $name = 'users'; @override - VerificationContext validateIntegrity( - Insertable instance, + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('user_id')) { - context.handle(_userIdMeta, - userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); - } else if (isInserting) { - context.missing(_userIdMeta); - } - if (data.containsKey('message_id')) { - context.handle(_messageIdMeta, - messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } else if (isInserting) { - context.missing(_messageIdMeta); + context.missing(_idMeta); } - if (data.containsKey('type')) { + if (data.containsKey('role')) { context.handle( - _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); - } else if (isInserting) { - context.missing(_typeMeta); + _roleMeta, role.isAcceptableOrUnknown(data['role']!, _roleMeta)); + } + if (data.containsKey('language')) { + context.handle(_languageMeta, + language.isAcceptableOrUnknown(data['language']!, _languageMeta)); } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); } - if (data.containsKey('score')) { + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('last_active')) { + context.handle( + _lastActiveMeta, + lastActive.isAcceptableOrUnknown( + data['last_active']!, _lastActiveMeta)); + } + if (data.containsKey('online')) { + context.handle(_onlineMeta, + online.isAcceptableOrUnknown(data['online']!, _onlineMeta)); + } + if (data.containsKey('banned')) { + context.handle(_bannedMeta, + banned.isAcceptableOrUnknown(data['banned']!, _bannedMeta)); + } + if (data.containsKey('avg_response_time')) { context.handle( - _scoreMeta, score.isAcceptableOrUnknown(data['score']!, _scoreMeta)); + _avgResponseTimeMeta, + avgResponseTime.isAcceptableOrUnknown( + data['avg_response_time']!, _avgResponseTimeMeta)); } return context; } @override - Set get $primaryKey => {messageId, type, userId}; + Set get $primaryKey => {id}; @override - PinnedMessageReactionEntity map(Map data, - {String? tablePrefix}) { + UserEntity map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return PinnedMessageReactionEntity( - userId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, - messageId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, - type: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + return UserEntity( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + role: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}role']), + language: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}language']), createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - score: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}score'])!, - extraData: $PinnedMessageReactionsTable.$converterextraDatan.fromSql( - attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at']), + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at']), + lastActive: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_active']), + online: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}online'])!, + banned: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}banned'])!, + teamsRole: $UsersTable.$converterteamsRolen.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}teams_role'])), + avgResponseTime: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}avg_response_time']), + extraData: $UsersTable.$converterextraData.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])!), ); } @override - $PinnedMessageReactionsTable createAlias(String alias) { - return $PinnedMessageReactionsTable(attachedDatabase, alias); + $UsersTable createAlias(String alias) { + return $UsersTable(attachedDatabase, alias); } + static TypeConverter, String> $converterteamsRole = + MapConverter(); + static TypeConverter?, String?> $converterteamsRolen = + NullAwareTypeConverter.wrap($converterteamsRole); static TypeConverter, String> $converterextraData = MapConverter(); - static TypeConverter?, String?> $converterextraDatan = - NullAwareTypeConverter.wrap($converterextraData); } -class PinnedMessageReactionEntity extends DataClass - implements Insertable { - /// The id of the user that sent the reaction - final String userId; +class UserEntity extends DataClass implements Insertable { + /// User id + final String id; - /// The messageId to which the reaction belongs - final String messageId; + /// User role + final String? role; - /// The type of the reaction - final String type; + /// The language this user prefers. + final String? language; - /// The DateTime on which the reaction is created - final DateTime createdAt; + /// Date of user creation + final DateTime? createdAt; - /// The score of the reaction (ie. number of reactions sent) - final int score; + /// Date of last user update + final DateTime? updatedAt; - /// Reaction custom extraData - final Map? extraData; - const PinnedMessageReactionEntity( - {required this.userId, - required this.messageId, - required this.type, - required this.createdAt, - required this.score, - this.extraData}); + /// Date of last user connection + final DateTime? lastActive; + + /// True if user is online + final bool online; + + /// True if user is banned from the chat + final bool banned; + + /// The roles for the user in the teams. + /// + /// eg: `{'teamId': 'role', 'teamId2': 'role2'}` + final Map? teamsRole; + + /// The average response time for the user in seconds. + final int? avgResponseTime; + + /// Map of custom user extraData + final Map extraData; + const UserEntity( + {required this.id, + this.role, + this.language, + this.createdAt, + this.updatedAt, + this.lastActive, + required this.online, + required this.banned, + this.teamsRole, + this.avgResponseTime, + required this.extraData}); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['user_id'] = Variable(userId); - map['message_id'] = Variable(messageId); - map['type'] = Variable(type); - map['created_at'] = Variable(createdAt); - map['score'] = Variable(score); - if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $PinnedMessageReactionsTable.$converterextraDatan.toSql(extraData)); + map['id'] = Variable(id); + if (!nullToAbsent || role != null) { + map['role'] = Variable(role); + } + if (!nullToAbsent || language != null) { + map['language'] = Variable(language); + } + if (!nullToAbsent || createdAt != null) { + map['created_at'] = Variable(createdAt); + } + if (!nullToAbsent || updatedAt != null) { + map['updated_at'] = Variable(updatedAt); + } + if (!nullToAbsent || lastActive != null) { + map['last_active'] = Variable(lastActive); + } + map['online'] = Variable(online); + map['banned'] = Variable(banned); + if (!nullToAbsent || teamsRole != null) { + map['teams_role'] = + Variable($UsersTable.$converterteamsRolen.toSql(teamsRole)); + } + if (!nullToAbsent || avgResponseTime != null) { + map['avg_response_time'] = Variable(avgResponseTime); + } + { + map['extra_data'] = + Variable($UsersTable.$converterextraData.toSql(extraData)); } return map; } - factory PinnedMessageReactionEntity.fromJson(Map json, + factory UserEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return PinnedMessageReactionEntity( - userId: serializer.fromJson(json['userId']), - messageId: serializer.fromJson(json['messageId']), - type: serializer.fromJson(json['type']), - createdAt: serializer.fromJson(json['createdAt']), - score: serializer.fromJson(json['score']), - extraData: serializer.fromJson?>(json['extraData']), + return UserEntity( + id: serializer.fromJson(json['id']), + role: serializer.fromJson(json['role']), + language: serializer.fromJson(json['language']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + lastActive: serializer.fromJson(json['lastActive']), + online: serializer.fromJson(json['online']), + banned: serializer.fromJson(json['banned']), + teamsRole: serializer.fromJson?>(json['teamsRole']), + avgResponseTime: serializer.fromJson(json['avgResponseTime']), + extraData: serializer.fromJson>(json['extraData']), ); } @override Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'userId': serializer.toJson(userId), - 'messageId': serializer.toJson(messageId), - 'type': serializer.toJson(type), - 'createdAt': serializer.toJson(createdAt), - 'score': serializer.toJson(score), - 'extraData': serializer.toJson?>(extraData), + 'id': serializer.toJson(id), + 'role': serializer.toJson(role), + 'language': serializer.toJson(language), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'lastActive': serializer.toJson(lastActive), + 'online': serializer.toJson(online), + 'banned': serializer.toJson(banned), + 'teamsRole': serializer.toJson?>(teamsRole), + 'avgResponseTime': serializer.toJson(avgResponseTime), + 'extraData': serializer.toJson>(extraData), }; } - PinnedMessageReactionEntity copyWith( - {String? userId, - String? messageId, - String? type, - DateTime? createdAt, - int? score, - Value?> extraData = const Value.absent()}) => - PinnedMessageReactionEntity( - userId: userId ?? this.userId, - messageId: messageId ?? this.messageId, - type: type ?? this.type, - createdAt: createdAt ?? this.createdAt, - score: score ?? this.score, - extraData: extraData.present ? extraData.value : this.extraData, + UserEntity copyWith( + {String? id, + Value role = const Value.absent(), + Value language = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value lastActive = const Value.absent(), + bool? online, + bool? banned, + Value?> teamsRole = const Value.absent(), + Value avgResponseTime = const Value.absent(), + Map? extraData}) => + UserEntity( + id: id ?? this.id, + role: role.present ? role.value : this.role, + language: language.present ? language.value : this.language, + createdAt: createdAt.present ? createdAt.value : this.createdAt, + updatedAt: updatedAt.present ? updatedAt.value : this.updatedAt, + lastActive: lastActive.present ? lastActive.value : this.lastActive, + online: online ?? this.online, + banned: banned ?? this.banned, + teamsRole: teamsRole.present ? teamsRole.value : this.teamsRole, + avgResponseTime: avgResponseTime.present + ? avgResponseTime.value + : this.avgResponseTime, + extraData: extraData ?? this.extraData, ); - PinnedMessageReactionEntity copyWithCompanion( - PinnedMessageReactionsCompanion data) { - return PinnedMessageReactionEntity( - userId: data.userId.present ? data.userId.value : this.userId, - messageId: data.messageId.present ? data.messageId.value : this.messageId, - type: data.type.present ? data.type.value : this.type, + UserEntity copyWithCompanion(UsersCompanion data) { + return UserEntity( + id: data.id.present ? data.id.value : this.id, + role: data.role.present ? data.role.value : this.role, + language: data.language.present ? data.language.value : this.language, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, - score: data.score.present ? data.score.value : this.score, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + lastActive: + data.lastActive.present ? data.lastActive.value : this.lastActive, + online: data.online.present ? data.online.value : this.online, + banned: data.banned.present ? data.banned.value : this.banned, + teamsRole: data.teamsRole.present ? data.teamsRole.value : this.teamsRole, + avgResponseTime: data.avgResponseTime.present + ? data.avgResponseTime.value + : this.avgResponseTime, extraData: data.extraData.present ? data.extraData.value : this.extraData, ); } @override - String toString() { - return (StringBuffer('PinnedMessageReactionEntity(') - ..write('userId: $userId, ') - ..write('messageId: $messageId, ') - ..write('type: $type, ') + String toString() { + return (StringBuffer('UserEntity(') + ..write('id: $id, ') + ..write('role: $role, ') + ..write('language: $language, ') ..write('createdAt: $createdAt, ') - ..write('score: $score, ') + ..write('updatedAt: $updatedAt, ') + ..write('lastActive: $lastActive, ') + ..write('online: $online, ') + ..write('banned: $banned, ') + ..write('teamsRole: $teamsRole, ') + ..write('avgResponseTime: $avgResponseTime, ') ..write('extraData: $extraData') ..write(')')) .toString(); } @override - int get hashCode => - Object.hash(userId, messageId, type, createdAt, score, extraData); + int get hashCode => Object.hash(id, role, language, createdAt, updatedAt, + lastActive, online, banned, teamsRole, avgResponseTime, extraData); @override bool operator ==(Object other) => identical(this, other) || - (other is PinnedMessageReactionEntity && - other.userId == this.userId && - other.messageId == this.messageId && - other.type == this.type && + (other is UserEntity && + other.id == this.id && + other.role == this.role && + other.language == this.language && other.createdAt == this.createdAt && - other.score == this.score && + other.updatedAt == this.updatedAt && + other.lastActive == this.lastActive && + other.online == this.online && + other.banned == this.banned && + other.teamsRole == this.teamsRole && + other.avgResponseTime == this.avgResponseTime && other.extraData == this.extraData); } -class PinnedMessageReactionsCompanion - extends UpdateCompanion { - final Value userId; - final Value messageId; - final Value type; - final Value createdAt; - final Value score; - final Value?> extraData; +class UsersCompanion extends UpdateCompanion { + final Value id; + final Value role; + final Value language; + final Value createdAt; + final Value updatedAt; + final Value lastActive; + final Value online; + final Value banned; + final Value?> teamsRole; + final Value avgResponseTime; + final Value> extraData; final Value rowid; - const PinnedMessageReactionsCompanion({ - this.userId = const Value.absent(), - this.messageId = const Value.absent(), - this.type = const Value.absent(), + const UsersCompanion({ + this.id = const Value.absent(), + this.role = const Value.absent(), + this.language = const Value.absent(), this.createdAt = const Value.absent(), - this.score = const Value.absent(), + this.updatedAt = const Value.absent(), + this.lastActive = const Value.absent(), + this.online = const Value.absent(), + this.banned = const Value.absent(), + this.teamsRole = const Value.absent(), + this.avgResponseTime = const Value.absent(), this.extraData = const Value.absent(), this.rowid = const Value.absent(), }); - PinnedMessageReactionsCompanion.insert({ - required String userId, - required String messageId, - required String type, + UsersCompanion.insert({ + required String id, + this.role = const Value.absent(), + this.language = const Value.absent(), this.createdAt = const Value.absent(), - this.score = const Value.absent(), - this.extraData = const Value.absent(), + this.updatedAt = const Value.absent(), + this.lastActive = const Value.absent(), + this.online = const Value.absent(), + this.banned = const Value.absent(), + this.teamsRole = const Value.absent(), + this.avgResponseTime = const Value.absent(), + required Map extraData, this.rowid = const Value.absent(), - }) : userId = Value(userId), - messageId = Value(messageId), - type = Value(type); - static Insertable custom({ - Expression? userId, - Expression? messageId, - Expression? type, + }) : id = Value(id), + extraData = Value(extraData); + static Insertable custom({ + Expression? id, + Expression? role, + Expression? language, Expression? createdAt, - Expression? score, + Expression? updatedAt, + Expression? lastActive, + Expression? online, + Expression? banned, + Expression? teamsRole, + Expression? avgResponseTime, Expression? extraData, Expression? rowid, }) { return RawValuesInsertable({ - if (userId != null) 'user_id': userId, - if (messageId != null) 'message_id': messageId, - if (type != null) 'type': type, + if (id != null) 'id': id, + if (role != null) 'role': role, + if (language != null) 'language': language, if (createdAt != null) 'created_at': createdAt, - if (score != null) 'score': score, + if (updatedAt != null) 'updated_at': updatedAt, + if (lastActive != null) 'last_active': lastActive, + if (online != null) 'online': online, + if (banned != null) 'banned': banned, + if (teamsRole != null) 'teams_role': teamsRole, + if (avgResponseTime != null) 'avg_response_time': avgResponseTime, if (extraData != null) 'extra_data': extraData, if (rowid != null) 'rowid': rowid, }); } - PinnedMessageReactionsCompanion copyWith( - {Value? userId, - Value? messageId, - Value? type, - Value? createdAt, - Value? score, - Value?>? extraData, + UsersCompanion copyWith( + {Value? id, + Value? role, + Value? language, + Value? createdAt, + Value? updatedAt, + Value? lastActive, + Value? online, + Value? banned, + Value?>? teamsRole, + Value? avgResponseTime, + Value>? extraData, Value? rowid}) { - return PinnedMessageReactionsCompanion( - userId: userId ?? this.userId, - messageId: messageId ?? this.messageId, - type: type ?? this.type, + return UsersCompanion( + id: id ?? this.id, + role: role ?? this.role, + language: language ?? this.language, createdAt: createdAt ?? this.createdAt, - score: score ?? this.score, + updatedAt: updatedAt ?? this.updatedAt, + lastActive: lastActive ?? this.lastActive, + online: online ?? this.online, + banned: banned ?? this.banned, + teamsRole: teamsRole ?? this.teamsRole, + avgResponseTime: avgResponseTime ?? this.avgResponseTime, extraData: extraData ?? this.extraData, rowid: rowid ?? this.rowid, ); @@ -5731,25 +7255,40 @@ class PinnedMessageReactionsCompanion @override Map toColumns(bool nullToAbsent) { final map = {}; - if (userId.present) { - map['user_id'] = Variable(userId.value); + if (id.present) { + map['id'] = Variable(id.value); } - if (messageId.present) { - map['message_id'] = Variable(messageId.value); + if (role.present) { + map['role'] = Variable(role.value); } - if (type.present) { - map['type'] = Variable(type.value); + if (language.present) { + map['language'] = Variable(language.value); } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } - if (score.present) { - map['score'] = Variable(score.value); + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } + if (lastActive.present) { + map['last_active'] = Variable(lastActive.value); + } + if (online.present) { + map['online'] = Variable(online.value); + } + if (banned.present) { + map['banned'] = Variable(banned.value); + } + if (teamsRole.present) { + map['teams_role'] = Variable( + $UsersTable.$converterteamsRolen.toSql(teamsRole.value)); + } + if (avgResponseTime.present) { + map['avg_response_time'] = Variable(avgResponseTime.value); } if (extraData.present) { - map['extra_data'] = Variable($PinnedMessageReactionsTable - .$converterextraDatan - .toSql(extraData.value)); + map['extra_data'] = Variable( + $UsersTable.$converterextraData.toSql(extraData.value)); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -5758,45 +7297,124 @@ class PinnedMessageReactionsCompanion } @override - String toString() { - return (StringBuffer('PinnedMessageReactionsCompanion(') - ..write('userId: $userId, ') - ..write('messageId: $messageId, ') - ..write('type: $type, ') - ..write('createdAt: $createdAt, ') - ..write('score: $score, ') - ..write('extraData: $extraData, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); - } -} - -class $ReactionsTable extends Reactions - with TableInfo<$ReactionsTable, ReactionEntity> { + String toString() { + return (StringBuffer('UsersCompanion(') + ..write('id: $id, ') + ..write('role: $role, ') + ..write('language: $language, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('lastActive: $lastActive, ') + ..write('online: $online, ') + ..write('banned: $banned, ') + ..write('teamsRole: $teamsRole, ') + ..write('avgResponseTime: $avgResponseTime, ') + ..write('extraData: $extraData, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $MembersTable extends Members + with TableInfo<$MembersTable, MemberEntity> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MembersTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + @override + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _channelCidMeta = + const VerificationMeta('channelCid'); + @override + late final GeneratedColumn channelCid = GeneratedColumn( + 'channel_cid', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES channels (cid) ON DELETE CASCADE')); + static const VerificationMeta _channelRoleMeta = + const VerificationMeta('channelRole'); + @override + late final GeneratedColumn channelRole = GeneratedColumn( + 'channel_role', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _inviteAcceptedAtMeta = + const VerificationMeta('inviteAcceptedAt'); + @override + late final GeneratedColumn inviteAcceptedAt = + GeneratedColumn('invite_accepted_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _inviteRejectedAtMeta = + const VerificationMeta('inviteRejectedAt'); + @override + late final GeneratedColumn inviteRejectedAt = + GeneratedColumn('invite_rejected_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _invitedMeta = + const VerificationMeta('invited'); + @override + late final GeneratedColumn invited = GeneratedColumn( + 'invited', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("invited" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _bannedMeta = const VerificationMeta('banned'); + @override + late final GeneratedColumn banned = GeneratedColumn( + 'banned', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("banned" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _shadowBannedMeta = + const VerificationMeta('shadowBanned'); + @override + late final GeneratedColumn shadowBanned = GeneratedColumn( + 'shadow_banned', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("shadow_banned" IN (0, 1))'), + defaultValue: const Constant(false)); + static const VerificationMeta _pinnedAtMeta = + const VerificationMeta('pinnedAt'); @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $ReactionsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + late final GeneratedColumn pinnedAt = GeneratedColumn( + 'pinned_at', aliasedName, true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const Constant(null)); + static const VerificationMeta _archivedAtMeta = + const VerificationMeta('archivedAt'); @override - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _messageIdMeta = - const VerificationMeta('messageId'); + late final GeneratedColumn archivedAt = GeneratedColumn( + 'archived_at', aliasedName, true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: const Constant(null)); + static const VerificationMeta _isModeratorMeta = + const VerificationMeta('isModerator'); @override - late final GeneratedColumn messageId = GeneratedColumn( - 'message_id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, + late final GeneratedColumn isModerator = GeneratedColumn( + 'is_moderator', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES messages (id) ON DELETE CASCADE')); - static const VerificationMeta _typeMeta = const VerificationMeta('type'); + 'CHECK ("is_moderator" IN (0, 1))'), + defaultValue: const Constant(false)); @override - late final GeneratedColumn type = GeneratedColumn( - 'type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumnWithTypeConverter?, String> + extraData = GeneratedColumn('extra_data', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $MembersTable.$converterextraDatan); static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override @@ -5805,29 +7423,38 @@ class $ReactionsTable extends Reactions type: DriftSqlType.dateTime, requiredDuringInsert: false, defaultValue: currentDateAndTime); - static const VerificationMeta _scoreMeta = const VerificationMeta('score'); + static const VerificationMeta _updatedAtMeta = + const VerificationMeta('updatedAt'); @override - late final GeneratedColumn score = GeneratedColumn( - 'score', aliasedName, false, - type: DriftSqlType.int, + late final GeneratedColumn updatedAt = GeneratedColumn( + 'updated_at', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: false, - defaultValue: const Constant(0)); - @override - late final GeneratedColumnWithTypeConverter?, String> - extraData = GeneratedColumn('extra_data', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $ReactionsTable.$converterextraDatan); + defaultValue: currentDateAndTime); @override - List get $columns => - [userId, messageId, type, createdAt, score, extraData]; + List get $columns => [ + userId, + channelCid, + channelRole, + inviteAcceptedAt, + inviteRejectedAt, + invited, + banned, + shadowBanned, + pinnedAt, + archivedAt, + isModerator, + extraData, + createdAt, + updatedAt + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'reactions'; + static const String $name = 'members'; @override - VerificationContext validateIntegrity(Insertable instance, + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); @@ -5837,54 +7464,114 @@ class $ReactionsTable extends Reactions } else if (isInserting) { context.missing(_userIdMeta); } - if (data.containsKey('message_id')) { - context.handle(_messageIdMeta, - messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); + if (data.containsKey('channel_cid')) { + context.handle( + _channelCidMeta, + channelCid.isAcceptableOrUnknown( + data['channel_cid']!, _channelCidMeta)); } else if (isInserting) { - context.missing(_messageIdMeta); + context.missing(_channelCidMeta); } - if (data.containsKey('type')) { + if (data.containsKey('channel_role')) { context.handle( - _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); - } else if (isInserting) { - context.missing(_typeMeta); + _channelRoleMeta, + channelRole.isAcceptableOrUnknown( + data['channel_role']!, _channelRoleMeta)); + } + if (data.containsKey('invite_accepted_at')) { + context.handle( + _inviteAcceptedAtMeta, + inviteAcceptedAt.isAcceptableOrUnknown( + data['invite_accepted_at']!, _inviteAcceptedAtMeta)); + } + if (data.containsKey('invite_rejected_at')) { + context.handle( + _inviteRejectedAtMeta, + inviteRejectedAt.isAcceptableOrUnknown( + data['invite_rejected_at']!, _inviteRejectedAtMeta)); + } + if (data.containsKey('invited')) { + context.handle(_invitedMeta, + invited.isAcceptableOrUnknown(data['invited']!, _invitedMeta)); + } + if (data.containsKey('banned')) { + context.handle(_bannedMeta, + banned.isAcceptableOrUnknown(data['banned']!, _bannedMeta)); + } + if (data.containsKey('shadow_banned')) { + context.handle( + _shadowBannedMeta, + shadowBanned.isAcceptableOrUnknown( + data['shadow_banned']!, _shadowBannedMeta)); + } + if (data.containsKey('pinned_at')) { + context.handle(_pinnedAtMeta, + pinnedAt.isAcceptableOrUnknown(data['pinned_at']!, _pinnedAtMeta)); + } + if (data.containsKey('archived_at')) { + context.handle( + _archivedAtMeta, + archivedAt.isAcceptableOrUnknown( + data['archived_at']!, _archivedAtMeta)); + } + if (data.containsKey('is_moderator')) { + context.handle( + _isModeratorMeta, + isModerator.isAcceptableOrUnknown( + data['is_moderator']!, _isModeratorMeta)); } if (data.containsKey('created_at')) { context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); } - if (data.containsKey('score')) { - context.handle( - _scoreMeta, score.isAcceptableOrUnknown(data['score']!, _scoreMeta)); + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); } return context; } @override - Set get $primaryKey => {messageId, type, userId}; + Set get $primaryKey => {userId, channelCid}; @override - ReactionEntity map(Map data, {String? tablePrefix}) { + MemberEntity map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return ReactionEntity( + return MemberEntity( userId: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, - messageId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, - type: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}type'])!, - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - score: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}score'])!, - extraData: $ReactionsTable.$converterextraDatan.fromSql(attachedDatabase + channelCid: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, + channelRole: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}channel_role']), + inviteAcceptedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}invite_accepted_at']), + inviteRejectedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, data['${effectivePrefix}invite_rejected_at']), + invited: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}invited'])!, + banned: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}banned'])!, + shadowBanned: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}shadow_banned'])!, + pinnedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}pinned_at']), + archivedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}archived_at']), + isModerator: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_moderator'])!, + extraData: $MembersTable.$converterextraDatan.fromSql(attachedDatabase .typeMapping .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, ); } @override - $ReactionsTable createAlias(String alias) { - return $ReactionsTable(attachedDatabase, alias); + $MembersTable createAlias(String alias) { + return $MembersTable(attachedDatabase, alias); } static TypeConverter, String> $converterextraData = @@ -5893,56 +7580,116 @@ class $ReactionsTable extends Reactions NullAwareTypeConverter.wrap($converterextraData); } -class ReactionEntity extends DataClass implements Insertable { - /// The id of the user that sent the reaction +class MemberEntity extends DataClass implements Insertable { + /// The interested user id final String userId; - /// The messageId to which the reaction belongs - final String messageId; + /// The channel cid of which this user is part of + final String channelCid; - /// The type of the reaction - final String type; + /// The role of the user in the channel + final String? channelRole; - /// The DateTime on which the reaction is created + /// The date on which the user accepted the invite to the channel + final DateTime? inviteAcceptedAt; + + /// The date on which the user rejected the invite to the channel + final DateTime? inviteRejectedAt; + + /// True if the user has been invited to the channel + final bool invited; + + /// True if the member is banned from the channel + final bool banned; + + /// True if the member is shadow banned from the channel + final bool shadowBanned; + + /// The date at which the channel was pinned by the member + final DateTime? pinnedAt; + + /// The date at which the channel was archived by the member + final DateTime? archivedAt; + + /// True if the user is a moderator of the channel + final bool isModerator; + + /// Map of custom member extraData + final Map? extraData; + + /// The date of creation final DateTime createdAt; - /// The score of the reaction (ie. number of reactions sent) - final int score; - - /// Reaction custom extraData - final Map? extraData; - const ReactionEntity( + /// The last date of update + final DateTime updatedAt; + const MemberEntity( {required this.userId, - required this.messageId, - required this.type, + required this.channelCid, + this.channelRole, + this.inviteAcceptedAt, + this.inviteRejectedAt, + required this.invited, + required this.banned, + required this.shadowBanned, + this.pinnedAt, + this.archivedAt, + required this.isModerator, + this.extraData, required this.createdAt, - required this.score, - this.extraData}); + required this.updatedAt}); @override Map toColumns(bool nullToAbsent) { final map = {}; map['user_id'] = Variable(userId); - map['message_id'] = Variable(messageId); - map['type'] = Variable(type); - map['created_at'] = Variable(createdAt); - map['score'] = Variable(score); + map['channel_cid'] = Variable(channelCid); + if (!nullToAbsent || channelRole != null) { + map['channel_role'] = Variable(channelRole); + } + if (!nullToAbsent || inviteAcceptedAt != null) { + map['invite_accepted_at'] = Variable(inviteAcceptedAt); + } + if (!nullToAbsent || inviteRejectedAt != null) { + map['invite_rejected_at'] = Variable(inviteRejectedAt); + } + map['invited'] = Variable(invited); + map['banned'] = Variable(banned); + map['shadow_banned'] = Variable(shadowBanned); + if (!nullToAbsent || pinnedAt != null) { + map['pinned_at'] = Variable(pinnedAt); + } + if (!nullToAbsent || archivedAt != null) { + map['archived_at'] = Variable(archivedAt); + } + map['is_moderator'] = Variable(isModerator); if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $ReactionsTable.$converterextraDatan.toSql(extraData)); + map['extra_data'] = + Variable($MembersTable.$converterextraDatan.toSql(extraData)); } + map['created_at'] = Variable(createdAt); + map['updated_at'] = Variable(updatedAt); return map; } - factory ReactionEntity.fromJson(Map json, + factory MemberEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return ReactionEntity( + return MemberEntity( userId: serializer.fromJson(json['userId']), - messageId: serializer.fromJson(json['messageId']), - type: serializer.fromJson(json['type']), - createdAt: serializer.fromJson(json['createdAt']), - score: serializer.fromJson(json['score']), + channelCid: serializer.fromJson(json['channelCid']), + channelRole: serializer.fromJson(json['channelRole']), + inviteAcceptedAt: + serializer.fromJson(json['inviteAcceptedAt']), + inviteRejectedAt: + serializer.fromJson(json['inviteRejectedAt']), + invited: serializer.fromJson(json['invited']), + banned: serializer.fromJson(json['banned']), + shadowBanned: serializer.fromJson(json['shadowBanned']), + pinnedAt: serializer.fromJson(json['pinnedAt']), + archivedAt: serializer.fromJson(json['archivedAt']), + isModerator: serializer.fromJson(json['isModerator']), extraData: serializer.fromJson?>(json['extraData']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), ); } @override @@ -5950,131 +7697,261 @@ class ReactionEntity extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return { 'userId': serializer.toJson(userId), - 'messageId': serializer.toJson(messageId), - 'type': serializer.toJson(type), - 'createdAt': serializer.toJson(createdAt), - 'score': serializer.toJson(score), + 'channelCid': serializer.toJson(channelCid), + 'channelRole': serializer.toJson(channelRole), + 'inviteAcceptedAt': serializer.toJson(inviteAcceptedAt), + 'inviteRejectedAt': serializer.toJson(inviteRejectedAt), + 'invited': serializer.toJson(invited), + 'banned': serializer.toJson(banned), + 'shadowBanned': serializer.toJson(shadowBanned), + 'pinnedAt': serializer.toJson(pinnedAt), + 'archivedAt': serializer.toJson(archivedAt), + 'isModerator': serializer.toJson(isModerator), 'extraData': serializer.toJson?>(extraData), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), }; } - ReactionEntity copyWith( + MemberEntity copyWith( {String? userId, - String? messageId, - String? type, + String? channelCid, + Value channelRole = const Value.absent(), + Value inviteAcceptedAt = const Value.absent(), + Value inviteRejectedAt = const Value.absent(), + bool? invited, + bool? banned, + bool? shadowBanned, + Value pinnedAt = const Value.absent(), + Value archivedAt = const Value.absent(), + bool? isModerator, + Value?> extraData = const Value.absent(), DateTime? createdAt, - int? score, - Value?> extraData = const Value.absent()}) => - ReactionEntity( + DateTime? updatedAt}) => + MemberEntity( userId: userId ?? this.userId, - messageId: messageId ?? this.messageId, - type: type ?? this.type, - createdAt: createdAt ?? this.createdAt, - score: score ?? this.score, + channelCid: channelCid ?? this.channelCid, + channelRole: channelRole.present ? channelRole.value : this.channelRole, + inviteAcceptedAt: inviteAcceptedAt.present + ? inviteAcceptedAt.value + : this.inviteAcceptedAt, + inviteRejectedAt: inviteRejectedAt.present + ? inviteRejectedAt.value + : this.inviteRejectedAt, + invited: invited ?? this.invited, + banned: banned ?? this.banned, + shadowBanned: shadowBanned ?? this.shadowBanned, + pinnedAt: pinnedAt.present ? pinnedAt.value : this.pinnedAt, + archivedAt: archivedAt.present ? archivedAt.value : this.archivedAt, + isModerator: isModerator ?? this.isModerator, extraData: extraData.present ? extraData.value : this.extraData, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, ); - ReactionEntity copyWithCompanion(ReactionsCompanion data) { - return ReactionEntity( + MemberEntity copyWithCompanion(MembersCompanion data) { + return MemberEntity( userId: data.userId.present ? data.userId.value : this.userId, - messageId: data.messageId.present ? data.messageId.value : this.messageId, - type: data.type.present ? data.type.value : this.type, - createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, - score: data.score.present ? data.score.value : this.score, + channelCid: + data.channelCid.present ? data.channelCid.value : this.channelCid, + channelRole: + data.channelRole.present ? data.channelRole.value : this.channelRole, + inviteAcceptedAt: data.inviteAcceptedAt.present + ? data.inviteAcceptedAt.value + : this.inviteAcceptedAt, + inviteRejectedAt: data.inviteRejectedAt.present + ? data.inviteRejectedAt.value + : this.inviteRejectedAt, + invited: data.invited.present ? data.invited.value : this.invited, + banned: data.banned.present ? data.banned.value : this.banned, + shadowBanned: data.shadowBanned.present + ? data.shadowBanned.value + : this.shadowBanned, + pinnedAt: data.pinnedAt.present ? data.pinnedAt.value : this.pinnedAt, + archivedAt: + data.archivedAt.present ? data.archivedAt.value : this.archivedAt, + isModerator: + data.isModerator.present ? data.isModerator.value : this.isModerator, extraData: data.extraData.present ? data.extraData.value : this.extraData, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, ); } @override String toString() { - return (StringBuffer('ReactionEntity(') + return (StringBuffer('MemberEntity(') ..write('userId: $userId, ') - ..write('messageId: $messageId, ') - ..write('type: $type, ') + ..write('channelCid: $channelCid, ') + ..write('channelRole: $channelRole, ') + ..write('inviteAcceptedAt: $inviteAcceptedAt, ') + ..write('inviteRejectedAt: $inviteRejectedAt, ') + ..write('invited: $invited, ') + ..write('banned: $banned, ') + ..write('shadowBanned: $shadowBanned, ') + ..write('pinnedAt: $pinnedAt, ') + ..write('archivedAt: $archivedAt, ') + ..write('isModerator: $isModerator, ') + ..write('extraData: $extraData, ') ..write('createdAt: $createdAt, ') - ..write('score: $score, ') - ..write('extraData: $extraData') + ..write('updatedAt: $updatedAt') ..write(')')) .toString(); } @override - int get hashCode => - Object.hash(userId, messageId, type, createdAt, score, extraData); + int get hashCode => Object.hash( + userId, + channelCid, + channelRole, + inviteAcceptedAt, + inviteRejectedAt, + invited, + banned, + shadowBanned, + pinnedAt, + archivedAt, + isModerator, + extraData, + createdAt, + updatedAt); @override bool operator ==(Object other) => identical(this, other) || - (other is ReactionEntity && + (other is MemberEntity && other.userId == this.userId && - other.messageId == this.messageId && - other.type == this.type && + other.channelCid == this.channelCid && + other.channelRole == this.channelRole && + other.inviteAcceptedAt == this.inviteAcceptedAt && + other.inviteRejectedAt == this.inviteRejectedAt && + other.invited == this.invited && + other.banned == this.banned && + other.shadowBanned == this.shadowBanned && + other.pinnedAt == this.pinnedAt && + other.archivedAt == this.archivedAt && + other.isModerator == this.isModerator && + other.extraData == this.extraData && other.createdAt == this.createdAt && - other.score == this.score && - other.extraData == this.extraData); + other.updatedAt == this.updatedAt); } -class ReactionsCompanion extends UpdateCompanion { +class MembersCompanion extends UpdateCompanion { final Value userId; - final Value messageId; - final Value type; - final Value createdAt; - final Value score; + final Value channelCid; + final Value channelRole; + final Value inviteAcceptedAt; + final Value inviteRejectedAt; + final Value invited; + final Value banned; + final Value shadowBanned; + final Value pinnedAt; + final Value archivedAt; + final Value isModerator; final Value?> extraData; + final Value createdAt; + final Value updatedAt; final Value rowid; - const ReactionsCompanion({ + const MembersCompanion({ this.userId = const Value.absent(), - this.messageId = const Value.absent(), - this.type = const Value.absent(), - this.createdAt = const Value.absent(), - this.score = const Value.absent(), + this.channelCid = const Value.absent(), + this.channelRole = const Value.absent(), + this.inviteAcceptedAt = const Value.absent(), + this.inviteRejectedAt = const Value.absent(), + this.invited = const Value.absent(), + this.banned = const Value.absent(), + this.shadowBanned = const Value.absent(), + this.pinnedAt = const Value.absent(), + this.archivedAt = const Value.absent(), + this.isModerator = const Value.absent(), this.extraData = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), this.rowid = const Value.absent(), }); - ReactionsCompanion.insert({ + MembersCompanion.insert({ required String userId, - required String messageId, - required String type, - this.createdAt = const Value.absent(), - this.score = const Value.absent(), + required String channelCid, + this.channelRole = const Value.absent(), + this.inviteAcceptedAt = const Value.absent(), + this.inviteRejectedAt = const Value.absent(), + this.invited = const Value.absent(), + this.banned = const Value.absent(), + this.shadowBanned = const Value.absent(), + this.pinnedAt = const Value.absent(), + this.archivedAt = const Value.absent(), + this.isModerator = const Value.absent(), this.extraData = const Value.absent(), + this.createdAt = const Value.absent(), + this.updatedAt = const Value.absent(), this.rowid = const Value.absent(), }) : userId = Value(userId), - messageId = Value(messageId), - type = Value(type); - static Insertable custom({ + channelCid = Value(channelCid); + static Insertable custom({ Expression? userId, - Expression? messageId, - Expression? type, - Expression? createdAt, - Expression? score, + Expression? channelCid, + Expression? channelRole, + Expression? inviteAcceptedAt, + Expression? inviteRejectedAt, + Expression? invited, + Expression? banned, + Expression? shadowBanned, + Expression? pinnedAt, + Expression? archivedAt, + Expression? isModerator, Expression? extraData, + Expression? createdAt, + Expression? updatedAt, Expression? rowid, }) { return RawValuesInsertable({ if (userId != null) 'user_id': userId, - if (messageId != null) 'message_id': messageId, - if (type != null) 'type': type, - if (createdAt != null) 'created_at': createdAt, - if (score != null) 'score': score, + if (channelCid != null) 'channel_cid': channelCid, + if (channelRole != null) 'channel_role': channelRole, + if (inviteAcceptedAt != null) 'invite_accepted_at': inviteAcceptedAt, + if (inviteRejectedAt != null) 'invite_rejected_at': inviteRejectedAt, + if (invited != null) 'invited': invited, + if (banned != null) 'banned': banned, + if (shadowBanned != null) 'shadow_banned': shadowBanned, + if (pinnedAt != null) 'pinned_at': pinnedAt, + if (archivedAt != null) 'archived_at': archivedAt, + if (isModerator != null) 'is_moderator': isModerator, if (extraData != null) 'extra_data': extraData, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, if (rowid != null) 'rowid': rowid, }); } - ReactionsCompanion copyWith( + MembersCompanion copyWith( {Value? userId, - Value? messageId, - Value? type, - Value? createdAt, - Value? score, + Value? channelCid, + Value? channelRole, + Value? inviteAcceptedAt, + Value? inviteRejectedAt, + Value? invited, + Value? banned, + Value? shadowBanned, + Value? pinnedAt, + Value? archivedAt, + Value? isModerator, Value?>? extraData, + Value? createdAt, + Value? updatedAt, Value? rowid}) { - return ReactionsCompanion( + return MembersCompanion( userId: userId ?? this.userId, - messageId: messageId ?? this.messageId, - type: type ?? this.type, - createdAt: createdAt ?? this.createdAt, - score: score ?? this.score, + channelCid: channelCid ?? this.channelCid, + channelRole: channelRole ?? this.channelRole, + inviteAcceptedAt: inviteAcceptedAt ?? this.inviteAcceptedAt, + inviteRejectedAt: inviteRejectedAt ?? this.inviteRejectedAt, + invited: invited ?? this.invited, + banned: banned ?? this.banned, + shadowBanned: shadowBanned ?? this.shadowBanned, + pinnedAt: pinnedAt ?? this.pinnedAt, + archivedAt: archivedAt ?? this.archivedAt, + isModerator: isModerator ?? this.isModerator, extraData: extraData ?? this.extraData, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, rowid: rowid ?? this.rowid, ); } @@ -6085,21 +7962,45 @@ class ReactionsCompanion extends UpdateCompanion { if (userId.present) { map['user_id'] = Variable(userId.value); } - if (messageId.present) { - map['message_id'] = Variable(messageId.value); + if (channelCid.present) { + map['channel_cid'] = Variable(channelCid.value); } - if (type.present) { - map['type'] = Variable(type.value); + if (channelRole.present) { + map['channel_role'] = Variable(channelRole.value); } - if (createdAt.present) { - map['created_at'] = Variable(createdAt.value); + if (inviteAcceptedAt.present) { + map['invite_accepted_at'] = Variable(inviteAcceptedAt.value); } - if (score.present) { - map['score'] = Variable(score.value); + if (inviteRejectedAt.present) { + map['invite_rejected_at'] = Variable(inviteRejectedAt.value); + } + if (invited.present) { + map['invited'] = Variable(invited.value); + } + if (banned.present) { + map['banned'] = Variable(banned.value); + } + if (shadowBanned.present) { + map['shadow_banned'] = Variable(shadowBanned.value); + } + if (pinnedAt.present) { + map['pinned_at'] = Variable(pinnedAt.value); + } + if (archivedAt.present) { + map['archived_at'] = Variable(archivedAt.value); + } + if (isModerator.present) { + map['is_moderator'] = Variable(isModerator.value); } if (extraData.present) { map['extra_data'] = Variable( - $ReactionsTable.$converterextraDatan.toSql(extraData.value)); + $MembersTable.$converterextraDatan.toSql(extraData.value)); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -6109,546 +8010,330 @@ class ReactionsCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('ReactionsCompanion(') + return (StringBuffer('MembersCompanion(') ..write('userId: $userId, ') - ..write('messageId: $messageId, ') - ..write('type: $type, ') - ..write('createdAt: $createdAt, ') - ..write('score: $score, ') + ..write('channelCid: $channelCid, ') + ..write('channelRole: $channelRole, ') + ..write('inviteAcceptedAt: $inviteAcceptedAt, ') + ..write('inviteRejectedAt: $inviteRejectedAt, ') + ..write('invited: $invited, ') + ..write('banned: $banned, ') + ..write('shadowBanned: $shadowBanned, ') + ..write('pinnedAt: $pinnedAt, ') + ..write('archivedAt: $archivedAt, ') + ..write('isModerator: $isModerator, ') ..write('extraData: $extraData, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') ..write('rowid: $rowid') ..write(')')) .toString(); } } -class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { +class $ReadsTable extends Reads with TableInfo<$ReadsTable, ReadEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $UsersTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _roleMeta = const VerificationMeta('role'); - @override - late final GeneratedColumn role = GeneratedColumn( - 'role', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _languageMeta = - const VerificationMeta('language'); - @override - late final GeneratedColumn language = GeneratedColumn( - 'language', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); - @override - late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _updatedAtMeta = - const VerificationMeta('updatedAt'); + $ReadsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _lastReadMeta = + const VerificationMeta('lastRead'); @override - late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _lastActiveMeta = - const VerificationMeta('lastActive'); + late final GeneratedColumn lastRead = GeneratedColumn( + 'last_read', aliasedName, false, + type: DriftSqlType.dateTime, requiredDuringInsert: true); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); @override - late final GeneratedColumn lastActive = GeneratedColumn( - 'last_active', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _onlineMeta = const VerificationMeta('online'); + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _channelCidMeta = + const VerificationMeta('channelCid'); @override - late final GeneratedColumn online = GeneratedColumn( - 'online', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("online" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _bannedMeta = const VerificationMeta('banned'); + late final GeneratedColumn channelCid = GeneratedColumn( + 'channel_cid', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES channels (cid) ON DELETE CASCADE')); + static const VerificationMeta _unreadMessagesMeta = + const VerificationMeta('unreadMessages'); @override - late final GeneratedColumn banned = GeneratedColumn( - 'banned', aliasedName, false, - type: DriftSqlType.bool, + late final GeneratedColumn unreadMessages = GeneratedColumn( + 'unread_messages', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("banned" IN (0, 1))'), - defaultValue: const Constant(false)); - @override - late final GeneratedColumnWithTypeConverter?, String> - teamsRole = GeneratedColumn('teams_role', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $UsersTable.$converterteamsRolen); - static const VerificationMeta _avgResponseTimeMeta = - const VerificationMeta('avgResponseTime'); - @override - late final GeneratedColumn avgResponseTime = GeneratedColumn( - 'avg_response_time', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); + defaultValue: const Constant(0)); + static const VerificationMeta _lastReadMessageIdMeta = + const VerificationMeta('lastReadMessageId'); @override - late final GeneratedColumnWithTypeConverter, String> - extraData = GeneratedColumn('extra_data', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>($UsersTable.$converterextraData); + late final GeneratedColumn lastReadMessageId = + GeneratedColumn('last_read_message_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); @override - List get $columns => [ - id, - role, - language, - createdAt, - updatedAt, - lastActive, - online, - banned, - teamsRole, - avgResponseTime, - extraData - ]; + List get $columns => + [lastRead, userId, channelCid, unreadMessages, lastReadMessageId]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'users'; + static const String $name = 'reads'; @override - VerificationContext validateIntegrity(Insertable instance, + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + if (data.containsKey('last_read')) { + context.handle(_lastReadMeta, + lastRead.isAcceptableOrUnknown(data['last_read']!, _lastReadMeta)); + } else if (isInserting) { + context.missing(_lastReadMeta); + } + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); } else if (isInserting) { - context.missing(_idMeta); + context.missing(_userIdMeta); } - if (data.containsKey('role')) { + if (data.containsKey('channel_cid')) { context.handle( - _roleMeta, role.isAcceptableOrUnknown(data['role']!, _roleMeta)); - } - if (data.containsKey('language')) { - context.handle(_languageMeta, - language.isAcceptableOrUnknown(data['language']!, _languageMeta)); - } - if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); - } - if (data.containsKey('updated_at')) { - context.handle(_updatedAtMeta, - updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + _channelCidMeta, + channelCid.isAcceptableOrUnknown( + data['channel_cid']!, _channelCidMeta)); + } else if (isInserting) { + context.missing(_channelCidMeta); } - if (data.containsKey('last_active')) { + if (data.containsKey('unread_messages')) { context.handle( - _lastActiveMeta, - lastActive.isAcceptableOrUnknown( - data['last_active']!, _lastActiveMeta)); - } - if (data.containsKey('online')) { - context.handle(_onlineMeta, - online.isAcceptableOrUnknown(data['online']!, _onlineMeta)); - } - if (data.containsKey('banned')) { - context.handle(_bannedMeta, - banned.isAcceptableOrUnknown(data['banned']!, _bannedMeta)); + _unreadMessagesMeta, + unreadMessages.isAcceptableOrUnknown( + data['unread_messages']!, _unreadMessagesMeta)); } - if (data.containsKey('avg_response_time')) { + if (data.containsKey('last_read_message_id')) { context.handle( - _avgResponseTimeMeta, - avgResponseTime.isAcceptableOrUnknown( - data['avg_response_time']!, _avgResponseTimeMeta)); + _lastReadMessageIdMeta, + lastReadMessageId.isAcceptableOrUnknown( + data['last_read_message_id']!, _lastReadMessageIdMeta)); } return context; } @override - Set get $primaryKey => {id}; + Set get $primaryKey => {userId, channelCid}; @override - UserEntity map(Map data, {String? tablePrefix}) { + ReadEntity map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return UserEntity( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - role: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}role']), - language: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}language']), - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at']), - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at']), - lastActive: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}last_active']), - online: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}online'])!, - banned: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}banned'])!, - teamsRole: $UsersTable.$converterteamsRolen.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}teams_role'])), - avgResponseTime: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}avg_response_time']), - extraData: $UsersTable.$converterextraData.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])!), + return ReadEntity( + lastRead: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_read'])!, + userId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + channelCid: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, + unreadMessages: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}unread_messages'])!, + lastReadMessageId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}last_read_message_id']), ); } @override - $UsersTable createAlias(String alias) { - return $UsersTable(attachedDatabase, alias); + $ReadsTable createAlias(String alias) { + return $ReadsTable(attachedDatabase, alias); } - - static TypeConverter, String> $converterteamsRole = - MapConverter(); - static TypeConverter?, String?> $converterteamsRolen = - NullAwareTypeConverter.wrap($converterteamsRole); - static TypeConverter, String> $converterextraData = - MapConverter(); } -class UserEntity extends DataClass implements Insertable { - /// User id - final String id; - - /// User role - final String? role; - - /// The language this user prefers. - final String? language; - - /// Date of user creation - final DateTime? createdAt; - - /// Date of last user update - final DateTime? updatedAt; - - /// Date of last user connection - final DateTime? lastActive; - - /// True if user is online - final bool online; +class ReadEntity extends DataClass implements Insertable { + /// Date of the read event + final DateTime lastRead; - /// True if user is banned from the chat - final bool banned; + /// Id of the User who sent the event + final String userId; - /// The roles for the user in the teams. - /// - /// eg: `{'teamId': 'role', 'teamId2': 'role2'}` - final Map? teamsRole; + /// The channel cid of which this read belongs + final String channelCid; - /// The average response time for the user in seconds. - final int? avgResponseTime; + /// Number of unread messages + final int unreadMessages; - /// Map of custom user extraData - final Map extraData; - const UserEntity( - {required this.id, - this.role, - this.language, - this.createdAt, - this.updatedAt, - this.lastActive, - required this.online, - required this.banned, - this.teamsRole, - this.avgResponseTime, - required this.extraData}); + /// Id of the last read message + final String? lastReadMessageId; + const ReadEntity( + {required this.lastRead, + required this.userId, + required this.channelCid, + required this.unreadMessages, + this.lastReadMessageId}); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['id'] = Variable(id); - if (!nullToAbsent || role != null) { - map['role'] = Variable(role); - } - if (!nullToAbsent || language != null) { - map['language'] = Variable(language); - } - if (!nullToAbsent || createdAt != null) { - map['created_at'] = Variable(createdAt); - } - if (!nullToAbsent || updatedAt != null) { - map['updated_at'] = Variable(updatedAt); - } - if (!nullToAbsent || lastActive != null) { - map['last_active'] = Variable(lastActive); - } - map['online'] = Variable(online); - map['banned'] = Variable(banned); - if (!nullToAbsent || teamsRole != null) { - map['teams_role'] = - Variable($UsersTable.$converterteamsRolen.toSql(teamsRole)); - } - if (!nullToAbsent || avgResponseTime != null) { - map['avg_response_time'] = Variable(avgResponseTime); - } - { - map['extra_data'] = - Variable($UsersTable.$converterextraData.toSql(extraData)); + map['last_read'] = Variable(lastRead); + map['user_id'] = Variable(userId); + map['channel_cid'] = Variable(channelCid); + map['unread_messages'] = Variable(unreadMessages); + if (!nullToAbsent || lastReadMessageId != null) { + map['last_read_message_id'] = Variable(lastReadMessageId); } return map; } - factory UserEntity.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return UserEntity( - id: serializer.fromJson(json['id']), - role: serializer.fromJson(json['role']), - language: serializer.fromJson(json['language']), - createdAt: serializer.fromJson(json['createdAt']), - updatedAt: serializer.fromJson(json['updatedAt']), - lastActive: serializer.fromJson(json['lastActive']), - online: serializer.fromJson(json['online']), - banned: serializer.fromJson(json['banned']), - teamsRole: serializer.fromJson?>(json['teamsRole']), - avgResponseTime: serializer.fromJson(json['avgResponseTime']), - extraData: serializer.fromJson>(json['extraData']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'role': serializer.toJson(role), - 'language': serializer.toJson(language), - 'createdAt': serializer.toJson(createdAt), - 'updatedAt': serializer.toJson(updatedAt), - 'lastActive': serializer.toJson(lastActive), - 'online': serializer.toJson(online), - 'banned': serializer.toJson(banned), - 'teamsRole': serializer.toJson?>(teamsRole), - 'avgResponseTime': serializer.toJson(avgResponseTime), - 'extraData': serializer.toJson>(extraData), - }; - } - - UserEntity copyWith( - {String? id, - Value role = const Value.absent(), - Value language = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value lastActive = const Value.absent(), - bool? online, - bool? banned, - Value?> teamsRole = const Value.absent(), - Value avgResponseTime = const Value.absent(), - Map? extraData}) => - UserEntity( - id: id ?? this.id, - role: role.present ? role.value : this.role, - language: language.present ? language.value : this.language, - createdAt: createdAt.present ? createdAt.value : this.createdAt, - updatedAt: updatedAt.present ? updatedAt.value : this.updatedAt, - lastActive: lastActive.present ? lastActive.value : this.lastActive, - online: online ?? this.online, - banned: banned ?? this.banned, - teamsRole: teamsRole.present ? teamsRole.value : this.teamsRole, - avgResponseTime: avgResponseTime.present - ? avgResponseTime.value - : this.avgResponseTime, - extraData: extraData ?? this.extraData, + factory ReadEntity.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ReadEntity( + lastRead: serializer.fromJson(json['lastRead']), + userId: serializer.fromJson(json['userId']), + channelCid: serializer.fromJson(json['channelCid']), + unreadMessages: serializer.fromJson(json['unreadMessages']), + lastReadMessageId: + serializer.fromJson(json['lastReadMessageId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'lastRead': serializer.toJson(lastRead), + 'userId': serializer.toJson(userId), + 'channelCid': serializer.toJson(channelCid), + 'unreadMessages': serializer.toJson(unreadMessages), + 'lastReadMessageId': serializer.toJson(lastReadMessageId), + }; + } + + ReadEntity copyWith( + {DateTime? lastRead, + String? userId, + String? channelCid, + int? unreadMessages, + Value lastReadMessageId = const Value.absent()}) => + ReadEntity( + lastRead: lastRead ?? this.lastRead, + userId: userId ?? this.userId, + channelCid: channelCid ?? this.channelCid, + unreadMessages: unreadMessages ?? this.unreadMessages, + lastReadMessageId: lastReadMessageId.present + ? lastReadMessageId.value + : this.lastReadMessageId, ); - UserEntity copyWithCompanion(UsersCompanion data) { - return UserEntity( - id: data.id.present ? data.id.value : this.id, - role: data.role.present ? data.role.value : this.role, - language: data.language.present ? data.language.value : this.language, - createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, - updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, - lastActive: - data.lastActive.present ? data.lastActive.value : this.lastActive, - online: data.online.present ? data.online.value : this.online, - banned: data.banned.present ? data.banned.value : this.banned, - teamsRole: data.teamsRole.present ? data.teamsRole.value : this.teamsRole, - avgResponseTime: data.avgResponseTime.present - ? data.avgResponseTime.value - : this.avgResponseTime, - extraData: data.extraData.present ? data.extraData.value : this.extraData, + ReadEntity copyWithCompanion(ReadsCompanion data) { + return ReadEntity( + lastRead: data.lastRead.present ? data.lastRead.value : this.lastRead, + userId: data.userId.present ? data.userId.value : this.userId, + channelCid: + data.channelCid.present ? data.channelCid.value : this.channelCid, + unreadMessages: data.unreadMessages.present + ? data.unreadMessages.value + : this.unreadMessages, + lastReadMessageId: data.lastReadMessageId.present + ? data.lastReadMessageId.value + : this.lastReadMessageId, ); } @override String toString() { - return (StringBuffer('UserEntity(') - ..write('id: $id, ') - ..write('role: $role, ') - ..write('language: $language, ') - ..write('createdAt: $createdAt, ') - ..write('updatedAt: $updatedAt, ') - ..write('lastActive: $lastActive, ') - ..write('online: $online, ') - ..write('banned: $banned, ') - ..write('teamsRole: $teamsRole, ') - ..write('avgResponseTime: $avgResponseTime, ') - ..write('extraData: $extraData') + return (StringBuffer('ReadEntity(') + ..write('lastRead: $lastRead, ') + ..write('userId: $userId, ') + ..write('channelCid: $channelCid, ') + ..write('unreadMessages: $unreadMessages, ') + ..write('lastReadMessageId: $lastReadMessageId') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, role, language, createdAt, updatedAt, - lastActive, online, banned, teamsRole, avgResponseTime, extraData); + int get hashCode => Object.hash( + lastRead, userId, channelCid, unreadMessages, lastReadMessageId); @override bool operator ==(Object other) => identical(this, other) || - (other is UserEntity && - other.id == this.id && - other.role == this.role && - other.language == this.language && - other.createdAt == this.createdAt && - other.updatedAt == this.updatedAt && - other.lastActive == this.lastActive && - other.online == this.online && - other.banned == this.banned && - other.teamsRole == this.teamsRole && - other.avgResponseTime == this.avgResponseTime && - other.extraData == this.extraData); + (other is ReadEntity && + other.lastRead == this.lastRead && + other.userId == this.userId && + other.channelCid == this.channelCid && + other.unreadMessages == this.unreadMessages && + other.lastReadMessageId == this.lastReadMessageId); } -class UsersCompanion extends UpdateCompanion { - final Value id; - final Value role; - final Value language; - final Value createdAt; - final Value updatedAt; - final Value lastActive; - final Value online; - final Value banned; - final Value?> teamsRole; - final Value avgResponseTime; - final Value> extraData; +class ReadsCompanion extends UpdateCompanion { + final Value lastRead; + final Value userId; + final Value channelCid; + final Value unreadMessages; + final Value lastReadMessageId; final Value rowid; - const UsersCompanion({ - this.id = const Value.absent(), - this.role = const Value.absent(), - this.language = const Value.absent(), - this.createdAt = const Value.absent(), - this.updatedAt = const Value.absent(), - this.lastActive = const Value.absent(), - this.online = const Value.absent(), - this.banned = const Value.absent(), - this.teamsRole = const Value.absent(), - this.avgResponseTime = const Value.absent(), - this.extraData = const Value.absent(), + const ReadsCompanion({ + this.lastRead = const Value.absent(), + this.userId = const Value.absent(), + this.channelCid = const Value.absent(), + this.unreadMessages = const Value.absent(), + this.lastReadMessageId = const Value.absent(), this.rowid = const Value.absent(), }); - UsersCompanion.insert({ - required String id, - this.role = const Value.absent(), - this.language = const Value.absent(), - this.createdAt = const Value.absent(), - this.updatedAt = const Value.absent(), - this.lastActive = const Value.absent(), - this.online = const Value.absent(), - this.banned = const Value.absent(), - this.teamsRole = const Value.absent(), - this.avgResponseTime = const Value.absent(), - required Map extraData, + ReadsCompanion.insert({ + required DateTime lastRead, + required String userId, + required String channelCid, + this.unreadMessages = const Value.absent(), + this.lastReadMessageId = const Value.absent(), this.rowid = const Value.absent(), - }) : id = Value(id), - extraData = Value(extraData); - static Insertable custom({ - Expression? id, - Expression? role, - Expression? language, - Expression? createdAt, - Expression? updatedAt, - Expression? lastActive, - Expression? online, - Expression? banned, - Expression? teamsRole, - Expression? avgResponseTime, - Expression? extraData, + }) : lastRead = Value(lastRead), + userId = Value(userId), + channelCid = Value(channelCid); + static Insertable custom({ + Expression? lastRead, + Expression? userId, + Expression? channelCid, + Expression? unreadMessages, + Expression? lastReadMessageId, Expression? rowid, }) { return RawValuesInsertable({ - if (id != null) 'id': id, - if (role != null) 'role': role, - if (language != null) 'language': language, - if (createdAt != null) 'created_at': createdAt, - if (updatedAt != null) 'updated_at': updatedAt, - if (lastActive != null) 'last_active': lastActive, - if (online != null) 'online': online, - if (banned != null) 'banned': banned, - if (teamsRole != null) 'teams_role': teamsRole, - if (avgResponseTime != null) 'avg_response_time': avgResponseTime, - if (extraData != null) 'extra_data': extraData, + if (lastRead != null) 'last_read': lastRead, + if (userId != null) 'user_id': userId, + if (channelCid != null) 'channel_cid': channelCid, + if (unreadMessages != null) 'unread_messages': unreadMessages, + if (lastReadMessageId != null) 'last_read_message_id': lastReadMessageId, if (rowid != null) 'rowid': rowid, }); } - UsersCompanion copyWith( - {Value? id, - Value? role, - Value? language, - Value? createdAt, - Value? updatedAt, - Value? lastActive, - Value? online, - Value? banned, - Value?>? teamsRole, - Value? avgResponseTime, - Value>? extraData, - Value? rowid}) { - return UsersCompanion( - id: id ?? this.id, - role: role ?? this.role, - language: language ?? this.language, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - lastActive: lastActive ?? this.lastActive, - online: online ?? this.online, - banned: banned ?? this.banned, - teamsRole: teamsRole ?? this.teamsRole, - avgResponseTime: avgResponseTime ?? this.avgResponseTime, - extraData: extraData ?? this.extraData, - rowid: rowid ?? this.rowid, - ); - } - - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (role.present) { - map['role'] = Variable(role.value); - } - if (language.present) { - map['language'] = Variable(language.value); - } - if (createdAt.present) { - map['created_at'] = Variable(createdAt.value); - } - if (updatedAt.present) { - map['updated_at'] = Variable(updatedAt.value); - } - if (lastActive.present) { - map['last_active'] = Variable(lastActive.value); - } - if (online.present) { - map['online'] = Variable(online.value); + ReadsCompanion copyWith( + {Value? lastRead, + Value? userId, + Value? channelCid, + Value? unreadMessages, + Value? lastReadMessageId, + Value? rowid}) { + return ReadsCompanion( + lastRead: lastRead ?? this.lastRead, + userId: userId ?? this.userId, + channelCid: channelCid ?? this.channelCid, + unreadMessages: unreadMessages ?? this.unreadMessages, + lastReadMessageId: lastReadMessageId ?? this.lastReadMessageId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (lastRead.present) { + map['last_read'] = Variable(lastRead.value); } - if (banned.present) { - map['banned'] = Variable(banned.value); + if (userId.present) { + map['user_id'] = Variable(userId.value); } - if (teamsRole.present) { - map['teams_role'] = Variable( - $UsersTable.$converterteamsRolen.toSql(teamsRole.value)); + if (channelCid.present) { + map['channel_cid'] = Variable(channelCid.value); } - if (avgResponseTime.present) { - map['avg_response_time'] = Variable(avgResponseTime.value); + if (unreadMessages.present) { + map['unread_messages'] = Variable(unreadMessages.value); } - if (extraData.present) { - map['extra_data'] = Variable( - $UsersTable.$converterextraData.toSql(extraData.value)); + if (lastReadMessageId.present) { + map['last_read_message_id'] = Variable(lastReadMessageId.value); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -6658,660 +8343,183 @@ class UsersCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('UsersCompanion(') - ..write('id: $id, ') - ..write('role: $role, ') - ..write('language: $language, ') - ..write('createdAt: $createdAt, ') - ..write('updatedAt: $updatedAt, ') - ..write('lastActive: $lastActive, ') - ..write('online: $online, ') - ..write('banned: $banned, ') - ..write('teamsRole: $teamsRole, ') - ..write('avgResponseTime: $avgResponseTime, ') - ..write('extraData: $extraData, ') + return (StringBuffer('ReadsCompanion(') + ..write('lastRead: $lastRead, ') + ..write('userId: $userId, ') + ..write('channelCid: $channelCid, ') + ..write('unreadMessages: $unreadMessages, ') + ..write('lastReadMessageId: $lastReadMessageId, ') ..write('rowid: $rowid') ..write(')')) .toString(); } } -class $MembersTable extends Members - with TableInfo<$MembersTable, MemberEntity> { +class $ChannelQueriesTable extends ChannelQueries + with TableInfo<$ChannelQueriesTable, ChannelQueryEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $MembersTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + $ChannelQueriesTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _queryHashMeta = + const VerificationMeta('queryHash'); @override - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, false, + late final GeneratedColumn queryHash = GeneratedColumn( + 'query_hash', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); static const VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); @override late final GeneratedColumn channelCid = GeneratedColumn( 'channel_cid', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES channels (cid) ON DELETE CASCADE')); - static const VerificationMeta _channelRoleMeta = - const VerificationMeta('channelRole'); - @override - late final GeneratedColumn channelRole = GeneratedColumn( - 'channel_role', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _inviteAcceptedAtMeta = - const VerificationMeta('inviteAcceptedAt'); - @override - late final GeneratedColumn inviteAcceptedAt = - GeneratedColumn('invite_accepted_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _inviteRejectedAtMeta = - const VerificationMeta('inviteRejectedAt'); - @override - late final GeneratedColumn inviteRejectedAt = - GeneratedColumn('invite_rejected_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _invitedMeta = - const VerificationMeta('invited'); - @override - late final GeneratedColumn invited = GeneratedColumn( - 'invited', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("invited" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _bannedMeta = const VerificationMeta('banned'); - @override - late final GeneratedColumn banned = GeneratedColumn( - 'banned', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("banned" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _shadowBannedMeta = - const VerificationMeta('shadowBanned'); - @override - late final GeneratedColumn shadowBanned = GeneratedColumn( - 'shadow_banned', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("shadow_banned" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _pinnedAtMeta = - const VerificationMeta('pinnedAt'); - @override - late final GeneratedColumn pinnedAt = GeneratedColumn( - 'pinned_at', aliasedName, true, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: const Constant(null)); - static const VerificationMeta _archivedAtMeta = - const VerificationMeta('archivedAt'); - @override - late final GeneratedColumn archivedAt = GeneratedColumn( - 'archived_at', aliasedName, true, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: const Constant(null)); - static const VerificationMeta _isModeratorMeta = - const VerificationMeta('isModerator'); - @override - late final GeneratedColumn isModerator = GeneratedColumn( - 'is_moderator', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("is_moderator" IN (0, 1))'), - defaultValue: const Constant(false)); - @override - late final GeneratedColumnWithTypeConverter?, String> - extraData = GeneratedColumn('extra_data', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $MembersTable.$converterextraDatan); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); - @override - late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); - static const VerificationMeta _updatedAtMeta = - const VerificationMeta('updatedAt'); - @override - late final GeneratedColumn updatedAt = GeneratedColumn( - 'updated_at', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); + type: DriftSqlType.string, requiredDuringInsert: true); @override - List get $columns => [ - userId, - channelCid, - channelRole, - inviteAcceptedAt, - inviteRejectedAt, - invited, - banned, - shadowBanned, - pinnedAt, - archivedAt, - isModerator, - extraData, - createdAt, - updatedAt - ]; + List get $columns => [queryHash, channelCid]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'members'; + static const String $name = 'channel_queries'; @override - VerificationContext validateIntegrity(Insertable instance, + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('user_id')) { - context.handle(_userIdMeta, - userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + if (data.containsKey('query_hash')) { + context.handle(_queryHashMeta, + queryHash.isAcceptableOrUnknown(data['query_hash']!, _queryHashMeta)); } else if (isInserting) { - context.missing(_userIdMeta); + context.missing(_queryHashMeta); } if (data.containsKey('channel_cid')) { - context.handle( - _channelCidMeta, - channelCid.isAcceptableOrUnknown( - data['channel_cid']!, _channelCidMeta)); - } else if (isInserting) { - context.missing(_channelCidMeta); - } - if (data.containsKey('channel_role')) { - context.handle( - _channelRoleMeta, - channelRole.isAcceptableOrUnknown( - data['channel_role']!, _channelRoleMeta)); - } - if (data.containsKey('invite_accepted_at')) { - context.handle( - _inviteAcceptedAtMeta, - inviteAcceptedAt.isAcceptableOrUnknown( - data['invite_accepted_at']!, _inviteAcceptedAtMeta)); - } - if (data.containsKey('invite_rejected_at')) { - context.handle( - _inviteRejectedAtMeta, - inviteRejectedAt.isAcceptableOrUnknown( - data['invite_rejected_at']!, _inviteRejectedAtMeta)); - } - if (data.containsKey('invited')) { - context.handle(_invitedMeta, - invited.isAcceptableOrUnknown(data['invited']!, _invitedMeta)); - } - if (data.containsKey('banned')) { - context.handle(_bannedMeta, - banned.isAcceptableOrUnknown(data['banned']!, _bannedMeta)); - } - if (data.containsKey('shadow_banned')) { - context.handle( - _shadowBannedMeta, - shadowBanned.isAcceptableOrUnknown( - data['shadow_banned']!, _shadowBannedMeta)); - } - if (data.containsKey('pinned_at')) { - context.handle(_pinnedAtMeta, - pinnedAt.isAcceptableOrUnknown(data['pinned_at']!, _pinnedAtMeta)); - } - if (data.containsKey('archived_at')) { - context.handle( - _archivedAtMeta, - archivedAt.isAcceptableOrUnknown( - data['archived_at']!, _archivedAtMeta)); - } - if (data.containsKey('is_moderator')) { - context.handle( - _isModeratorMeta, - isModerator.isAcceptableOrUnknown( - data['is_moderator']!, _isModeratorMeta)); - } - if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); - } - if (data.containsKey('updated_at')) { - context.handle(_updatedAtMeta, - updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + context.handle( + _channelCidMeta, + channelCid.isAcceptableOrUnknown( + data['channel_cid']!, _channelCidMeta)); + } else if (isInserting) { + context.missing(_channelCidMeta); } return context; } @override - Set get $primaryKey => {userId, channelCid}; + Set get $primaryKey => {queryHash, channelCid}; @override - MemberEntity map(Map data, {String? tablePrefix}) { + ChannelQueryEntity map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return MemberEntity( - userId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + return ChannelQueryEntity( + queryHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}query_hash'])!, channelCid: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, - channelRole: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}channel_role']), - inviteAcceptedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}invite_accepted_at']), - inviteRejectedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}invite_rejected_at']), - invited: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}invited'])!, - banned: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}banned'])!, - shadowBanned: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}shadow_banned'])!, - pinnedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}pinned_at']), - archivedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}archived_at']), - isModerator: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}is_moderator'])!, - extraData: $MembersTable.$converterextraDatan.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, ); } @override - $MembersTable createAlias(String alias) { - return $MembersTable(attachedDatabase, alias); + $ChannelQueriesTable createAlias(String alias) { + return $ChannelQueriesTable(attachedDatabase, alias); } - - static TypeConverter, String> $converterextraData = - MapConverter(); - static TypeConverter?, String?> $converterextraDatan = - NullAwareTypeConverter.wrap($converterextraData); } -class MemberEntity extends DataClass implements Insertable { - /// The interested user id - final String userId; +class ChannelQueryEntity extends DataClass + implements Insertable { + /// The unique hash of this query + final String queryHash; - /// The channel cid of which this user is part of + /// The channel cid of this query final String channelCid; - - /// The role of the user in the channel - final String? channelRole; - - /// The date on which the user accepted the invite to the channel - final DateTime? inviteAcceptedAt; - - /// The date on which the user rejected the invite to the channel - final DateTime? inviteRejectedAt; - - /// True if the user has been invited to the channel - final bool invited; - - /// True if the member is banned from the channel - final bool banned; - - /// True if the member is shadow banned from the channel - final bool shadowBanned; - - /// The date at which the channel was pinned by the member - final DateTime? pinnedAt; - - /// The date at which the channel was archived by the member - final DateTime? archivedAt; - - /// True if the user is a moderator of the channel - final bool isModerator; - - /// Map of custom member extraData - final Map? extraData; - - /// The date of creation - final DateTime createdAt; - - /// The last date of update - final DateTime updatedAt; - const MemberEntity( - {required this.userId, - required this.channelCid, - this.channelRole, - this.inviteAcceptedAt, - this.inviteRejectedAt, - required this.invited, - required this.banned, - required this.shadowBanned, - this.pinnedAt, - this.archivedAt, - required this.isModerator, - this.extraData, - required this.createdAt, - required this.updatedAt}); + const ChannelQueryEntity({required this.queryHash, required this.channelCid}); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['user_id'] = Variable(userId); + map['query_hash'] = Variable(queryHash); map['channel_cid'] = Variable(channelCid); - if (!nullToAbsent || channelRole != null) { - map['channel_role'] = Variable(channelRole); - } - if (!nullToAbsent || inviteAcceptedAt != null) { - map['invite_accepted_at'] = Variable(inviteAcceptedAt); - } - if (!nullToAbsent || inviteRejectedAt != null) { - map['invite_rejected_at'] = Variable(inviteRejectedAt); - } - map['invited'] = Variable(invited); - map['banned'] = Variable(banned); - map['shadow_banned'] = Variable(shadowBanned); - if (!nullToAbsent || pinnedAt != null) { - map['pinned_at'] = Variable(pinnedAt); - } - if (!nullToAbsent || archivedAt != null) { - map['archived_at'] = Variable(archivedAt); - } - map['is_moderator'] = Variable(isModerator); - if (!nullToAbsent || extraData != null) { - map['extra_data'] = - Variable($MembersTable.$converterextraDatan.toSql(extraData)); - } - map['created_at'] = Variable(createdAt); - map['updated_at'] = Variable(updatedAt); return map; } - factory MemberEntity.fromJson(Map json, + factory ChannelQueryEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return MemberEntity( - userId: serializer.fromJson(json['userId']), + return ChannelQueryEntity( + queryHash: serializer.fromJson(json['queryHash']), channelCid: serializer.fromJson(json['channelCid']), - channelRole: serializer.fromJson(json['channelRole']), - inviteAcceptedAt: - serializer.fromJson(json['inviteAcceptedAt']), - inviteRejectedAt: - serializer.fromJson(json['inviteRejectedAt']), - invited: serializer.fromJson(json['invited']), - banned: serializer.fromJson(json['banned']), - shadowBanned: serializer.fromJson(json['shadowBanned']), - pinnedAt: serializer.fromJson(json['pinnedAt']), - archivedAt: serializer.fromJson(json['archivedAt']), - isModerator: serializer.fromJson(json['isModerator']), - extraData: serializer.fromJson?>(json['extraData']), - createdAt: serializer.fromJson(json['createdAt']), - updatedAt: serializer.fromJson(json['updatedAt']), ); } @override Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'userId': serializer.toJson(userId), + 'queryHash': serializer.toJson(queryHash), 'channelCid': serializer.toJson(channelCid), - 'channelRole': serializer.toJson(channelRole), - 'inviteAcceptedAt': serializer.toJson(inviteAcceptedAt), - 'inviteRejectedAt': serializer.toJson(inviteRejectedAt), - 'invited': serializer.toJson(invited), - 'banned': serializer.toJson(banned), - 'shadowBanned': serializer.toJson(shadowBanned), - 'pinnedAt': serializer.toJson(pinnedAt), - 'archivedAt': serializer.toJson(archivedAt), - 'isModerator': serializer.toJson(isModerator), - 'extraData': serializer.toJson?>(extraData), - 'createdAt': serializer.toJson(createdAt), - 'updatedAt': serializer.toJson(updatedAt), }; } - MemberEntity copyWith( - {String? userId, - String? channelCid, - Value channelRole = const Value.absent(), - Value inviteAcceptedAt = const Value.absent(), - Value inviteRejectedAt = const Value.absent(), - bool? invited, - bool? banned, - bool? shadowBanned, - Value pinnedAt = const Value.absent(), - Value archivedAt = const Value.absent(), - bool? isModerator, - Value?> extraData = const Value.absent(), - DateTime? createdAt, - DateTime? updatedAt}) => - MemberEntity( - userId: userId ?? this.userId, - channelCid: channelCid ?? this.channelCid, - channelRole: channelRole.present ? channelRole.value : this.channelRole, - inviteAcceptedAt: inviteAcceptedAt.present - ? inviteAcceptedAt.value - : this.inviteAcceptedAt, - inviteRejectedAt: inviteRejectedAt.present - ? inviteRejectedAt.value - : this.inviteRejectedAt, - invited: invited ?? this.invited, - banned: banned ?? this.banned, - shadowBanned: shadowBanned ?? this.shadowBanned, - pinnedAt: pinnedAt.present ? pinnedAt.value : this.pinnedAt, - archivedAt: archivedAt.present ? archivedAt.value : this.archivedAt, - isModerator: isModerator ?? this.isModerator, - extraData: extraData.present ? extraData.value : this.extraData, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - ); - MemberEntity copyWithCompanion(MembersCompanion data) { - return MemberEntity( - userId: data.userId.present ? data.userId.value : this.userId, - channelCid: - data.channelCid.present ? data.channelCid.value : this.channelCid, - channelRole: - data.channelRole.present ? data.channelRole.value : this.channelRole, - inviteAcceptedAt: data.inviteAcceptedAt.present - ? data.inviteAcceptedAt.value - : this.inviteAcceptedAt, - inviteRejectedAt: data.inviteRejectedAt.present - ? data.inviteRejectedAt.value - : this.inviteRejectedAt, - invited: data.invited.present ? data.invited.value : this.invited, - banned: data.banned.present ? data.banned.value : this.banned, - shadowBanned: data.shadowBanned.present - ? data.shadowBanned.value - : this.shadowBanned, - pinnedAt: data.pinnedAt.present ? data.pinnedAt.value : this.pinnedAt, - archivedAt: - data.archivedAt.present ? data.archivedAt.value : this.archivedAt, - isModerator: - data.isModerator.present ? data.isModerator.value : this.isModerator, - extraData: data.extraData.present ? data.extraData.value : this.extraData, - createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, - updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, - ); - } - - @override - String toString() { - return (StringBuffer('MemberEntity(') - ..write('userId: $userId, ') - ..write('channelCid: $channelCid, ') - ..write('channelRole: $channelRole, ') - ..write('inviteAcceptedAt: $inviteAcceptedAt, ') - ..write('inviteRejectedAt: $inviteRejectedAt, ') - ..write('invited: $invited, ') - ..write('banned: $banned, ') - ..write('shadowBanned: $shadowBanned, ') - ..write('pinnedAt: $pinnedAt, ') - ..write('archivedAt: $archivedAt, ') - ..write('isModerator: $isModerator, ') - ..write('extraData: $extraData, ') - ..write('createdAt: $createdAt, ') - ..write('updatedAt: $updatedAt') + ChannelQueryEntity copyWith({String? queryHash, String? channelCid}) => + ChannelQueryEntity( + queryHash: queryHash ?? this.queryHash, + channelCid: channelCid ?? this.channelCid, + ); + ChannelQueryEntity copyWithCompanion(ChannelQueriesCompanion data) { + return ChannelQueryEntity( + queryHash: data.queryHash.present ? data.queryHash.value : this.queryHash, + channelCid: + data.channelCid.present ? data.channelCid.value : this.channelCid, + ); + } + + @override + String toString() { + return (StringBuffer('ChannelQueryEntity(') + ..write('queryHash: $queryHash, ') + ..write('channelCid: $channelCid') ..write(')')) .toString(); } @override - int get hashCode => Object.hash( - userId, - channelCid, - channelRole, - inviteAcceptedAt, - inviteRejectedAt, - invited, - banned, - shadowBanned, - pinnedAt, - archivedAt, - isModerator, - extraData, - createdAt, - updatedAt); + int get hashCode => Object.hash(queryHash, channelCid); @override bool operator ==(Object other) => identical(this, other) || - (other is MemberEntity && - other.userId == this.userId && - other.channelCid == this.channelCid && - other.channelRole == this.channelRole && - other.inviteAcceptedAt == this.inviteAcceptedAt && - other.inviteRejectedAt == this.inviteRejectedAt && - other.invited == this.invited && - other.banned == this.banned && - other.shadowBanned == this.shadowBanned && - other.pinnedAt == this.pinnedAt && - other.archivedAt == this.archivedAt && - other.isModerator == this.isModerator && - other.extraData == this.extraData && - other.createdAt == this.createdAt && - other.updatedAt == this.updatedAt); + (other is ChannelQueryEntity && + other.queryHash == this.queryHash && + other.channelCid == this.channelCid); } -class MembersCompanion extends UpdateCompanion { - final Value userId; +class ChannelQueriesCompanion extends UpdateCompanion { + final Value queryHash; final Value channelCid; - final Value channelRole; - final Value inviteAcceptedAt; - final Value inviteRejectedAt; - final Value invited; - final Value banned; - final Value shadowBanned; - final Value pinnedAt; - final Value archivedAt; - final Value isModerator; - final Value?> extraData; - final Value createdAt; - final Value updatedAt; final Value rowid; - const MembersCompanion({ - this.userId = const Value.absent(), + const ChannelQueriesCompanion({ + this.queryHash = const Value.absent(), this.channelCid = const Value.absent(), - this.channelRole = const Value.absent(), - this.inviteAcceptedAt = const Value.absent(), - this.inviteRejectedAt = const Value.absent(), - this.invited = const Value.absent(), - this.banned = const Value.absent(), - this.shadowBanned = const Value.absent(), - this.pinnedAt = const Value.absent(), - this.archivedAt = const Value.absent(), - this.isModerator = const Value.absent(), - this.extraData = const Value.absent(), - this.createdAt = const Value.absent(), - this.updatedAt = const Value.absent(), this.rowid = const Value.absent(), }); - MembersCompanion.insert({ - required String userId, + ChannelQueriesCompanion.insert({ + required String queryHash, required String channelCid, - this.channelRole = const Value.absent(), - this.inviteAcceptedAt = const Value.absent(), - this.inviteRejectedAt = const Value.absent(), - this.invited = const Value.absent(), - this.banned = const Value.absent(), - this.shadowBanned = const Value.absent(), - this.pinnedAt = const Value.absent(), - this.archivedAt = const Value.absent(), - this.isModerator = const Value.absent(), - this.extraData = const Value.absent(), - this.createdAt = const Value.absent(), - this.updatedAt = const Value.absent(), this.rowid = const Value.absent(), - }) : userId = Value(userId), + }) : queryHash = Value(queryHash), channelCid = Value(channelCid); - static Insertable custom({ - Expression? userId, + static Insertable custom({ + Expression? queryHash, Expression? channelCid, - Expression? channelRole, - Expression? inviteAcceptedAt, - Expression? inviteRejectedAt, - Expression? invited, - Expression? banned, - Expression? shadowBanned, - Expression? pinnedAt, - Expression? archivedAt, - Expression? isModerator, - Expression? extraData, - Expression? createdAt, - Expression? updatedAt, Expression? rowid, }) { return RawValuesInsertable({ - if (userId != null) 'user_id': userId, + if (queryHash != null) 'query_hash': queryHash, if (channelCid != null) 'channel_cid': channelCid, - if (channelRole != null) 'channel_role': channelRole, - if (inviteAcceptedAt != null) 'invite_accepted_at': inviteAcceptedAt, - if (inviteRejectedAt != null) 'invite_rejected_at': inviteRejectedAt, - if (invited != null) 'invited': invited, - if (banned != null) 'banned': banned, - if (shadowBanned != null) 'shadow_banned': shadowBanned, - if (pinnedAt != null) 'pinned_at': pinnedAt, - if (archivedAt != null) 'archived_at': archivedAt, - if (isModerator != null) 'is_moderator': isModerator, - if (extraData != null) 'extra_data': extraData, - if (createdAt != null) 'created_at': createdAt, - if (updatedAt != null) 'updated_at': updatedAt, if (rowid != null) 'rowid': rowid, }); } - MembersCompanion copyWith( - {Value? userId, + ChannelQueriesCompanion copyWith( + {Value? queryHash, Value? channelCid, - Value? channelRole, - Value? inviteAcceptedAt, - Value? inviteRejectedAt, - Value? invited, - Value? banned, - Value? shadowBanned, - Value? pinnedAt, - Value? archivedAt, - Value? isModerator, - Value?>? extraData, - Value? createdAt, - Value? updatedAt, Value? rowid}) { - return MembersCompanion( - userId: userId ?? this.userId, + return ChannelQueriesCompanion( + queryHash: queryHash ?? this.queryHash, channelCid: channelCid ?? this.channelCid, - channelRole: channelRole ?? this.channelRole, - inviteAcceptedAt: inviteAcceptedAt ?? this.inviteAcceptedAt, - inviteRejectedAt: inviteRejectedAt ?? this.inviteRejectedAt, - invited: invited ?? this.invited, - banned: banned ?? this.banned, - shadowBanned: shadowBanned ?? this.shadowBanned, - pinnedAt: pinnedAt ?? this.pinnedAt, - archivedAt: archivedAt ?? this.archivedAt, - isModerator: isModerator ?? this.isModerator, - extraData: extraData ?? this.extraData, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, rowid: rowid ?? this.rowid, ); } @@ -7319,49 +8527,12 @@ class MembersCompanion extends UpdateCompanion { @override Map toColumns(bool nullToAbsent) { final map = {}; - if (userId.present) { - map['user_id'] = Variable(userId.value); + if (queryHash.present) { + map['query_hash'] = Variable(queryHash.value); } if (channelCid.present) { map['channel_cid'] = Variable(channelCid.value); } - if (channelRole.present) { - map['channel_role'] = Variable(channelRole.value); - } - if (inviteAcceptedAt.present) { - map['invite_accepted_at'] = Variable(inviteAcceptedAt.value); - } - if (inviteRejectedAt.present) { - map['invite_rejected_at'] = Variable(inviteRejectedAt.value); - } - if (invited.present) { - map['invited'] = Variable(invited.value); - } - if (banned.present) { - map['banned'] = Variable(banned.value); - } - if (shadowBanned.present) { - map['shadow_banned'] = Variable(shadowBanned.value); - } - if (pinnedAt.present) { - map['pinned_at'] = Variable(pinnedAt.value); - } - if (archivedAt.present) { - map['archived_at'] = Variable(archivedAt.value); - } - if (isModerator.present) { - map['is_moderator'] = Variable(isModerator.value); - } - if (extraData.present) { - map['extra_data'] = Variable( - $MembersTable.$converterextraDatan.toSql(extraData.value)); - } - if (createdAt.present) { - map['created_at'] = Variable(createdAt.value); - } - if (updatedAt.present) { - map['updated_at'] = Variable(updatedAt.value); - } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -7370,1207 +8541,1474 @@ class MembersCompanion extends UpdateCompanion { @override String toString() { - return (StringBuffer('MembersCompanion(') - ..write('userId: $userId, ') + return (StringBuffer('ChannelQueriesCompanion(') + ..write('queryHash: $queryHash, ') ..write('channelCid: $channelCid, ') - ..write('channelRole: $channelRole, ') - ..write('inviteAcceptedAt: $inviteAcceptedAt, ') - ..write('inviteRejectedAt: $inviteRejectedAt, ') - ..write('invited: $invited, ') - ..write('banned: $banned, ') - ..write('shadowBanned: $shadowBanned, ') - ..write('pinnedAt: $pinnedAt, ') - ..write('archivedAt: $archivedAt, ') - ..write('isModerator: $isModerator, ') - ..write('extraData: $extraData, ') - ..write('createdAt: $createdAt, ') - ..write('updatedAt: $updatedAt, ') ..write('rowid: $rowid') ..write(')')) .toString(); } } -class $ReadsTable extends Reads with TableInfo<$ReadsTable, ReadEntity> { +class $ConnectionEventsTable extends ConnectionEvents + with TableInfo<$ConnectionEventsTable, ConnectionEventEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; - $ReadsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _lastReadMeta = - const VerificationMeta('lastRead'); + $ConnectionEventsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); @override - late final GeneratedColumn lastRead = GeneratedColumn( - 'last_read', aliasedName, false, - type: DriftSqlType.dateTime, requiredDuringInsert: true); - static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); @override - late final GeneratedColumn userId = GeneratedColumn( - 'user_id', aliasedName, false, + late final GeneratedColumn type = GeneratedColumn( + 'type', aliasedName, false, type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _channelCidMeta = - const VerificationMeta('channelCid'); @override - late final GeneratedColumn channelCid = GeneratedColumn( - 'channel_cid', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES channels (cid) ON DELETE CASCADE')); - static const VerificationMeta _unreadMessagesMeta = - const VerificationMeta('unreadMessages'); + late final GeneratedColumnWithTypeConverter?, String> + ownUser = GeneratedColumn('own_user', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false) + .withConverter?>( + $ConnectionEventsTable.$converterownUsern); + static const VerificationMeta _totalUnreadCountMeta = + const VerificationMeta('totalUnreadCount'); + @override + late final GeneratedColumn totalUnreadCount = GeneratedColumn( + 'total_unread_count', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _unreadChannelsMeta = + const VerificationMeta('unreadChannels'); @override - late final GeneratedColumn unreadMessages = GeneratedColumn( - 'unread_messages', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const Constant(0)); - static const VerificationMeta _lastReadMessageIdMeta = - const VerificationMeta('lastReadMessageId'); + late final GeneratedColumn unreadChannels = GeneratedColumn( + 'unread_channels', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _lastEventAtMeta = + const VerificationMeta('lastEventAt'); @override - late final GeneratedColumn lastReadMessageId = - GeneratedColumn('last_read_message_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + late final GeneratedColumn lastEventAt = GeneratedColumn( + 'last_event_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _lastSyncAtMeta = + const VerificationMeta('lastSyncAt'); @override - List get $columns => - [lastRead, userId, channelCid, unreadMessages, lastReadMessageId]; + late final GeneratedColumn lastSyncAt = GeneratedColumn( + 'last_sync_at', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [ + id, + type, + ownUser, + totalUnreadCount, + unreadChannels, + lastEventAt, + lastSyncAt + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; - static const String $name = 'reads'; + static const String $name = 'connection_events'; @override - VerificationContext validateIntegrity(Insertable instance, + VerificationContext validateIntegrity( + Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('last_read')) { - context.handle(_lastReadMeta, - lastRead.isAcceptableOrUnknown(data['last_read']!, _lastReadMeta)); - } else if (isInserting) { - context.missing(_lastReadMeta); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } - if (data.containsKey('user_id')) { - context.handle(_userIdMeta, - userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + if (data.containsKey('type')) { + context.handle( + _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); } else if (isInserting) { - context.missing(_userIdMeta); + context.missing(_typeMeta); } - if (data.containsKey('channel_cid')) { + if (data.containsKey('total_unread_count')) { context.handle( - _channelCidMeta, - channelCid.isAcceptableOrUnknown( - data['channel_cid']!, _channelCidMeta)); - } else if (isInserting) { - context.missing(_channelCidMeta); + _totalUnreadCountMeta, + totalUnreadCount.isAcceptableOrUnknown( + data['total_unread_count']!, _totalUnreadCountMeta)); } - if (data.containsKey('unread_messages')) { + if (data.containsKey('unread_channels')) { context.handle( - _unreadMessagesMeta, - unreadMessages.isAcceptableOrUnknown( - data['unread_messages']!, _unreadMessagesMeta)); + _unreadChannelsMeta, + unreadChannels.isAcceptableOrUnknown( + data['unread_channels']!, _unreadChannelsMeta)); } - if (data.containsKey('last_read_message_id')) { + if (data.containsKey('last_event_at')) { context.handle( - _lastReadMessageIdMeta, - lastReadMessageId.isAcceptableOrUnknown( - data['last_read_message_id']!, _lastReadMessageIdMeta)); + _lastEventAtMeta, + lastEventAt.isAcceptableOrUnknown( + data['last_event_at']!, _lastEventAtMeta)); + } + if (data.containsKey('last_sync_at')) { + context.handle( + _lastSyncAtMeta, + lastSyncAt.isAcceptableOrUnknown( + data['last_sync_at']!, _lastSyncAtMeta)); } return context; } @override - Set get $primaryKey => {userId, channelCid}; + Set get $primaryKey => {id}; @override - ReadEntity map(Map data, {String? tablePrefix}) { + ConnectionEventEntity map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return ReadEntity( - lastRead: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}last_read'])!, - userId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, - channelCid: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, - unreadMessages: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}unread_messages'])!, - lastReadMessageId: attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}last_read_message_id']), + return ConnectionEventEntity( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + type: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + ownUser: $ConnectionEventsTable.$converterownUsern.fromSql( + attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}own_user'])), + totalUnreadCount: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}total_unread_count']), + unreadChannels: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}unread_channels']), + lastEventAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_event_at']), + lastSyncAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_sync_at']), ); } @override - $ReadsTable createAlias(String alias) { - return $ReadsTable(attachedDatabase, alias); + $ConnectionEventsTable createAlias(String alias) { + return $ConnectionEventsTable(attachedDatabase, alias); } + + static TypeConverter, String> $converterownUser = + MapConverter(); + static TypeConverter?, String?> $converterownUsern = + NullAwareTypeConverter.wrap($converterownUser); } -class ReadEntity extends DataClass implements Insertable { - /// Date of the read event - final DateTime lastRead; +class ConnectionEventEntity extends DataClass + implements Insertable { + /// event id + final int id; - /// Id of the User who sent the event - final String userId; + /// event type + final String type; - /// The channel cid of which this read belongs - final String channelCid; + /// User object of the current user + final Map? ownUser; - /// Number of unread messages - final int unreadMessages; + /// The number of unread messages for current user + final int? totalUnreadCount; - /// Id of the last read message - final String? lastReadMessageId; - const ReadEntity( - {required this.lastRead, - required this.userId, - required this.channelCid, - required this.unreadMessages, - this.lastReadMessageId}); + /// User total unread channels for current user + final int? unreadChannels; + + /// DateTime of the last event + final DateTime? lastEventAt; + + /// DateTime of the last sync + final DateTime? lastSyncAt; + const ConnectionEventEntity( + {required this.id, + required this.type, + this.ownUser, + this.totalUnreadCount, + this.unreadChannels, + this.lastEventAt, + this.lastSyncAt}); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['last_read'] = Variable(lastRead); - map['user_id'] = Variable(userId); - map['channel_cid'] = Variable(channelCid); - map['unread_messages'] = Variable(unreadMessages); - if (!nullToAbsent || lastReadMessageId != null) { - map['last_read_message_id'] = Variable(lastReadMessageId); + map['id'] = Variable(id); + map['type'] = Variable(type); + if (!nullToAbsent || ownUser != null) { + map['own_user'] = Variable( + $ConnectionEventsTable.$converterownUsern.toSql(ownUser)); + } + if (!nullToAbsent || totalUnreadCount != null) { + map['total_unread_count'] = Variable(totalUnreadCount); + } + if (!nullToAbsent || unreadChannels != null) { + map['unread_channels'] = Variable(unreadChannels); + } + if (!nullToAbsent || lastEventAt != null) { + map['last_event_at'] = Variable(lastEventAt); + } + if (!nullToAbsent || lastSyncAt != null) { + map['last_sync_at'] = Variable(lastSyncAt); } return map; } - factory ReadEntity.fromJson(Map json, + factory ConnectionEventEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; - return ReadEntity( - lastRead: serializer.fromJson(json['lastRead']), - userId: serializer.fromJson(json['userId']), - channelCid: serializer.fromJson(json['channelCid']), - unreadMessages: serializer.fromJson(json['unreadMessages']), - lastReadMessageId: - serializer.fromJson(json['lastReadMessageId']), + return ConnectionEventEntity( + id: serializer.fromJson(json['id']), + type: serializer.fromJson(json['type']), + ownUser: serializer.fromJson?>(json['ownUser']), + totalUnreadCount: serializer.fromJson(json['totalUnreadCount']), + unreadChannels: serializer.fromJson(json['unreadChannels']), + lastEventAt: serializer.fromJson(json['lastEventAt']), + lastSyncAt: serializer.fromJson(json['lastSyncAt']), ); } @override Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'lastRead': serializer.toJson(lastRead), - 'userId': serializer.toJson(userId), - 'channelCid': serializer.toJson(channelCid), - 'unreadMessages': serializer.toJson(unreadMessages), - 'lastReadMessageId': serializer.toJson(lastReadMessageId), + 'id': serializer.toJson(id), + 'type': serializer.toJson(type), + 'ownUser': serializer.toJson?>(ownUser), + 'totalUnreadCount': serializer.toJson(totalUnreadCount), + 'unreadChannels': serializer.toJson(unreadChannels), + 'lastEventAt': serializer.toJson(lastEventAt), + 'lastSyncAt': serializer.toJson(lastSyncAt), }; } - ReadEntity copyWith( - {DateTime? lastRead, - String? userId, - String? channelCid, - int? unreadMessages, - Value lastReadMessageId = const Value.absent()}) => - ReadEntity( - lastRead: lastRead ?? this.lastRead, - userId: userId ?? this.userId, - channelCid: channelCid ?? this.channelCid, - unreadMessages: unreadMessages ?? this.unreadMessages, - lastReadMessageId: lastReadMessageId.present - ? lastReadMessageId.value - : this.lastReadMessageId, + ConnectionEventEntity copyWith( + {int? id, + String? type, + Value?> ownUser = const Value.absent(), + Value totalUnreadCount = const Value.absent(), + Value unreadChannels = const Value.absent(), + Value lastEventAt = const Value.absent(), + Value lastSyncAt = const Value.absent()}) => + ConnectionEventEntity( + id: id ?? this.id, + type: type ?? this.type, + ownUser: ownUser.present ? ownUser.value : this.ownUser, + totalUnreadCount: totalUnreadCount.present + ? totalUnreadCount.value + : this.totalUnreadCount, + unreadChannels: + unreadChannels.present ? unreadChannels.value : this.unreadChannels, + lastEventAt: lastEventAt.present ? lastEventAt.value : this.lastEventAt, + lastSyncAt: lastSyncAt.present ? lastSyncAt.value : this.lastSyncAt, ); - ReadEntity copyWithCompanion(ReadsCompanion data) { - return ReadEntity( - lastRead: data.lastRead.present ? data.lastRead.value : this.lastRead, - userId: data.userId.present ? data.userId.value : this.userId, - channelCid: - data.channelCid.present ? data.channelCid.value : this.channelCid, - unreadMessages: data.unreadMessages.present - ? data.unreadMessages.value - : this.unreadMessages, - lastReadMessageId: data.lastReadMessageId.present - ? data.lastReadMessageId.value - : this.lastReadMessageId, + ConnectionEventEntity copyWithCompanion(ConnectionEventsCompanion data) { + return ConnectionEventEntity( + id: data.id.present ? data.id.value : this.id, + type: data.type.present ? data.type.value : this.type, + ownUser: data.ownUser.present ? data.ownUser.value : this.ownUser, + totalUnreadCount: data.totalUnreadCount.present + ? data.totalUnreadCount.value + : this.totalUnreadCount, + unreadChannels: data.unreadChannels.present + ? data.unreadChannels.value + : this.unreadChannels, + lastEventAt: + data.lastEventAt.present ? data.lastEventAt.value : this.lastEventAt, + lastSyncAt: + data.lastSyncAt.present ? data.lastSyncAt.value : this.lastSyncAt, ); } @override String toString() { - return (StringBuffer('ReadEntity(') - ..write('lastRead: $lastRead, ') - ..write('userId: $userId, ') - ..write('channelCid: $channelCid, ') - ..write('unreadMessages: $unreadMessages, ') - ..write('lastReadMessageId: $lastReadMessageId') + return (StringBuffer('ConnectionEventEntity(') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('ownUser: $ownUser, ') + ..write('totalUnreadCount: $totalUnreadCount, ') + ..write('unreadChannels: $unreadChannels, ') + ..write('lastEventAt: $lastEventAt, ') + ..write('lastSyncAt: $lastSyncAt') ..write(')')) .toString(); } @override - int get hashCode => Object.hash( - lastRead, userId, channelCid, unreadMessages, lastReadMessageId); + int get hashCode => Object.hash(id, type, ownUser, totalUnreadCount, + unreadChannels, lastEventAt, lastSyncAt); @override bool operator ==(Object other) => identical(this, other) || - (other is ReadEntity && - other.lastRead == this.lastRead && - other.userId == this.userId && - other.channelCid == this.channelCid && - other.unreadMessages == this.unreadMessages && - other.lastReadMessageId == this.lastReadMessageId); + (other is ConnectionEventEntity && + other.id == this.id && + other.type == this.type && + other.ownUser == this.ownUser && + other.totalUnreadCount == this.totalUnreadCount && + other.unreadChannels == this.unreadChannels && + other.lastEventAt == this.lastEventAt && + other.lastSyncAt == this.lastSyncAt); } -class ReadsCompanion extends UpdateCompanion { - final Value lastRead; - final Value userId; - final Value channelCid; - final Value unreadMessages; - final Value lastReadMessageId; - final Value rowid; - const ReadsCompanion({ - this.lastRead = const Value.absent(), - this.userId = const Value.absent(), - this.channelCid = const Value.absent(), - this.unreadMessages = const Value.absent(), - this.lastReadMessageId = const Value.absent(), - this.rowid = const Value.absent(), +class ConnectionEventsCompanion extends UpdateCompanion { + final Value id; + final Value type; + final Value?> ownUser; + final Value totalUnreadCount; + final Value unreadChannels; + final Value lastEventAt; + final Value lastSyncAt; + const ConnectionEventsCompanion({ + this.id = const Value.absent(), + this.type = const Value.absent(), + this.ownUser = const Value.absent(), + this.totalUnreadCount = const Value.absent(), + this.unreadChannels = const Value.absent(), + this.lastEventAt = const Value.absent(), + this.lastSyncAt = const Value.absent(), }); - ReadsCompanion.insert({ - required DateTime lastRead, - required String userId, - required String channelCid, - this.unreadMessages = const Value.absent(), - this.lastReadMessageId = const Value.absent(), - this.rowid = const Value.absent(), - }) : lastRead = Value(lastRead), - userId = Value(userId), - channelCid = Value(channelCid); - static Insertable custom({ - Expression? lastRead, - Expression? userId, - Expression? channelCid, - Expression? unreadMessages, - Expression? lastReadMessageId, - Expression? rowid, + ConnectionEventsCompanion.insert({ + this.id = const Value.absent(), + required String type, + this.ownUser = const Value.absent(), + this.totalUnreadCount = const Value.absent(), + this.unreadChannels = const Value.absent(), + this.lastEventAt = const Value.absent(), + this.lastSyncAt = const Value.absent(), + }) : type = Value(type); + static Insertable custom({ + Expression? id, + Expression? type, + Expression? ownUser, + Expression? totalUnreadCount, + Expression? unreadChannels, + Expression? lastEventAt, + Expression? lastSyncAt, }) { return RawValuesInsertable({ - if (lastRead != null) 'last_read': lastRead, - if (userId != null) 'user_id': userId, - if (channelCid != null) 'channel_cid': channelCid, - if (unreadMessages != null) 'unread_messages': unreadMessages, - if (lastReadMessageId != null) 'last_read_message_id': lastReadMessageId, - if (rowid != null) 'rowid': rowid, + if (id != null) 'id': id, + if (type != null) 'type': type, + if (ownUser != null) 'own_user': ownUser, + if (totalUnreadCount != null) 'total_unread_count': totalUnreadCount, + if (unreadChannels != null) 'unread_channels': unreadChannels, + if (lastEventAt != null) 'last_event_at': lastEventAt, + if (lastSyncAt != null) 'last_sync_at': lastSyncAt, }); } - ReadsCompanion copyWith( - {Value? lastRead, - Value? userId, - Value? channelCid, - Value? unreadMessages, - Value? lastReadMessageId, - Value? rowid}) { - return ReadsCompanion( - lastRead: lastRead ?? this.lastRead, - userId: userId ?? this.userId, - channelCid: channelCid ?? this.channelCid, - unreadMessages: unreadMessages ?? this.unreadMessages, - lastReadMessageId: lastReadMessageId ?? this.lastReadMessageId, - rowid: rowid ?? this.rowid, + ConnectionEventsCompanion copyWith( + {Value? id, + Value? type, + Value?>? ownUser, + Value? totalUnreadCount, + Value? unreadChannels, + Value? lastEventAt, + Value? lastSyncAt}) { + return ConnectionEventsCompanion( + id: id ?? this.id, + type: type ?? this.type, + ownUser: ownUser ?? this.ownUser, + totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, + unreadChannels: unreadChannels ?? this.unreadChannels, + lastEventAt: lastEventAt ?? this.lastEventAt, + lastSyncAt: lastSyncAt ?? this.lastSyncAt, ); } @override Map toColumns(bool nullToAbsent) { final map = {}; - if (lastRead.present) { - map['last_read'] = Variable(lastRead.value); + if (id.present) { + map['id'] = Variable(id.value); } - if (userId.present) { - map['user_id'] = Variable(userId.value); + if (type.present) { + map['type'] = Variable(type.value); } - if (channelCid.present) { - map['channel_cid'] = Variable(channelCid.value); + if (ownUser.present) { + map['own_user'] = Variable( + $ConnectionEventsTable.$converterownUsern.toSql(ownUser.value)); } - if (unreadMessages.present) { - map['unread_messages'] = Variable(unreadMessages.value); + if (totalUnreadCount.present) { + map['total_unread_count'] = Variable(totalUnreadCount.value); } - if (lastReadMessageId.present) { - map['last_read_message_id'] = Variable(lastReadMessageId.value); + if (unreadChannels.present) { + map['unread_channels'] = Variable(unreadChannels.value); } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); + if (lastEventAt.present) { + map['last_event_at'] = Variable(lastEventAt.value); + } + if (lastSyncAt.present) { + map['last_sync_at'] = Variable(lastSyncAt.value); } return map; } @override String toString() { - return (StringBuffer('ReadsCompanion(') - ..write('lastRead: $lastRead, ') - ..write('userId: $userId, ') - ..write('channelCid: $channelCid, ') - ..write('unreadMessages: $unreadMessages, ') - ..write('lastReadMessageId: $lastReadMessageId, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); - } -} - -class $ChannelQueriesTable extends ChannelQueries - with TableInfo<$ChannelQueriesTable, ChannelQueryEntity> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $ChannelQueriesTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _queryHashMeta = - const VerificationMeta('queryHash'); - @override - late final GeneratedColumn queryHash = GeneratedColumn( - 'query_hash', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _channelCidMeta = - const VerificationMeta('channelCid'); - @override - late final GeneratedColumn channelCid = GeneratedColumn( - 'channel_cid', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - @override - List get $columns => [queryHash, channelCid]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'channel_queries'; - @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('query_hash')) { - context.handle(_queryHashMeta, - queryHash.isAcceptableOrUnknown(data['query_hash']!, _queryHashMeta)); - } else if (isInserting) { - context.missing(_queryHashMeta); - } - if (data.containsKey('channel_cid')) { - context.handle( - _channelCidMeta, - channelCid.isAcceptableOrUnknown( - data['channel_cid']!, _channelCidMeta)); - } else if (isInserting) { - context.missing(_channelCidMeta); - } - return context; + return (StringBuffer('ConnectionEventsCompanion(') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('ownUser: $ownUser, ') + ..write('totalUnreadCount: $totalUnreadCount, ') + ..write('unreadChannels: $unreadChannels, ') + ..write('lastEventAt: $lastEventAt, ') + ..write('lastSyncAt: $lastSyncAt') + ..write(')')) + .toString(); } +} +abstract class _$DriftChatDatabase extends GeneratedDatabase { + _$DriftChatDatabase(QueryExecutor e) : super(e); + $DriftChatDatabaseManager get managers => $DriftChatDatabaseManager(this); + late final $ChannelsTable channels = $ChannelsTable(this); + late final $MessagesTable messages = $MessagesTable(this); + late final $DraftMessagesTable draftMessages = $DraftMessagesTable(this); + late final $LocationsTable locations = $LocationsTable(this); + late final $PinnedMessagesTable pinnedMessages = $PinnedMessagesTable(this); + late final $PollsTable polls = $PollsTable(this); + late final $PollVotesTable pollVotes = $PollVotesTable(this); + late final $PinnedMessageReactionsTable pinnedMessageReactions = + $PinnedMessageReactionsTable(this); + late final $ReactionsTable reactions = $ReactionsTable(this); + late final $UsersTable users = $UsersTable(this); + late final $MembersTable members = $MembersTable(this); + late final $ReadsTable reads = $ReadsTable(this); + late final $ChannelQueriesTable channelQueries = $ChannelQueriesTable(this); + late final $ConnectionEventsTable connectionEvents = + $ConnectionEventsTable(this); + late final UserDao userDao = UserDao(this as DriftChatDatabase); + late final ChannelDao channelDao = ChannelDao(this as DriftChatDatabase); + late final MessageDao messageDao = MessageDao(this as DriftChatDatabase); + late final DraftMessageDao draftMessageDao = + DraftMessageDao(this as DriftChatDatabase); + late final LocationDao locationDao = LocationDao(this as DriftChatDatabase); + late final PinnedMessageDao pinnedMessageDao = + PinnedMessageDao(this as DriftChatDatabase); + late final PinnedMessageReactionDao pinnedMessageReactionDao = + PinnedMessageReactionDao(this as DriftChatDatabase); + late final MemberDao memberDao = MemberDao(this as DriftChatDatabase); + late final PollDao pollDao = PollDao(this as DriftChatDatabase); + late final PollVoteDao pollVoteDao = PollVoteDao(this as DriftChatDatabase); + late final ReactionDao reactionDao = ReactionDao(this as DriftChatDatabase); + late final ReadDao readDao = ReadDao(this as DriftChatDatabase); + late final ChannelQueryDao channelQueryDao = + ChannelQueryDao(this as DriftChatDatabase); + late final ConnectionEventDao connectionEventDao = + ConnectionEventDao(this as DriftChatDatabase); @override - Set get $primaryKey => {queryHash, channelCid}; + Iterable> get allTables => + allSchemaEntities.whereType>(); @override - ChannelQueryEntity map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return ChannelQueryEntity( - queryHash: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}query_hash'])!, - channelCid: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, - ); - } - + List get allSchemaEntities => [ + channels, + messages, + draftMessages, + locations, + pinnedMessages, + polls, + pollVotes, + pinnedMessageReactions, + reactions, + users, + members, + reads, + channelQueries, + connectionEvents + ]; @override - $ChannelQueriesTable createAlias(String alias) { - return $ChannelQueriesTable(attachedDatabase, alias); - } + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( + [ + WritePropagation( + on: TableUpdateQuery.onTableName('channels', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('messages', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('draft_messages', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('channels', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('draft_messages', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('channels', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('locations', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('locations', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('polls', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('poll_votes', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('pinned_messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('pinned_message_reactions', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('messages', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('reactions', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('channels', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('members', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('channels', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('reads', kind: UpdateKind.delete), + ], + ), + ], + ); } -class ChannelQueryEntity extends DataClass - implements Insertable { - /// The unique hash of this query - final String queryHash; +typedef $$ChannelsTableCreateCompanionBuilder = ChannelsCompanion Function({ + required String id, + required String type, + required String cid, + Value?> ownCapabilities, + required Map config, + Value frozen, + Value lastMessageAt, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value memberCount, + Value createdById, + Value?> extraData, + Value rowid, +}); +typedef $$ChannelsTableUpdateCompanionBuilder = ChannelsCompanion Function({ + Value id, + Value type, + Value cid, + Value?> ownCapabilities, + Value> config, + Value frozen, + Value lastMessageAt, + Value createdAt, + Value updatedAt, + Value deletedAt, + Value memberCount, + Value createdById, + Value?> extraData, + Value rowid, +}); - /// The channel cid of this query - final String channelCid; - const ChannelQueryEntity({required this.queryHash, required this.channelCid}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['query_hash'] = Variable(queryHash); - map['channel_cid'] = Variable(channelCid); - return map; - } +final class $$ChannelsTableReferences + extends BaseReferences<_$DriftChatDatabase, $ChannelsTable, ChannelEntity> { + $$ChannelsTableReferences(super.$_db, super.$_table, super.$_typedResult); - factory ChannelQueryEntity.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return ChannelQueryEntity( - queryHash: serializer.fromJson(json['queryHash']), - channelCid: serializer.fromJson(json['channelCid']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'queryHash': serializer.toJson(queryHash), - 'channelCid': serializer.toJson(channelCid), - }; - } + static MultiTypedResultKey<$MessagesTable, List> + _messagesRefsTable(_$DriftChatDatabase db) => + MultiTypedResultKey.fromTable(db.messages, + aliasName: $_aliasNameGenerator( + db.channels.cid, db.messages.channelCid)); - ChannelQueryEntity copyWith({String? queryHash, String? channelCid}) => - ChannelQueryEntity( - queryHash: queryHash ?? this.queryHash, - channelCid: channelCid ?? this.channelCid, - ); - ChannelQueryEntity copyWithCompanion(ChannelQueriesCompanion data) { - return ChannelQueryEntity( - queryHash: data.queryHash.present ? data.queryHash.value : this.queryHash, - channelCid: - data.channelCid.present ? data.channelCid.value : this.channelCid, - ); + $$MessagesTableProcessedTableManager get messagesRefs { + final manager = $$MessagesTableTableManager($_db, $_db.messages).filter( + (f) => f.channelCid.cid.sqlEquals($_itemColumn('cid')!)); + + final cache = $_typedResult.readTableOrNull(_messagesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); } - @override - String toString() { - return (StringBuffer('ChannelQueryEntity(') - ..write('queryHash: $queryHash, ') - ..write('channelCid: $channelCid') - ..write(')')) - .toString(); + static MultiTypedResultKey<$DraftMessagesTable, List> + _draftMessagesRefsTable(_$DriftChatDatabase db) => + MultiTypedResultKey.fromTable(db.draftMessages, + aliasName: $_aliasNameGenerator( + db.channels.cid, db.draftMessages.channelCid)); + + $$DraftMessagesTableProcessedTableManager get draftMessagesRefs { + final manager = $$DraftMessagesTableTableManager($_db, $_db.draftMessages) + .filter( + (f) => f.channelCid.cid.sqlEquals($_itemColumn('cid')!)); + + final cache = $_typedResult.readTableOrNull(_draftMessagesRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); } - @override - int get hashCode => Object.hash(queryHash, channelCid); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is ChannelQueryEntity && - other.queryHash == this.queryHash && - other.channelCid == this.channelCid); -} + static MultiTypedResultKey<$LocationsTable, List> + _locationsRefsTable(_$DriftChatDatabase db) => + MultiTypedResultKey.fromTable(db.locations, + aliasName: $_aliasNameGenerator( + db.channels.cid, db.locations.channelCid)); -class ChannelQueriesCompanion extends UpdateCompanion { - final Value queryHash; - final Value channelCid; - final Value rowid; - const ChannelQueriesCompanion({ - this.queryHash = const Value.absent(), - this.channelCid = const Value.absent(), - this.rowid = const Value.absent(), - }); - ChannelQueriesCompanion.insert({ - required String queryHash, - required String channelCid, - this.rowid = const Value.absent(), - }) : queryHash = Value(queryHash), - channelCid = Value(channelCid); - static Insertable custom({ - Expression? queryHash, - Expression? channelCid, - Expression? rowid, - }) { - return RawValuesInsertable({ - if (queryHash != null) 'query_hash': queryHash, - if (channelCid != null) 'channel_cid': channelCid, - if (rowid != null) 'rowid': rowid, - }); - } + $$LocationsTableProcessedTableManager get locationsRefs { + final manager = $$LocationsTableTableManager($_db, $_db.locations).filter( + (f) => f.channelCid.cid.sqlEquals($_itemColumn('cid')!)); - ChannelQueriesCompanion copyWith( - {Value? queryHash, - Value? channelCid, - Value? rowid}) { - return ChannelQueriesCompanion( - queryHash: queryHash ?? this.queryHash, - channelCid: channelCid ?? this.channelCid, - rowid: rowid ?? this.rowid, - ); + final cache = $_typedResult.readTableOrNull(_locationsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); } - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (queryHash.present) { - map['query_hash'] = Variable(queryHash.value); - } - if (channelCid.present) { - map['channel_cid'] = Variable(channelCid.value); - } - if (rowid.present) { - map['rowid'] = Variable(rowid.value); - } - return map; + static MultiTypedResultKey<$MembersTable, List> + _membersRefsTable(_$DriftChatDatabase db) => + MultiTypedResultKey.fromTable(db.members, + aliasName: + $_aliasNameGenerator(db.channels.cid, db.members.channelCid)); + + $$MembersTableProcessedTableManager get membersRefs { + final manager = $$MembersTableTableManager($_db, $_db.members).filter( + (f) => f.channelCid.cid.sqlEquals($_itemColumn('cid')!)); + + final cache = $_typedResult.readTableOrNull(_membersRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); } - @override - String toString() { - return (StringBuffer('ChannelQueriesCompanion(') - ..write('queryHash: $queryHash, ') - ..write('channelCid: $channelCid, ') - ..write('rowid: $rowid') - ..write(')')) - .toString(); + static MultiTypedResultKey<$ReadsTable, List> _readsRefsTable( + _$DriftChatDatabase db) => + MultiTypedResultKey.fromTable(db.reads, + aliasName: + $_aliasNameGenerator(db.channels.cid, db.reads.channelCid)); + + $$ReadsTableProcessedTableManager get readsRefs { + final manager = $$ReadsTableTableManager($_db, $_db.reads).filter( + (f) => f.channelCid.cid.sqlEquals($_itemColumn('cid')!)); + + final cache = $_typedResult.readTableOrNull(_readsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); } } -class $ConnectionEventsTable extends ConnectionEvents - with TableInfo<$ConnectionEventsTable, ConnectionEventEntity> { - @override - final GeneratedDatabase attachedDatabase; - final String? _alias; - $ConnectionEventsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _idMeta = const VerificationMeta('id'); - @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: false); - static const VerificationMeta _typeMeta = const VerificationMeta('type'); - @override - late final GeneratedColumn type = GeneratedColumn( - 'type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - @override - late final GeneratedColumnWithTypeConverter?, String> - ownUser = GeneratedColumn('own_user', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $ConnectionEventsTable.$converterownUsern); - static const VerificationMeta _totalUnreadCountMeta = - const VerificationMeta('totalUnreadCount'); - @override - late final GeneratedColumn totalUnreadCount = GeneratedColumn( - 'total_unread_count', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - static const VerificationMeta _unreadChannelsMeta = - const VerificationMeta('unreadChannels'); - @override - late final GeneratedColumn unreadChannels = GeneratedColumn( - 'unread_channels', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - static const VerificationMeta _lastEventAtMeta = - const VerificationMeta('lastEventAt'); - @override - late final GeneratedColumn lastEventAt = GeneratedColumn( - 'last_event_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _lastSyncAtMeta = - const VerificationMeta('lastSyncAt'); - @override - late final GeneratedColumn lastSyncAt = GeneratedColumn( - 'last_sync_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - @override - List get $columns => [ - id, - type, - ownUser, - totalUnreadCount, - unreadChannels, - lastEventAt, - lastSyncAt - ]; - @override - String get aliasedName => _alias ?? actualTableName; - @override - String get actualTableName => $name; - static const String $name = 'connection_events'; - @override - VerificationContext validateIntegrity( - Insertable instance, - {bool isInserting = false}) { - final context = VerificationContext(); - final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } - if (data.containsKey('type')) { - context.handle( - _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); - } else if (isInserting) { - context.missing(_typeMeta); - } - if (data.containsKey('total_unread_count')) { - context.handle( - _totalUnreadCountMeta, - totalUnreadCount.isAcceptableOrUnknown( - data['total_unread_count']!, _totalUnreadCountMeta)); - } - if (data.containsKey('unread_channels')) { - context.handle( - _unreadChannelsMeta, - unreadChannels.isAcceptableOrUnknown( - data['unread_channels']!, _unreadChannelsMeta)); - } - if (data.containsKey('last_event_at')) { - context.handle( - _lastEventAtMeta, - lastEventAt.isAcceptableOrUnknown( - data['last_event_at']!, _lastEventAtMeta)); - } - if (data.containsKey('last_sync_at')) { - context.handle( - _lastSyncAtMeta, - lastSyncAt.isAcceptableOrUnknown( - data['last_sync_at']!, _lastSyncAtMeta)); - } - return context; +class $$ChannelsTableFilterComposer + extends Composer<_$DriftChatDatabase, $ChannelsTable> { + $$ChannelsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnFilters(column)); + + ColumnFilters get cid => $composableBuilder( + column: $table.cid, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters?, List, String> + get ownCapabilities => $composableBuilder( + column: $table.ownCapabilities, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters, Map, + String> + get config => $composableBuilder( + column: $table.config, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get frozen => $composableBuilder( + column: $table.frozen, builder: (column) => ColumnFilters(column)); + + ColumnFilters get lastMessageAt => $composableBuilder( + column: $table.lastMessageAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnFilters(column)); + + ColumnFilters get memberCount => $composableBuilder( + column: $table.memberCount, builder: (column) => ColumnFilters(column)); + + ColumnFilters get createdById => $composableBuilder( + column: $table.createdById, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters?, Map, + String> + get extraData => $composableBuilder( + column: $table.extraData, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + Expression messagesRefs( + Expression Function($$MessagesTableFilterComposer f) f) { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.cid, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.channelCid, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); } - @override - Set get $primaryKey => {id}; - @override - ConnectionEventEntity map(Map data, {String? tablePrefix}) { - final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; - return ConnectionEventEntity( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, - type: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}type'])!, - ownUser: $ConnectionEventsTable.$converterownUsern.fromSql( - attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}own_user'])), - totalUnreadCount: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}total_unread_count']), - unreadChannels: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}unread_channels']), - lastEventAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}last_event_at']), - lastSyncAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}last_sync_at']), - ); + Expression draftMessagesRefs( + Expression Function($$DraftMessagesTableFilterComposer f) f) { + final $$DraftMessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.cid, + referencedTable: $db.draftMessages, + getReferencedColumn: (t) => t.channelCid, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$DraftMessagesTableFilterComposer( + $db: $db, + $table: $db.draftMessages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); } - @override - $ConnectionEventsTable createAlias(String alias) { - return $ConnectionEventsTable(attachedDatabase, alias); + Expression locationsRefs( + Expression Function($$LocationsTableFilterComposer f) f) { + final $$LocationsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.cid, + referencedTable: $db.locations, + getReferencedColumn: (t) => t.channelCid, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$LocationsTableFilterComposer( + $db: $db, + $table: $db.locations, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); } - static TypeConverter, String> $converterownUser = - MapConverter(); - static TypeConverter?, String?> $converterownUsern = - NullAwareTypeConverter.wrap($converterownUser); + Expression membersRefs( + Expression Function($$MembersTableFilterComposer f) f) { + final $$MembersTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.cid, + referencedTable: $db.members, + getReferencedColumn: (t) => t.channelCid, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MembersTableFilterComposer( + $db: $db, + $table: $db.members, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression readsRefs( + Expression Function($$ReadsTableFilterComposer f) f) { + final $$ReadsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.cid, + referencedTable: $db.reads, + getReferencedColumn: (t) => t.channelCid, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReadsTableFilterComposer( + $db: $db, + $table: $db.reads, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } } -class ConnectionEventEntity extends DataClass - implements Insertable { - /// event id - final int id; +class $$ChannelsTableOrderingComposer + extends Composer<_$DriftChatDatabase, $ChannelsTable> { + $$ChannelsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); - /// event type - final String type; + ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnOrderings(column)); - /// User object of the current user - final Map? ownUser; + ColumnOrderings get cid => $composableBuilder( + column: $table.cid, builder: (column) => ColumnOrderings(column)); - /// The number of unread messages for current user - final int? totalUnreadCount; + ColumnOrderings get ownCapabilities => $composableBuilder( + column: $table.ownCapabilities, + builder: (column) => ColumnOrderings(column)); - /// User total unread channels for current user - final int? unreadChannels; + ColumnOrderings get config => $composableBuilder( + column: $table.config, builder: (column) => ColumnOrderings(column)); - /// DateTime of the last event - final DateTime? lastEventAt; + ColumnOrderings get frozen => $composableBuilder( + column: $table.frozen, builder: (column) => ColumnOrderings(column)); - /// DateTime of the last sync - final DateTime? lastSyncAt; - const ConnectionEventEntity( - {required this.id, - required this.type, - this.ownUser, - this.totalUnreadCount, - this.unreadChannels, - this.lastEventAt, - this.lastSyncAt}); - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - map['id'] = Variable(id); - map['type'] = Variable(type); - if (!nullToAbsent || ownUser != null) { - map['own_user'] = Variable( - $ConnectionEventsTable.$converterownUsern.toSql(ownUser)); - } - if (!nullToAbsent || totalUnreadCount != null) { - map['total_unread_count'] = Variable(totalUnreadCount); - } - if (!nullToAbsent || unreadChannels != null) { - map['unread_channels'] = Variable(unreadChannels); - } - if (!nullToAbsent || lastEventAt != null) { - map['last_event_at'] = Variable(lastEventAt); - } - if (!nullToAbsent || lastSyncAt != null) { - map['last_sync_at'] = Variable(lastSyncAt); - } - return map; - } + ColumnOrderings get lastMessageAt => $composableBuilder( + column: $table.lastMessageAt, + builder: (column) => ColumnOrderings(column)); - factory ConnectionEventEntity.fromJson(Map json, - {ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return ConnectionEventEntity( - id: serializer.fromJson(json['id']), - type: serializer.fromJson(json['type']), - ownUser: serializer.fromJson?>(json['ownUser']), - totalUnreadCount: serializer.fromJson(json['totalUnreadCount']), - unreadChannels: serializer.fromJson(json['unreadChannels']), - lastEventAt: serializer.fromJson(json['lastEventAt']), - lastSyncAt: serializer.fromJson(json['lastSyncAt']), - ); - } - @override - Map toJson({ValueSerializer? serializer}) { - serializer ??= driftRuntimeOptions.defaultSerializer; - return { - 'id': serializer.toJson(id), - 'type': serializer.toJson(type), - 'ownUser': serializer.toJson?>(ownUser), - 'totalUnreadCount': serializer.toJson(totalUnreadCount), - 'unreadChannels': serializer.toJson(unreadChannels), - 'lastEventAt': serializer.toJson(lastEventAt), - 'lastSyncAt': serializer.toJson(lastSyncAt), - }; - } + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); - ConnectionEventEntity copyWith( - {int? id, - String? type, - Value?> ownUser = const Value.absent(), - Value totalUnreadCount = const Value.absent(), - Value unreadChannels = const Value.absent(), - Value lastEventAt = const Value.absent(), - Value lastSyncAt = const Value.absent()}) => - ConnectionEventEntity( - id: id ?? this.id, - type: type ?? this.type, - ownUser: ownUser.present ? ownUser.value : this.ownUser, - totalUnreadCount: totalUnreadCount.present - ? totalUnreadCount.value - : this.totalUnreadCount, - unreadChannels: - unreadChannels.present ? unreadChannels.value : this.unreadChannels, - lastEventAt: lastEventAt.present ? lastEventAt.value : this.lastEventAt, - lastSyncAt: lastSyncAt.present ? lastSyncAt.value : this.lastSyncAt, - ); - ConnectionEventEntity copyWithCompanion(ConnectionEventsCompanion data) { - return ConnectionEventEntity( - id: data.id.present ? data.id.value : this.id, - type: data.type.present ? data.type.value : this.type, - ownUser: data.ownUser.present ? data.ownUser.value : this.ownUser, - totalUnreadCount: data.totalUnreadCount.present - ? data.totalUnreadCount.value - : this.totalUnreadCount, - unreadChannels: data.unreadChannels.present - ? data.unreadChannels.value - : this.unreadChannels, - lastEventAt: - data.lastEventAt.present ? data.lastEventAt.value : this.lastEventAt, - lastSyncAt: - data.lastSyncAt.present ? data.lastSyncAt.value : this.lastSyncAt, - ); - } + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - @override - String toString() { - return (StringBuffer('ConnectionEventEntity(') - ..write('id: $id, ') - ..write('type: $type, ') - ..write('ownUser: $ownUser, ') - ..write('totalUnreadCount: $totalUnreadCount, ') - ..write('unreadChannels: $unreadChannels, ') - ..write('lastEventAt: $lastEventAt, ') - ..write('lastSyncAt: $lastSyncAt') - ..write(')')) - .toString(); - } + ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); - @override - int get hashCode => Object.hash(id, type, ownUser, totalUnreadCount, - unreadChannels, lastEventAt, lastSyncAt); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is ConnectionEventEntity && - other.id == this.id && - other.type == this.type && - other.ownUser == this.ownUser && - other.totalUnreadCount == this.totalUnreadCount && - other.unreadChannels == this.unreadChannels && - other.lastEventAt == this.lastEventAt && - other.lastSyncAt == this.lastSyncAt); + ColumnOrderings get memberCount => $composableBuilder( + column: $table.memberCount, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdById => $composableBuilder( + column: $table.createdById, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get extraData => $composableBuilder( + column: $table.extraData, builder: (column) => ColumnOrderings(column)); } -class ConnectionEventsCompanion extends UpdateCompanion { - final Value id; - final Value type; - final Value?> ownUser; - final Value totalUnreadCount; - final Value unreadChannels; - final Value lastEventAt; - final Value lastSyncAt; - const ConnectionEventsCompanion({ - this.id = const Value.absent(), - this.type = const Value.absent(), - this.ownUser = const Value.absent(), - this.totalUnreadCount = const Value.absent(), - this.unreadChannels = const Value.absent(), - this.lastEventAt = const Value.absent(), - this.lastSyncAt = const Value.absent(), +class $$ChannelsTableAnnotationComposer + extends Composer<_$DriftChatDatabase, $ChannelsTable> { + $$ChannelsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, }); - ConnectionEventsCompanion.insert({ - this.id = const Value.absent(), - required String type, - this.ownUser = const Value.absent(), - this.totalUnreadCount = const Value.absent(), - this.unreadChannels = const Value.absent(), - this.lastEventAt = const Value.absent(), - this.lastSyncAt = const Value.absent(), - }) : type = Value(type); - static Insertable custom({ - Expression? id, - Expression? type, - Expression? ownUser, - Expression? totalUnreadCount, - Expression? unreadChannels, - Expression? lastEventAt, - Expression? lastSyncAt, - }) { - return RawValuesInsertable({ - if (id != null) 'id': id, - if (type != null) 'type': type, - if (ownUser != null) 'own_user': ownUser, - if (totalUnreadCount != null) 'total_unread_count': totalUnreadCount, - if (unreadChannels != null) 'unread_channels': unreadChannels, - if (lastEventAt != null) 'last_event_at': lastEventAt, - if (lastSyncAt != null) 'last_sync_at': lastSyncAt, - }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get cid => + $composableBuilder(column: $table.cid, builder: (column) => column); + + GeneratedColumnWithTypeConverter?, String> get ownCapabilities => + $composableBuilder( + column: $table.ownCapabilities, builder: (column) => column); + + GeneratedColumnWithTypeConverter, String> get config => + $composableBuilder(column: $table.config, builder: (column) => column); + + GeneratedColumn get frozen => + $composableBuilder(column: $table.frozen, builder: (column) => column); + + GeneratedColumn get lastMessageAt => $composableBuilder( + column: $table.lastMessageAt, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + GeneratedColumn get memberCount => $composableBuilder( + column: $table.memberCount, builder: (column) => column); + + GeneratedColumn get createdById => $composableBuilder( + column: $table.createdById, builder: (column) => column); + + GeneratedColumnWithTypeConverter?, String> + get extraData => $composableBuilder( + column: $table.extraData, builder: (column) => column); + + Expression messagesRefs( + Expression Function($$MessagesTableAnnotationComposer a) f) { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.cid, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.channelCid, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); } - ConnectionEventsCompanion copyWith( - {Value? id, - Value? type, - Value?>? ownUser, - Value? totalUnreadCount, - Value? unreadChannels, - Value? lastEventAt, - Value? lastSyncAt}) { - return ConnectionEventsCompanion( - id: id ?? this.id, - type: type ?? this.type, - ownUser: ownUser ?? this.ownUser, - totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, - unreadChannels: unreadChannels ?? this.unreadChannels, - lastEventAt: lastEventAt ?? this.lastEventAt, - lastSyncAt: lastSyncAt ?? this.lastSyncAt, - ); + Expression draftMessagesRefs( + Expression Function($$DraftMessagesTableAnnotationComposer a) f) { + final $$DraftMessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.cid, + referencedTable: $db.draftMessages, + getReferencedColumn: (t) => t.channelCid, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$DraftMessagesTableAnnotationComposer( + $db: $db, + $table: $db.draftMessages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); } - @override - Map toColumns(bool nullToAbsent) { - final map = {}; - if (id.present) { - map['id'] = Variable(id.value); - } - if (type.present) { - map['type'] = Variable(type.value); - } - if (ownUser.present) { - map['own_user'] = Variable( - $ConnectionEventsTable.$converterownUsern.toSql(ownUser.value)); - } - if (totalUnreadCount.present) { - map['total_unread_count'] = Variable(totalUnreadCount.value); - } - if (unreadChannels.present) { - map['unread_channels'] = Variable(unreadChannels.value); - } - if (lastEventAt.present) { - map['last_event_at'] = Variable(lastEventAt.value); - } - if (lastSyncAt.present) { - map['last_sync_at'] = Variable(lastSyncAt.value); - } - return map; + Expression locationsRefs( + Expression Function($$LocationsTableAnnotationComposer a) f) { + final $$LocationsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.cid, + referencedTable: $db.locations, + getReferencedColumn: (t) => t.channelCid, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$LocationsTableAnnotationComposer( + $db: $db, + $table: $db.locations, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); } - @override - String toString() { - return (StringBuffer('ConnectionEventsCompanion(') - ..write('id: $id, ') - ..write('type: $type, ') - ..write('ownUser: $ownUser, ') - ..write('totalUnreadCount: $totalUnreadCount, ') - ..write('unreadChannels: $unreadChannels, ') - ..write('lastEventAt: $lastEventAt, ') - ..write('lastSyncAt: $lastSyncAt') - ..write(')')) - .toString(); + Expression membersRefs( + Expression Function($$MembersTableAnnotationComposer a) f) { + final $$MembersTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.cid, + referencedTable: $db.members, + getReferencedColumn: (t) => t.channelCid, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MembersTableAnnotationComposer( + $db: $db, + $table: $db.members, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); + } + + Expression readsRefs( + Expression Function($$ReadsTableAnnotationComposer a) f) { + final $$ReadsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.cid, + referencedTable: $db.reads, + getReferencedColumn: (t) => t.channelCid, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ReadsTableAnnotationComposer( + $db: $db, + $table: $db.reads, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return f(composer); } } -abstract class _$DriftChatDatabase extends GeneratedDatabase { - _$DriftChatDatabase(QueryExecutor e) : super(e); - $DriftChatDatabaseManager get managers => $DriftChatDatabaseManager(this); - late final $ChannelsTable channels = $ChannelsTable(this); - late final $MessagesTable messages = $MessagesTable(this); - late final $DraftMessagesTable draftMessages = $DraftMessagesTable(this); - late final $PinnedMessagesTable pinnedMessages = $PinnedMessagesTable(this); - late final $PollsTable polls = $PollsTable(this); - late final $PollVotesTable pollVotes = $PollVotesTable(this); - late final $PinnedMessageReactionsTable pinnedMessageReactions = - $PinnedMessageReactionsTable(this); - late final $ReactionsTable reactions = $ReactionsTable(this); - late final $UsersTable users = $UsersTable(this); - late final $MembersTable members = $MembersTable(this); - late final $ReadsTable reads = $ReadsTable(this); - late final $ChannelQueriesTable channelQueries = $ChannelQueriesTable(this); - late final $ConnectionEventsTable connectionEvents = - $ConnectionEventsTable(this); - late final UserDao userDao = UserDao(this as DriftChatDatabase); - late final ChannelDao channelDao = ChannelDao(this as DriftChatDatabase); - late final MessageDao messageDao = MessageDao(this as DriftChatDatabase); - late final DraftMessageDao draftMessageDao = - DraftMessageDao(this as DriftChatDatabase); - late final PinnedMessageDao pinnedMessageDao = - PinnedMessageDao(this as DriftChatDatabase); - late final PinnedMessageReactionDao pinnedMessageReactionDao = - PinnedMessageReactionDao(this as DriftChatDatabase); - late final MemberDao memberDao = MemberDao(this as DriftChatDatabase); - late final PollDao pollDao = PollDao(this as DriftChatDatabase); - late final PollVoteDao pollVoteDao = PollVoteDao(this as DriftChatDatabase); - late final ReactionDao reactionDao = ReactionDao(this as DriftChatDatabase); - late final ReadDao readDao = ReadDao(this as DriftChatDatabase); - late final ChannelQueryDao channelQueryDao = - ChannelQueryDao(this as DriftChatDatabase); - late final ConnectionEventDao connectionEventDao = - ConnectionEventDao(this as DriftChatDatabase); - @override - Iterable> get allTables => - allSchemaEntities.whereType>(); - @override - List get allSchemaEntities => [ - channels, - messages, - draftMessages, - pinnedMessages, - polls, - pollVotes, - pinnedMessageReactions, - reactions, - users, - members, - reads, - channelQueries, - connectionEvents - ]; - @override - StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( - [ - WritePropagation( - on: TableUpdateQuery.onTableName('channels', - limitUpdateKind: UpdateKind.delete), - result: [ - TableUpdate('messages', kind: UpdateKind.delete), - ], - ), - WritePropagation( - on: TableUpdateQuery.onTableName('messages', - limitUpdateKind: UpdateKind.delete), - result: [ - TableUpdate('draft_messages', kind: UpdateKind.delete), - ], - ), - WritePropagation( - on: TableUpdateQuery.onTableName('channels', - limitUpdateKind: UpdateKind.delete), - result: [ - TableUpdate('draft_messages', kind: UpdateKind.delete), - ], - ), - WritePropagation( - on: TableUpdateQuery.onTableName('polls', - limitUpdateKind: UpdateKind.delete), - result: [ - TableUpdate('poll_votes', kind: UpdateKind.delete), - ], - ), - WritePropagation( - on: TableUpdateQuery.onTableName('pinned_messages', - limitUpdateKind: UpdateKind.delete), - result: [ - TableUpdate('pinned_message_reactions', kind: UpdateKind.delete), - ], - ), - WritePropagation( - on: TableUpdateQuery.onTableName('messages', - limitUpdateKind: UpdateKind.delete), - result: [ - TableUpdate('reactions', kind: UpdateKind.delete), - ], - ), - WritePropagation( - on: TableUpdateQuery.onTableName('channels', - limitUpdateKind: UpdateKind.delete), - result: [ - TableUpdate('members', kind: UpdateKind.delete), - ], +class $$ChannelsTableTableManager extends RootTableManager< + _$DriftChatDatabase, + $ChannelsTable, + ChannelEntity, + $$ChannelsTableFilterComposer, + $$ChannelsTableOrderingComposer, + $$ChannelsTableAnnotationComposer, + $$ChannelsTableCreateCompanionBuilder, + $$ChannelsTableUpdateCompanionBuilder, + (ChannelEntity, $$ChannelsTableReferences), + ChannelEntity, + PrefetchHooks Function( + {bool messagesRefs, + bool draftMessagesRefs, + bool locationsRefs, + bool membersRefs, + bool readsRefs})> { + $$ChannelsTableTableManager(_$DriftChatDatabase db, $ChannelsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$ChannelsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$ChannelsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$ChannelsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value type = const Value.absent(), + Value cid = const Value.absent(), + Value?> ownCapabilities = const Value.absent(), + Value> config = const Value.absent(), + Value frozen = const Value.absent(), + Value lastMessageAt = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value memberCount = const Value.absent(), + Value createdById = const Value.absent(), + Value?> extraData = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ChannelsCompanion( + id: id, + type: type, + cid: cid, + ownCapabilities: ownCapabilities, + config: config, + frozen: frozen, + lastMessageAt: lastMessageAt, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + memberCount: memberCount, + createdById: createdById, + extraData: extraData, + rowid: rowid, ), - WritePropagation( - on: TableUpdateQuery.onTableName('channels', - limitUpdateKind: UpdateKind.delete), - result: [ - TableUpdate('reads', kind: UpdateKind.delete), - ], + createCompanionCallback: ({ + required String id, + required String type, + required String cid, + Value?> ownCapabilities = const Value.absent(), + required Map config, + Value frozen = const Value.absent(), + Value lastMessageAt = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value deletedAt = const Value.absent(), + Value memberCount = const Value.absent(), + Value createdById = const Value.absent(), + Value?> extraData = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ChannelsCompanion.insert( + id: id, + type: type, + cid: cid, + ownCapabilities: ownCapabilities, + config: config, + frozen: frozen, + lastMessageAt: lastMessageAt, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + memberCount: memberCount, + createdById: createdById, + extraData: extraData, + rowid: rowid, ), - ], - ); + withReferenceMapper: (p0) => p0 + .map((e) => + (e.readTable(table), $$ChannelsTableReferences(db, table, e))) + .toList(), + prefetchHooksCallback: ( + {messagesRefs = false, + draftMessagesRefs = false, + locationsRefs = false, + membersRefs = false, + readsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (messagesRefs) db.messages, + if (draftMessagesRefs) db.draftMessages, + if (locationsRefs) db.locations, + if (membersRefs) db.members, + if (readsRefs) db.reads + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (messagesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$ChannelsTableReferences._messagesRefsTable(db), + managerFromTypedResult: (p0) => + $$ChannelsTableReferences(db, table, p0) + .messagesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.channelCid == item.cid), + typedResults: items), + if (draftMessagesRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$ChannelsTableReferences + ._draftMessagesRefsTable(db), + managerFromTypedResult: (p0) => + $$ChannelsTableReferences(db, table, p0) + .draftMessagesRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.channelCid == item.cid), + typedResults: items), + if (locationsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$ChannelsTableReferences._locationsRefsTable(db), + managerFromTypedResult: (p0) => + $$ChannelsTableReferences(db, table, p0) + .locationsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.channelCid == item.cid), + typedResults: items), + if (membersRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$ChannelsTableReferences._membersRefsTable(db), + managerFromTypedResult: (p0) => + $$ChannelsTableReferences(db, table, p0) + .membersRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.channelCid == item.cid), + typedResults: items), + if (readsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: + $$ChannelsTableReferences._readsRefsTable(db), + managerFromTypedResult: (p0) => + $$ChannelsTableReferences(db, table, p0).readsRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems + .where((e) => e.channelCid == item.cid), + typedResults: items) + ]; + }, + ); + }, + )); } -typedef $$ChannelsTableCreateCompanionBuilder = ChannelsCompanion Function({ +typedef $$ChannelsTableProcessedTableManager = ProcessedTableManager< + _$DriftChatDatabase, + $ChannelsTable, + ChannelEntity, + $$ChannelsTableFilterComposer, + $$ChannelsTableOrderingComposer, + $$ChannelsTableAnnotationComposer, + $$ChannelsTableCreateCompanionBuilder, + $$ChannelsTableUpdateCompanionBuilder, + (ChannelEntity, $$ChannelsTableReferences), + ChannelEntity, + PrefetchHooks Function( + {bool messagesRefs, + bool draftMessagesRefs, + bool locationsRefs, + bool membersRefs, + bool readsRefs})>; +typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ required String id, - required String type, - required String cid, - Value?> ownCapabilities, - required Map config, - Value frozen, - Value lastMessageAt, - Value createdAt, - Value updatedAt, - Value deletedAt, - Value memberCount, - Value createdById, + Value messageText, + required List attachments, + required String state, + Value type, + required List mentionedUsers, + Value?> reactionGroups, + Value parentId, + Value quotedMessageId, + Value pollId, + Value replyCount, + Value showInChannel, + Value shadowed, + Value command, + Value localCreatedAt, + Value remoteCreatedAt, + Value localUpdatedAt, + Value remoteUpdatedAt, + Value localDeletedAt, + Value remoteDeletedAt, + Value messageTextUpdatedAt, + Value userId, + Value pinned, + Value pinnedAt, + Value pinExpires, + Value pinnedByUserId, + required String channelCid, + Value?> i18n, + Value?> restrictedVisibility, Value?> extraData, Value rowid, }); -typedef $$ChannelsTableUpdateCompanionBuilder = ChannelsCompanion Function({ +typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value id, + Value messageText, + Value> attachments, + Value state, Value type, - Value cid, - Value?> ownCapabilities, - Value> config, - Value frozen, - Value lastMessageAt, - Value createdAt, - Value updatedAt, - Value deletedAt, - Value memberCount, - Value createdById, + Value> mentionedUsers, + Value?> reactionGroups, + Value parentId, + Value quotedMessageId, + Value pollId, + Value replyCount, + Value showInChannel, + Value shadowed, + Value command, + Value localCreatedAt, + Value remoteCreatedAt, + Value localUpdatedAt, + Value remoteUpdatedAt, + Value localDeletedAt, + Value remoteDeletedAt, + Value messageTextUpdatedAt, + Value userId, + Value pinned, + Value pinnedAt, + Value pinExpires, + Value pinnedByUserId, + Value channelCid, + Value?> i18n, + Value?> restrictedVisibility, Value?> extraData, Value rowid, }); -final class $$ChannelsTableReferences - extends BaseReferences<_$DriftChatDatabase, $ChannelsTable, ChannelEntity> { - $$ChannelsTableReferences(super.$_db, super.$_table, super.$_typedResult); +final class $$MessagesTableReferences + extends BaseReferences<_$DriftChatDatabase, $MessagesTable, MessageEntity> { + $$MessagesTableReferences(super.$_db, super.$_table, super.$_typedResult); - static MultiTypedResultKey<$MessagesTable, List> - _messagesRefsTable(_$DriftChatDatabase db) => - MultiTypedResultKey.fromTable(db.messages, - aliasName: $_aliasNameGenerator( - db.channels.cid, db.messages.channelCid)); + static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => + db.channels.createAlias( + $_aliasNameGenerator(db.messages.channelCid, db.channels.cid)); - $$MessagesTableProcessedTableManager get messagesRefs { - final manager = $$MessagesTableTableManager($_db, $_db.messages).filter( - (f) => f.channelCid.cid.sqlEquals($_itemColumn('cid')!)); + $$ChannelsTableProcessedTableManager get channelCid { + final $_column = $_itemColumn('channel_cid')!; - final cache = $_typedResult.readTableOrNull(_messagesRefsTable($_db)); + final manager = $$ChannelsTableTableManager($_db, $_db.channels) + .filter((f) => f.cid.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_channelCidTable($_db)); + if (item == null) return manager; return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); + manager.$state.copyWith(prefetchedData: [item])); } static MultiTypedResultKey<$DraftMessagesTable, List> _draftMessagesRefsTable(_$DriftChatDatabase db) => MultiTypedResultKey.fromTable(db.draftMessages, aliasName: $_aliasNameGenerator( - db.channels.cid, db.draftMessages.channelCid)); + db.messages.id, db.draftMessages.parentId)); $$DraftMessagesTableProcessedTableManager get draftMessagesRefs { final manager = $$DraftMessagesTableTableManager($_db, $_db.draftMessages) - .filter( - (f) => f.channelCid.cid.sqlEquals($_itemColumn('cid')!)); + .filter((f) => f.parentId.id.sqlEquals($_itemColumn('id')!)); final cache = $_typedResult.readTableOrNull(_draftMessagesRefsTable($_db)); return ProcessedTableManager( manager.$state.copyWith(prefetchedData: cache)); } - static MultiTypedResultKey<$MembersTable, List> - _membersRefsTable(_$DriftChatDatabase db) => - MultiTypedResultKey.fromTable(db.members, + static MultiTypedResultKey<$LocationsTable, List> + _locationsRefsTable(_$DriftChatDatabase db) => + MultiTypedResultKey.fromTable(db.locations, aliasName: - $_aliasNameGenerator(db.channels.cid, db.members.channelCid)); + $_aliasNameGenerator(db.messages.id, db.locations.messageId)); - $$MembersTableProcessedTableManager get membersRefs { - final manager = $$MembersTableTableManager($_db, $_db.members).filter( - (f) => f.channelCid.cid.sqlEquals($_itemColumn('cid')!)); + $$LocationsTableProcessedTableManager get locationsRefs { + final manager = $$LocationsTableTableManager($_db, $_db.locations) + .filter((f) => f.messageId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_locationsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$ReactionsTable, List> + _reactionsRefsTable(_$DriftChatDatabase db) => + MultiTypedResultKey.fromTable(db.reactions, + aliasName: + $_aliasNameGenerator(db.messages.id, db.reactions.messageId)); + + $$ReactionsTableProcessedTableManager get reactionsRefs { + final manager = $$ReactionsTableTableManager($_db, $_db.reactions) + .filter((f) => f.messageId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull(_reactionsRefsTable($_db)); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache)); + } +} + +class $$MessagesTableFilterComposer + extends Composer<_$DriftChatDatabase, $MessagesTable> { + $$MessagesTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get messageText => $composableBuilder( + column: $table.messageText, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters, List, String> + get attachments => $composableBuilder( + column: $table.attachments, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get state => $composableBuilder( + column: $table.state, builder: (column) => ColumnFilters(column)); + + ColumnFilters get type => $composableBuilder( + column: $table.type, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters, List, String> + get mentionedUsers => $composableBuilder( + column: $table.mentionedUsers, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters?, + Map, String> + get reactionGroups => $composableBuilder( + column: $table.reactionGroups, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnFilters get parentId => $composableBuilder( + column: $table.parentId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get quotedMessageId => $composableBuilder( + column: $table.quotedMessageId, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get pollId => $composableBuilder( + column: $table.pollId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get replyCount => $composableBuilder( + column: $table.replyCount, builder: (column) => ColumnFilters(column)); - final cache = $_typedResult.readTableOrNull(_membersRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); - } + ColumnFilters get showInChannel => $composableBuilder( + column: $table.showInChannel, builder: (column) => ColumnFilters(column)); - static MultiTypedResultKey<$ReadsTable, List> _readsRefsTable( - _$DriftChatDatabase db) => - MultiTypedResultKey.fromTable(db.reads, - aliasName: - $_aliasNameGenerator(db.channels.cid, db.reads.channelCid)); + ColumnFilters get shadowed => $composableBuilder( + column: $table.shadowed, builder: (column) => ColumnFilters(column)); - $$ReadsTableProcessedTableManager get readsRefs { - final manager = $$ReadsTableTableManager($_db, $_db.reads).filter( - (f) => f.channelCid.cid.sqlEquals($_itemColumn('cid')!)); + ColumnFilters get command => $composableBuilder( + column: $table.command, builder: (column) => ColumnFilters(column)); - final cache = $_typedResult.readTableOrNull(_readsRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); - } -} + ColumnFilters get localCreatedAt => $composableBuilder( + column: $table.localCreatedAt, + builder: (column) => ColumnFilters(column)); -class $$ChannelsTableFilterComposer - extends Composer<_$DriftChatDatabase, $ChannelsTable> { - $$ChannelsTableFilterComposer({ - required super.$db, - required super.$table, - super.joinBuilder, - super.$addJoinBuilderToRootComposer, - super.$removeJoinBuilderFromRootComposer, - }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get remoteCreatedAt => $composableBuilder( + column: $table.remoteCreatedAt, + builder: (column) => ColumnFilters(column)); - ColumnFilters get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnFilters(column)); + ColumnFilters get localUpdatedAt => $composableBuilder( + column: $table.localUpdatedAt, + builder: (column) => ColumnFilters(column)); - ColumnFilters get cid => $composableBuilder( - column: $table.cid, builder: (column) => ColumnFilters(column)); + ColumnFilters get remoteUpdatedAt => $composableBuilder( + column: $table.remoteUpdatedAt, + builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, List, String> - get ownCapabilities => $composableBuilder( - column: $table.ownCapabilities, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get localDeletedAt => $composableBuilder( + column: $table.localDeletedAt, + builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters, Map, - String> - get config => $composableBuilder( - column: $table.config, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get remoteDeletedAt => $composableBuilder( + column: $table.remoteDeletedAt, + builder: (column) => ColumnFilters(column)); - ColumnFilters get frozen => $composableBuilder( - column: $table.frozen, builder: (column) => ColumnFilters(column)); + ColumnFilters get messageTextUpdatedAt => $composableBuilder( + column: $table.messageTextUpdatedAt, + builder: (column) => ColumnFilters(column)); - ColumnFilters get lastMessageAt => $composableBuilder( - column: $table.lastMessageAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnFilters(column)); - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get pinned => $composableBuilder( + column: $table.pinned, builder: (column) => ColumnFilters(column)); - ColumnFilters get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get pinnedAt => $composableBuilder( + column: $table.pinnedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get deletedAt => $composableBuilder( - column: $table.deletedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get pinExpires => $composableBuilder( + column: $table.pinExpires, builder: (column) => ColumnFilters(column)); - ColumnFilters get memberCount => $composableBuilder( - column: $table.memberCount, builder: (column) => ColumnFilters(column)); + ColumnFilters get pinnedByUserId => $composableBuilder( + column: $table.pinnedByUserId, + builder: (column) => ColumnFilters(column)); - ColumnFilters get createdById => $composableBuilder( - column: $table.createdById, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters?, Map, + String> + get i18n => $composableBuilder( + column: $table.i18n, + builder: (column) => ColumnWithTypeConverterFilters(column)); + + ColumnWithTypeConverterFilters?, List, String> + get restrictedVisibility => $composableBuilder( + column: $table.restrictedVisibility, + builder: (column) => ColumnWithTypeConverterFilters(column)); ColumnWithTypeConverterFilters?, Map, String> @@ -8578,34 +10016,33 @@ class $$ChannelsTableFilterComposer column: $table.extraData, builder: (column) => ColumnWithTypeConverterFilters(column)); - Expression messagesRefs( - Expression Function($$MessagesTableFilterComposer f) f) { - final $$MessagesTableFilterComposer composer = $composerBuilder( + $$ChannelsTableFilterComposer get channelCid { + final $$ChannelsTableFilterComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.cid, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.channelCid, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$MessagesTableFilterComposer( + $$ChannelsTableFilterComposer( $db: $db, - $table: $db.messages, + $table: $db.channels, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, )); - return f(composer); + return composer; } Expression draftMessagesRefs( Expression Function($$DraftMessagesTableFilterComposer f) f) { final $$DraftMessagesTableFilterComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.cid, + getCurrentColumn: (t) => t.id, referencedTable: $db.draftMessages, - getReferencedColumn: (t) => t.channelCid, + getReferencedColumn: (t) => t.parentId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => @@ -8620,19 +10057,19 @@ class $$ChannelsTableFilterComposer return f(composer); } - Expression membersRefs( - Expression Function($$MembersTableFilterComposer f) f) { - final $$MembersTableFilterComposer composer = $composerBuilder( + Expression locationsRefs( + Expression Function($$LocationsTableFilterComposer f) f) { + final $$LocationsTableFilterComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.cid, - referencedTable: $db.members, - getReferencedColumn: (t) => t.channelCid, + getCurrentColumn: (t) => t.id, + referencedTable: $db.locations, + getReferencedColumn: (t) => t.messageId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$MembersTableFilterComposer( + $$LocationsTableFilterComposer( $db: $db, - $table: $db.members, + $table: $db.locations, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -8641,19 +10078,19 @@ class $$ChannelsTableFilterComposer return f(composer); } - Expression readsRefs( - Expression Function($$ReadsTableFilterComposer f) f) { - final $$ReadsTableFilterComposer composer = $composerBuilder( + Expression reactionsRefs( + Expression Function($$ReactionsTableFilterComposer f) f) { + final $$ReactionsTableFilterComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.cid, - referencedTable: $db.reads, - getReferencedColumn: (t) => t.channelCid, + getCurrentColumn: (t) => t.id, + referencedTable: $db.reactions, + getReferencedColumn: (t) => t.messageId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$ReadsTableFilterComposer( + $$ReactionsTableFilterComposer( $db: $db, - $table: $db.reads, + $table: $db.reactions, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -8663,9 +10100,9 @@ class $$ChannelsTableFilterComposer } } -class $$ChannelsTableOrderingComposer - extends Composer<_$DriftChatDatabase, $ChannelsTable> { - $$ChannelsTableOrderingComposer({ +class $$MessagesTableOrderingComposer + extends Composer<_$DriftChatDatabase, $MessagesTable> { + $$MessagesTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -8675,48 +10112,127 @@ class $$ChannelsTableOrderingComposer ColumnOrderings get id => $composableBuilder( column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get messageText => $composableBuilder( + column: $table.messageText, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get attachments => $composableBuilder( + column: $table.attachments, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get state => $composableBuilder( + column: $table.state, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get type => $composableBuilder( column: $table.type, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get cid => $composableBuilder( - column: $table.cid, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get mentionedUsers => $composableBuilder( + column: $table.mentionedUsers, + builder: (column) => ColumnOrderings(column)); - ColumnOrderings get ownCapabilities => $composableBuilder( - column: $table.ownCapabilities, + ColumnOrderings get reactionGroups => $composableBuilder( + column: $table.reactionGroups, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get config => $composableBuilder( - column: $table.config, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get parentId => $composableBuilder( + column: $table.parentId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get frozen => $composableBuilder( - column: $table.frozen, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get quotedMessageId => $composableBuilder( + column: $table.quotedMessageId, + builder: (column) => ColumnOrderings(column)); - ColumnOrderings get lastMessageAt => $composableBuilder( - column: $table.lastMessageAt, + ColumnOrderings get pollId => $composableBuilder( + column: $table.pollId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get replyCount => $composableBuilder( + column: $table.replyCount, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get showInChannel => $composableBuilder( + column: $table.showInChannel, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get shadowed => $composableBuilder( + column: $table.shadowed, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get command => $composableBuilder( + column: $table.command, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get deletedAt => $composableBuilder( - column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get localCreatedAt => $composableBuilder( + column: $table.localCreatedAt, + builder: (column) => ColumnOrderings(column)); - ColumnOrderings get memberCount => $composableBuilder( - column: $table.memberCount, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get remoteCreatedAt => $composableBuilder( + column: $table.remoteCreatedAt, + builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdById => $composableBuilder( - column: $table.createdById, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get localUpdatedAt => $composableBuilder( + column: $table.localUpdatedAt, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get remoteUpdatedAt => $composableBuilder( + column: $table.remoteUpdatedAt, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get localDeletedAt => $composableBuilder( + column: $table.localDeletedAt, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get remoteDeletedAt => $composableBuilder( + column: $table.remoteDeletedAt, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get messageTextUpdatedAt => $composableBuilder( + column: $table.messageTextUpdatedAt, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get pinned => $composableBuilder( + column: $table.pinned, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get pinnedAt => $composableBuilder( + column: $table.pinnedAt, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get pinExpires => $composableBuilder( + column: $table.pinExpires, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get pinnedByUserId => $composableBuilder( + column: $table.pinnedByUserId, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get i18n => $composableBuilder( + column: $table.i18n, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get restrictedVisibility => $composableBuilder( + column: $table.restrictedVisibility, + builder: (column) => ColumnOrderings(column)); ColumnOrderings get extraData => $composableBuilder( column: $table.extraData, builder: (column) => ColumnOrderings(column)); + + $$ChannelsTableOrderingComposer get channelCid { + final $$ChannelsTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$ChannelsTableOrderingComposer( + $db: $db, + $table: $db.channels, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } } -class $$ChannelsTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $ChannelsTable> { - $$ChannelsTableAnnotationComposer({ +class $$MessagesTableAnnotationComposer + extends Composer<_$DriftChatDatabase, $MessagesTable> { + $$MessagesTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -8726,72 +10242,122 @@ class $$ChannelsTableAnnotationComposer GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get messageText => $composableBuilder( + column: $table.messageText, builder: (column) => column); + + GeneratedColumnWithTypeConverter, String> get attachments => + $composableBuilder( + column: $table.attachments, builder: (column) => column); + + GeneratedColumn get state => + $composableBuilder(column: $table.state, builder: (column) => column); + GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); - GeneratedColumn get cid => - $composableBuilder(column: $table.cid, builder: (column) => column); - - GeneratedColumnWithTypeConverter?, String> get ownCapabilities => + GeneratedColumnWithTypeConverter, String> get mentionedUsers => $composableBuilder( - column: $table.ownCapabilities, builder: (column) => column); + column: $table.mentionedUsers, builder: (column) => column); - GeneratedColumnWithTypeConverter, String> get config => - $composableBuilder(column: $table.config, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> + get reactionGroups => $composableBuilder( + column: $table.reactionGroups, builder: (column) => column); - GeneratedColumn get frozen => - $composableBuilder(column: $table.frozen, builder: (column) => column); + GeneratedColumn get parentId => + $composableBuilder(column: $table.parentId, builder: (column) => column); - GeneratedColumn get lastMessageAt => $composableBuilder( - column: $table.lastMessageAt, builder: (column) => column); + GeneratedColumn get quotedMessageId => $composableBuilder( + column: $table.quotedMessageId, builder: (column) => column); - GeneratedColumn get createdAt => - $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get pollId => + $composableBuilder(column: $table.pollId, builder: (column) => column); - GeneratedColumn get updatedAt => - $composableBuilder(column: $table.updatedAt, builder: (column) => column); + GeneratedColumn get replyCount => $composableBuilder( + column: $table.replyCount, builder: (column) => column); - GeneratedColumn get deletedAt => - $composableBuilder(column: $table.deletedAt, builder: (column) => column); + GeneratedColumn get showInChannel => $composableBuilder( + column: $table.showInChannel, builder: (column) => column); - GeneratedColumn get memberCount => $composableBuilder( - column: $table.memberCount, builder: (column) => column); + GeneratedColumn get shadowed => + $composableBuilder(column: $table.shadowed, builder: (column) => column); - GeneratedColumn get createdById => $composableBuilder( - column: $table.createdById, builder: (column) => column); + GeneratedColumn get command => + $composableBuilder(column: $table.command, builder: (column) => column); + + GeneratedColumn get localCreatedAt => $composableBuilder( + column: $table.localCreatedAt, builder: (column) => column); + + GeneratedColumn get remoteCreatedAt => $composableBuilder( + column: $table.remoteCreatedAt, builder: (column) => column); + + GeneratedColumn get localUpdatedAt => $composableBuilder( + column: $table.localUpdatedAt, builder: (column) => column); + + GeneratedColumn get remoteUpdatedAt => $composableBuilder( + column: $table.remoteUpdatedAt, builder: (column) => column); + + GeneratedColumn get localDeletedAt => $composableBuilder( + column: $table.localDeletedAt, builder: (column) => column); + + GeneratedColumn get remoteDeletedAt => $composableBuilder( + column: $table.remoteDeletedAt, builder: (column) => column); + + GeneratedColumn get messageTextUpdatedAt => $composableBuilder( + column: $table.messageTextUpdatedAt, builder: (column) => column); + + GeneratedColumn get userId => + $composableBuilder(column: $table.userId, builder: (column) => column); + + GeneratedColumn get pinned => + $composableBuilder(column: $table.pinned, builder: (column) => column); + + GeneratedColumn get pinnedAt => + $composableBuilder(column: $table.pinnedAt, builder: (column) => column); + + GeneratedColumn get pinExpires => $composableBuilder( + column: $table.pinExpires, builder: (column) => column); + + GeneratedColumn get pinnedByUserId => $composableBuilder( + column: $table.pinnedByUserId, builder: (column) => column); + + GeneratedColumnWithTypeConverter?, String> get i18n => + $composableBuilder(column: $table.i18n, builder: (column) => column); + + GeneratedColumnWithTypeConverter?, String> + get restrictedVisibility => $composableBuilder( + column: $table.restrictedVisibility, builder: (column) => column); GeneratedColumnWithTypeConverter?, String> get extraData => $composableBuilder( column: $table.extraData, builder: (column) => column); - Expression messagesRefs( - Expression Function($$MessagesTableAnnotationComposer a) f) { - final $$MessagesTableAnnotationComposer composer = $composerBuilder( + $$ChannelsTableAnnotationComposer get channelCid { + final $$ChannelsTableAnnotationComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.cid, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.channelCid, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$MessagesTableAnnotationComposer( + $$ChannelsTableAnnotationComposer( $db: $db, - $table: $db.messages, + $table: $db.channels, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, )); - return f(composer); + return composer; } Expression draftMessagesRefs( Expression Function($$DraftMessagesTableAnnotationComposer a) f) { final $$DraftMessagesTableAnnotationComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.cid, + getCurrentColumn: (t) => t.id, referencedTable: $db.draftMessages, - getReferencedColumn: (t) => t.channelCid, + getReferencedColumn: (t) => t.parentId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => @@ -8806,19 +10372,19 @@ class $$ChannelsTableAnnotationComposer return f(composer); } - Expression membersRefs( - Expression Function($$MembersTableAnnotationComposer a) f) { - final $$MembersTableAnnotationComposer composer = $composerBuilder( + Expression locationsRefs( + Expression Function($$LocationsTableAnnotationComposer a) f) { + final $$LocationsTableAnnotationComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.cid, - referencedTable: $db.members, - getReferencedColumn: (t) => t.channelCid, + getCurrentColumn: (t) => t.id, + referencedTable: $db.locations, + getReferencedColumn: (t) => t.messageId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$MembersTableAnnotationComposer( + $$LocationsTableAnnotationComposer( $db: $db, - $table: $db.members, + $table: $db.locations, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -8827,19 +10393,19 @@ class $$ChannelsTableAnnotationComposer return f(composer); } - Expression readsRefs( - Expression Function($$ReadsTableAnnotationComposer a) f) { - final $$ReadsTableAnnotationComposer composer = $composerBuilder( + Expression reactionsRefs( + Expression Function($$ReactionsTableAnnotationComposer a) f) { + final $$ReactionsTableAnnotationComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.cid, - referencedTable: $db.reads, - getReferencedColumn: (t) => t.channelCid, + getCurrentColumn: (t) => t.id, + referencedTable: $db.reactions, + getReferencedColumn: (t) => t.messageId, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$ReadsTableAnnotationComposer( + $$ReactionsTableAnnotationComposer( $db: $db, - $table: $db.reads, + $table: $db.reactions, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -8849,166 +10415,248 @@ class $$ChannelsTableAnnotationComposer } } -class $$ChannelsTableTableManager extends RootTableManager< +class $$MessagesTableTableManager extends RootTableManager< _$DriftChatDatabase, - $ChannelsTable, - ChannelEntity, - $$ChannelsTableFilterComposer, - $$ChannelsTableOrderingComposer, - $$ChannelsTableAnnotationComposer, - $$ChannelsTableCreateCompanionBuilder, - $$ChannelsTableUpdateCompanionBuilder, - (ChannelEntity, $$ChannelsTableReferences), - ChannelEntity, + $MessagesTable, + MessageEntity, + $$MessagesTableFilterComposer, + $$MessagesTableOrderingComposer, + $$MessagesTableAnnotationComposer, + $$MessagesTableCreateCompanionBuilder, + $$MessagesTableUpdateCompanionBuilder, + (MessageEntity, $$MessagesTableReferences), + MessageEntity, PrefetchHooks Function( - {bool messagesRefs, + {bool channelCid, bool draftMessagesRefs, - bool membersRefs, - bool readsRefs})> { - $$ChannelsTableTableManager(_$DriftChatDatabase db, $ChannelsTable table) + bool locationsRefs, + bool reactionsRefs})> { + $$MessagesTableTableManager(_$DriftChatDatabase db, $MessagesTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$ChannelsTableFilterComposer($db: db, $table: table), + $$MessagesTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$ChannelsTableOrderingComposer($db: db, $table: table), + $$MessagesTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$ChannelsTableAnnotationComposer($db: db, $table: table), + $$MessagesTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), + Value messageText = const Value.absent(), + Value> attachments = const Value.absent(), + Value state = const Value.absent(), Value type = const Value.absent(), - Value cid = const Value.absent(), - Value?> ownCapabilities = const Value.absent(), - Value> config = const Value.absent(), - Value frozen = const Value.absent(), - Value lastMessageAt = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value deletedAt = const Value.absent(), - Value memberCount = const Value.absent(), - Value createdById = const Value.absent(), + Value> mentionedUsers = const Value.absent(), + Value?> reactionGroups = + const Value.absent(), + Value parentId = const Value.absent(), + Value quotedMessageId = const Value.absent(), + Value pollId = const Value.absent(), + Value replyCount = const Value.absent(), + Value showInChannel = const Value.absent(), + Value shadowed = const Value.absent(), + Value command = const Value.absent(), + Value localCreatedAt = const Value.absent(), + Value remoteCreatedAt = const Value.absent(), + Value localUpdatedAt = const Value.absent(), + Value remoteUpdatedAt = const Value.absent(), + Value localDeletedAt = const Value.absent(), + Value remoteDeletedAt = const Value.absent(), + Value messageTextUpdatedAt = const Value.absent(), + Value userId = const Value.absent(), + Value pinned = const Value.absent(), + Value pinnedAt = const Value.absent(), + Value pinExpires = const Value.absent(), + Value pinnedByUserId = const Value.absent(), + Value channelCid = const Value.absent(), + Value?> i18n = const Value.absent(), + Value?> restrictedVisibility = const Value.absent(), Value?> extraData = const Value.absent(), Value rowid = const Value.absent(), }) => - ChannelsCompanion( + MessagesCompanion( id: id, + messageText: messageText, + attachments: attachments, + state: state, type: type, - cid: cid, - ownCapabilities: ownCapabilities, - config: config, - frozen: frozen, - lastMessageAt: lastMessageAt, - createdAt: createdAt, - updatedAt: updatedAt, - deletedAt: deletedAt, - memberCount: memberCount, - createdById: createdById, + mentionedUsers: mentionedUsers, + reactionGroups: reactionGroups, + parentId: parentId, + quotedMessageId: quotedMessageId, + pollId: pollId, + replyCount: replyCount, + showInChannel: showInChannel, + shadowed: shadowed, + command: command, + localCreatedAt: localCreatedAt, + remoteCreatedAt: remoteCreatedAt, + localUpdatedAt: localUpdatedAt, + remoteUpdatedAt: remoteUpdatedAt, + localDeletedAt: localDeletedAt, + remoteDeletedAt: remoteDeletedAt, + messageTextUpdatedAt: messageTextUpdatedAt, + userId: userId, + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedByUserId: pinnedByUserId, + channelCid: channelCid, + i18n: i18n, + restrictedVisibility: restrictedVisibility, extraData: extraData, rowid: rowid, ), createCompanionCallback: ({ required String id, - required String type, - required String cid, - Value?> ownCapabilities = const Value.absent(), - required Map config, - Value frozen = const Value.absent(), - Value lastMessageAt = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value deletedAt = const Value.absent(), - Value memberCount = const Value.absent(), - Value createdById = const Value.absent(), + Value messageText = const Value.absent(), + required List attachments, + required String state, + Value type = const Value.absent(), + required List mentionedUsers, + Value?> reactionGroups = + const Value.absent(), + Value parentId = const Value.absent(), + Value quotedMessageId = const Value.absent(), + Value pollId = const Value.absent(), + Value replyCount = const Value.absent(), + Value showInChannel = const Value.absent(), + Value shadowed = const Value.absent(), + Value command = const Value.absent(), + Value localCreatedAt = const Value.absent(), + Value remoteCreatedAt = const Value.absent(), + Value localUpdatedAt = const Value.absent(), + Value remoteUpdatedAt = const Value.absent(), + Value localDeletedAt = const Value.absent(), + Value remoteDeletedAt = const Value.absent(), + Value messageTextUpdatedAt = const Value.absent(), + Value userId = const Value.absent(), + Value pinned = const Value.absent(), + Value pinnedAt = const Value.absent(), + Value pinExpires = const Value.absent(), + Value pinnedByUserId = const Value.absent(), + required String channelCid, + Value?> i18n = const Value.absent(), + Value?> restrictedVisibility = const Value.absent(), Value?> extraData = const Value.absent(), Value rowid = const Value.absent(), }) => - ChannelsCompanion.insert( + MessagesCompanion.insert( id: id, + messageText: messageText, + attachments: attachments, + state: state, type: type, - cid: cid, - ownCapabilities: ownCapabilities, - config: config, - frozen: frozen, - lastMessageAt: lastMessageAt, - createdAt: createdAt, - updatedAt: updatedAt, - deletedAt: deletedAt, - memberCount: memberCount, - createdById: createdById, + mentionedUsers: mentionedUsers, + reactionGroups: reactionGroups, + parentId: parentId, + quotedMessageId: quotedMessageId, + pollId: pollId, + replyCount: replyCount, + showInChannel: showInChannel, + shadowed: shadowed, + command: command, + localCreatedAt: localCreatedAt, + remoteCreatedAt: remoteCreatedAt, + localUpdatedAt: localUpdatedAt, + remoteUpdatedAt: remoteUpdatedAt, + localDeletedAt: localDeletedAt, + remoteDeletedAt: remoteDeletedAt, + messageTextUpdatedAt: messageTextUpdatedAt, + userId: userId, + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedByUserId: pinnedByUserId, + channelCid: channelCid, + i18n: i18n, + restrictedVisibility: restrictedVisibility, extraData: extraData, rowid: rowid, ), withReferenceMapper: (p0) => p0 .map((e) => - (e.readTable(table), $$ChannelsTableReferences(db, table, e))) + (e.readTable(table), $$MessagesTableReferences(db, table, e))) .toList(), prefetchHooksCallback: ( - {messagesRefs = false, + {channelCid = false, draftMessagesRefs = false, - membersRefs = false, - readsRefs = false}) { + locationsRefs = false, + reactionsRefs = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ - if (messagesRefs) db.messages, if (draftMessagesRefs) db.draftMessages, - if (membersRefs) db.members, - if (readsRefs) db.reads + if (locationsRefs) db.locations, + if (reactionsRefs) db.reactions ], - addJoins: null, + addJoins: < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (channelCid) { + state = state.withJoin( + currentTable: table, + currentColumn: table.channelCid, + referencedTable: + $$MessagesTableReferences._channelCidTable(db), + referencedColumn: + $$MessagesTableReferences._channelCidTable(db).cid, + ) as T; + } + + return state; + }, getPrefetchedDataCallback: (items) async { return [ - if (messagesRefs) - await $_getPrefetchedData( - currentTable: table, - referencedTable: - $$ChannelsTableReferences._messagesRefsTable(db), - managerFromTypedResult: (p0) => - $$ChannelsTableReferences(db, table, p0) - .messagesRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems - .where((e) => e.channelCid == item.cid), - typedResults: items), if (draftMessagesRefs) - await $_getPrefetchedData( currentTable: table, - referencedTable: $$ChannelsTableReferences + referencedTable: $$MessagesTableReferences ._draftMessagesRefsTable(db), managerFromTypedResult: (p0) => - $$ChannelsTableReferences(db, table, p0) + $$MessagesTableReferences(db, table, p0) .draftMessagesRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems - .where((e) => e.channelCid == item.cid), + referencedItemsForCurrentItem: (item, + referencedItems) => + referencedItems.where((e) => e.parentId == item.id), typedResults: items), - if (membersRefs) - await $_getPrefetchedData( + if (locationsRefs) + await $_getPrefetchedData( currentTable: table, referencedTable: - $$ChannelsTableReferences._membersRefsTable(db), + $$MessagesTableReferences._locationsRefsTable(db), managerFromTypedResult: (p0) => - $$ChannelsTableReferences(db, table, p0) - .membersRefs, + $$MessagesTableReferences(db, table, p0) + .locationsRefs, referencedItemsForCurrentItem: (item, referencedItems) => referencedItems - .where((e) => e.channelCid == item.cid), + .where((e) => e.messageId == item.id), typedResults: items), - if (readsRefs) - await $_getPrefetchedData( + if (reactionsRefs) + await $_getPrefetchedData( currentTable: table, referencedTable: - $$ChannelsTableReferences._readsRefsTable(db), + $$MessagesTableReferences._reactionsRefsTable(db), managerFromTypedResult: (p0) => - $$ChannelsTableReferences(db, table, p0).readsRefs, + $$MessagesTableReferences(db, table, p0) + .reactionsRefs, referencedItemsForCurrentItem: (item, referencedItems) => referencedItems - .where((e) => e.channelCid == item.cid), + .where((e) => e.messageId == item.id), typedResults: items) ]; }, @@ -9017,96 +10665,82 @@ class $$ChannelsTableTableManager extends RootTableManager< )); } -typedef $$ChannelsTableProcessedTableManager = ProcessedTableManager< +typedef $$MessagesTableProcessedTableManager = ProcessedTableManager< _$DriftChatDatabase, - $ChannelsTable, - ChannelEntity, - $$ChannelsTableFilterComposer, - $$ChannelsTableOrderingComposer, - $$ChannelsTableAnnotationComposer, - $$ChannelsTableCreateCompanionBuilder, - $$ChannelsTableUpdateCompanionBuilder, - (ChannelEntity, $$ChannelsTableReferences), - ChannelEntity, + $MessagesTable, + MessageEntity, + $$MessagesTableFilterComposer, + $$MessagesTableOrderingComposer, + $$MessagesTableAnnotationComposer, + $$MessagesTableCreateCompanionBuilder, + $$MessagesTableUpdateCompanionBuilder, + (MessageEntity, $$MessagesTableReferences), + MessageEntity, PrefetchHooks Function( - {bool messagesRefs, + {bool channelCid, bool draftMessagesRefs, - bool membersRefs, - bool readsRefs})>; -typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ + bool locationsRefs, + bool reactionsRefs})>; +typedef $$DraftMessagesTableCreateCompanionBuilder = DraftMessagesCompanion + Function({ required String id, Value messageText, required List attachments, - required String state, Value type, required List mentionedUsers, - Value?> reactionGroups, Value parentId, Value quotedMessageId, Value pollId, - Value replyCount, Value showInChannel, - Value shadowed, Value command, - Value localCreatedAt, - Value remoteCreatedAt, - Value localUpdatedAt, - Value remoteUpdatedAt, - Value localDeletedAt, - Value remoteDeletedAt, - Value messageTextUpdatedAt, - Value userId, - Value pinned, - Value pinnedAt, - Value pinExpires, - Value pinnedByUserId, + Value silent, + Value createdAt, required String channelCid, - Value?> i18n, - Value?> restrictedVisibility, Value?> extraData, Value rowid, }); -typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ +typedef $$DraftMessagesTableUpdateCompanionBuilder = DraftMessagesCompanion + Function({ Value id, Value messageText, Value> attachments, - Value state, Value type, Value> mentionedUsers, - Value?> reactionGroups, Value parentId, Value quotedMessageId, Value pollId, - Value replyCount, Value showInChannel, - Value shadowed, Value command, - Value localCreatedAt, - Value remoteCreatedAt, - Value localUpdatedAt, - Value remoteUpdatedAt, - Value localDeletedAt, - Value remoteDeletedAt, - Value messageTextUpdatedAt, - Value userId, - Value pinned, - Value pinnedAt, - Value pinExpires, - Value pinnedByUserId, + Value silent, + Value createdAt, Value channelCid, - Value?> i18n, - Value?> restrictedVisibility, Value?> extraData, Value rowid, }); -final class $$MessagesTableReferences - extends BaseReferences<_$DriftChatDatabase, $MessagesTable, MessageEntity> { - $$MessagesTableReferences(super.$_db, super.$_table, super.$_typedResult); +final class $$DraftMessagesTableReferences extends BaseReferences< + _$DriftChatDatabase, $DraftMessagesTable, DraftMessageEntity> { + $$DraftMessagesTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static $MessagesTable _parentIdTable(_$DriftChatDatabase db) => + db.messages.createAlias( + $_aliasNameGenerator(db.draftMessages.parentId, db.messages.id)); + + $$MessagesTableProcessedTableManager? get parentId { + final $_column = $_itemColumn('parent_id'); + if ($_column == null) return null; + final manager = $$MessagesTableTableManager($_db, $_db.messages) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_parentIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => db.channels.createAlias( - $_aliasNameGenerator(db.messages.channelCid, db.channels.cid)); + $_aliasNameGenerator(db.draftMessages.channelCid, db.channels.cid)); $$ChannelsTableProcessedTableManager get channelCid { final $_column = $_itemColumn('channel_cid')!; @@ -9118,41 +10752,11 @@ final class $$MessagesTableReferences return ProcessedTableManager( manager.$state.copyWith(prefetchedData: [item])); } - - static MultiTypedResultKey<$DraftMessagesTable, List> - _draftMessagesRefsTable(_$DriftChatDatabase db) => - MultiTypedResultKey.fromTable(db.draftMessages, - aliasName: $_aliasNameGenerator( - db.messages.id, db.draftMessages.parentId)); - - $$DraftMessagesTableProcessedTableManager get draftMessagesRefs { - final manager = $$DraftMessagesTableTableManager($_db, $_db.draftMessages) - .filter((f) => f.parentId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull(_draftMessagesRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); - } - - static MultiTypedResultKey<$ReactionsTable, List> - _reactionsRefsTable(_$DriftChatDatabase db) => - MultiTypedResultKey.fromTable(db.reactions, - aliasName: - $_aliasNameGenerator(db.messages.id, db.reactions.messageId)); - - $$ReactionsTableProcessedTableManager get reactionsRefs { - final manager = $$ReactionsTableTableManager($_db, $_db.reactions) - .filter((f) => f.messageId.id.sqlEquals($_itemColumn('id')!)); - - final cache = $_typedResult.readTableOrNull(_reactionsRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); - } } -class $$MessagesTableFilterComposer - extends Composer<_$DriftChatDatabase, $MessagesTable> { - $$MessagesTableFilterComposer({ +class $$DraftMessagesTableFilterComposer + extends Composer<_$DriftChatDatabase, $DraftMessagesTable> { + $$DraftMessagesTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -9170,9 +10774,6 @@ class $$MessagesTableFilterComposer column: $table.attachments, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get state => $composableBuilder( - column: $table.state, builder: (column) => ColumnFilters(column)); - ColumnFilters get type => $composableBuilder( column: $table.type, builder: (column) => ColumnFilters(column)); @@ -9181,15 +10782,6 @@ class $$MessagesTableFilterComposer column: $table.mentionedUsers, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnWithTypeConverterFilters?, - Map, String> - get reactionGroups => $composableBuilder( - column: $table.reactionGroups, - builder: (column) => ColumnWithTypeConverterFilters(column)); - - ColumnFilters get parentId => $composableBuilder( - column: $table.parentId, builder: (column) => ColumnFilters(column)); - ColumnFilters get quotedMessageId => $composableBuilder( column: $table.quotedMessageId, builder: (column) => ColumnFilters(column)); @@ -9197,72 +10789,17 @@ class $$MessagesTableFilterComposer ColumnFilters get pollId => $composableBuilder( column: $table.pollId, builder: (column) => ColumnFilters(column)); - ColumnFilters get replyCount => $composableBuilder( - column: $table.replyCount, builder: (column) => ColumnFilters(column)); - ColumnFilters get showInChannel => $composableBuilder( column: $table.showInChannel, builder: (column) => ColumnFilters(column)); - ColumnFilters get shadowed => $composableBuilder( - column: $table.shadowed, builder: (column) => ColumnFilters(column)); - ColumnFilters get command => $composableBuilder( column: $table.command, builder: (column) => ColumnFilters(column)); - ColumnFilters get localCreatedAt => $composableBuilder( - column: $table.localCreatedAt, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get remoteCreatedAt => $composableBuilder( - column: $table.remoteCreatedAt, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get localUpdatedAt => $composableBuilder( - column: $table.localUpdatedAt, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get remoteUpdatedAt => $composableBuilder( - column: $table.remoteUpdatedAt, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get localDeletedAt => $composableBuilder( - column: $table.localDeletedAt, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get remoteDeletedAt => $composableBuilder( - column: $table.remoteDeletedAt, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get messageTextUpdatedAt => $composableBuilder( - column: $table.messageTextUpdatedAt, - builder: (column) => ColumnFilters(column)); - - ColumnFilters get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnFilters(column)); - - ColumnFilters get pinned => $composableBuilder( - column: $table.pinned, builder: (column) => ColumnFilters(column)); - - ColumnFilters get pinnedAt => $composableBuilder( - column: $table.pinnedAt, builder: (column) => ColumnFilters(column)); - - ColumnFilters get pinExpires => $composableBuilder( - column: $table.pinExpires, builder: (column) => ColumnFilters(column)); - - ColumnFilters get pinnedByUserId => $composableBuilder( - column: $table.pinnedByUserId, - builder: (column) => ColumnFilters(column)); - - ColumnWithTypeConverterFilters?, Map, - String> - get i18n => $composableBuilder( - column: $table.i18n, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get silent => $composableBuilder( + column: $table.silent, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, List, String> - get restrictedVisibility => $composableBuilder( - column: $table.restrictedVisibility, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnFilters(column)); ColumnWithTypeConverterFilters?, Map, String> @@ -9270,18 +10807,18 @@ class $$MessagesTableFilterComposer column: $table.extraData, builder: (column) => ColumnWithTypeConverterFilters(column)); - $$ChannelsTableFilterComposer get channelCid { - final $$ChannelsTableFilterComposer composer = $composerBuilder( + $$MessagesTableFilterComposer get parentId { + final $$MessagesTableFilterComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, + getCurrentColumn: (t) => t.parentId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableFilterComposer( + $$MessagesTableFilterComposer( $db: $db, - $table: $db.channels, + $table: $db.messages, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -9290,52 +10827,30 @@ class $$MessagesTableFilterComposer return composer; } - Expression draftMessagesRefs( - Expression Function($$DraftMessagesTableFilterComposer f) f) { - final $$DraftMessagesTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.draftMessages, - getReferencedColumn: (t) => t.parentId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$DraftMessagesTableFilterComposer( - $db: $db, - $table: $db.draftMessages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } - - Expression reactionsRefs( - Expression Function($$ReactionsTableFilterComposer f) f) { - final $$ReactionsTableFilterComposer composer = $composerBuilder( + $$ChannelsTableFilterComposer get channelCid { + final $$ChannelsTableFilterComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.reactions, - getReferencedColumn: (t) => t.messageId, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$ReactionsTableFilterComposer( + $$ChannelsTableFilterComposer( $db: $db, - $table: $db.reactions, + $table: $db.channels, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, )); - return f(composer); + return composer; } } -class $$MessagesTableOrderingComposer - extends Composer<_$DriftChatDatabase, $MessagesTable> { - $$MessagesTableOrderingComposer({ +class $$DraftMessagesTableOrderingComposer + extends Composer<_$DriftChatDatabase, $DraftMessagesTable> { + $$DraftMessagesTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -9351,9 +10866,6 @@ class $$MessagesTableOrderingComposer ColumnOrderings get attachments => $composableBuilder( column: $table.attachments, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get state => $composableBuilder( - column: $table.state, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get type => $composableBuilder( column: $table.type, builder: (column) => ColumnOrderings(column)); @@ -9361,13 +10873,6 @@ class $$MessagesTableOrderingComposer column: $table.mentionedUsers, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get reactionGroups => $composableBuilder( - column: $table.reactionGroups, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get parentId => $composableBuilder( - column: $table.parentId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get quotedMessageId => $composableBuilder( column: $table.quotedMessageId, builder: (column) => ColumnOrderings(column)); @@ -9375,73 +10880,42 @@ class $$MessagesTableOrderingComposer ColumnOrderings get pollId => $composableBuilder( column: $table.pollId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get replyCount => $composableBuilder( - column: $table.replyCount, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get showInChannel => $composableBuilder( column: $table.showInChannel, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get shadowed => $composableBuilder( - column: $table.shadowed, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get command => $composableBuilder( column: $table.command, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get localCreatedAt => $composableBuilder( - column: $table.localCreatedAt, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get remoteCreatedAt => $composableBuilder( - column: $table.remoteCreatedAt, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get localUpdatedAt => $composableBuilder( - column: $table.localUpdatedAt, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get remoteUpdatedAt => $composableBuilder( - column: $table.remoteUpdatedAt, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get localDeletedAt => $composableBuilder( - column: $table.localDeletedAt, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get remoteDeletedAt => $composableBuilder( - column: $table.remoteDeletedAt, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get messageTextUpdatedAt => $composableBuilder( - column: $table.messageTextUpdatedAt, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get pinned => $composableBuilder( - column: $table.pinned, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get pinnedAt => $composableBuilder( - column: $table.pinnedAt, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get pinExpires => $composableBuilder( - column: $table.pinExpires, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get pinnedByUserId => $composableBuilder( - column: $table.pinnedByUserId, - builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get i18n => $composableBuilder( - column: $table.i18n, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get restrictedVisibility => $composableBuilder( - column: $table.restrictedVisibility, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get silent => $composableBuilder( + column: $table.silent, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => ColumnOrderings(column)); ColumnOrderings get extraData => $composableBuilder( column: $table.extraData, builder: (column) => ColumnOrderings(column)); + $$MessagesTableOrderingComposer get parentId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.parentId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + $$MessagesTableOrderingComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + $$ChannelsTableOrderingComposer get channelCid { final $$ChannelsTableOrderingComposer composer = $composerBuilder( composer: this, @@ -9463,9 +10937,9 @@ class $$MessagesTableOrderingComposer } } -class $$MessagesTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $MessagesTable> { - $$MessagesTableAnnotationComposer({ +class $$DraftMessagesTableAnnotationComposer + extends Composer<_$DriftChatDatabase, $DraftMessagesTable> { + $$DraftMessagesTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, @@ -9482,9 +10956,6 @@ class $$MessagesTableAnnotationComposer $composableBuilder( column: $table.attachments, builder: (column) => column); - GeneratedColumn get state => - $composableBuilder(column: $table.state, builder: (column) => column); - GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); @@ -9492,90 +10963,40 @@ class $$MessagesTableAnnotationComposer $composableBuilder( column: $table.mentionedUsers, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> - get reactionGroups => $composableBuilder( - column: $table.reactionGroups, builder: (column) => column); - - GeneratedColumn get parentId => - $composableBuilder(column: $table.parentId, builder: (column) => column); - GeneratedColumn get quotedMessageId => $composableBuilder( column: $table.quotedMessageId, builder: (column) => column); GeneratedColumn get pollId => $composableBuilder(column: $table.pollId, builder: (column) => column); - GeneratedColumn get replyCount => $composableBuilder( - column: $table.replyCount, builder: (column) => column); - GeneratedColumn get showInChannel => $composableBuilder( column: $table.showInChannel, builder: (column) => column); - GeneratedColumn get shadowed => - $composableBuilder(column: $table.shadowed, builder: (column) => column); - GeneratedColumn get command => $composableBuilder(column: $table.command, builder: (column) => column); - GeneratedColumn get localCreatedAt => $composableBuilder( - column: $table.localCreatedAt, builder: (column) => column); - - GeneratedColumn get remoteCreatedAt => $composableBuilder( - column: $table.remoteCreatedAt, builder: (column) => column); - - GeneratedColumn get localUpdatedAt => $composableBuilder( - column: $table.localUpdatedAt, builder: (column) => column); - - GeneratedColumn get remoteUpdatedAt => $composableBuilder( - column: $table.remoteUpdatedAt, builder: (column) => column); - - GeneratedColumn get localDeletedAt => $composableBuilder( - column: $table.localDeletedAt, builder: (column) => column); - - GeneratedColumn get remoteDeletedAt => $composableBuilder( - column: $table.remoteDeletedAt, builder: (column) => column); - - GeneratedColumn get messageTextUpdatedAt => $composableBuilder( - column: $table.messageTextUpdatedAt, builder: (column) => column); - - GeneratedColumn get userId => - $composableBuilder(column: $table.userId, builder: (column) => column); - - GeneratedColumn get pinned => - $composableBuilder(column: $table.pinned, builder: (column) => column); - - GeneratedColumn get pinnedAt => - $composableBuilder(column: $table.pinnedAt, builder: (column) => column); - - GeneratedColumn get pinExpires => $composableBuilder( - column: $table.pinExpires, builder: (column) => column); - - GeneratedColumn get pinnedByUserId => $composableBuilder( - column: $table.pinnedByUserId, builder: (column) => column); - - GeneratedColumnWithTypeConverter?, String> get i18n => - $composableBuilder(column: $table.i18n, builder: (column) => column); + GeneratedColumn get silent => + $composableBuilder(column: $table.silent, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> - get restrictedVisibility => $composableBuilder( - column: $table.restrictedVisibility, builder: (column) => column); + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); GeneratedColumnWithTypeConverter?, String> get extraData => $composableBuilder( column: $table.extraData, builder: (column) => column); - $$ChannelsTableAnnotationComposer get channelCid { - final $$ChannelsTableAnnotationComposer composer = $composerBuilder( + $$MessagesTableAnnotationComposer get parentId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, + getCurrentColumn: (t) => t.parentId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableAnnotationComposer( + $$MessagesTableAnnotationComposer( $db: $db, - $table: $db.channels, + $table: $db.messages, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -9584,136 +11005,81 @@ class $$MessagesTableAnnotationComposer return composer; } - Expression draftMessagesRefs( - Expression Function($$DraftMessagesTableAnnotationComposer a) f) { - final $$DraftMessagesTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.draftMessages, - getReferencedColumn: (t) => t.parentId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$DraftMessagesTableAnnotationComposer( - $db: $db, - $table: $db.draftMessages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } - - Expression reactionsRefs( - Expression Function($$ReactionsTableAnnotationComposer a) f) { - final $$ReactionsTableAnnotationComposer composer = $composerBuilder( + $$ChannelsTableAnnotationComposer get channelCid { + final $$ChannelsTableAnnotationComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.reactions, - getReferencedColumn: (t) => t.messageId, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$ReactionsTableAnnotationComposer( + $$ChannelsTableAnnotationComposer( $db: $db, - $table: $db.reactions, + $table: $db.channels, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, )); - return f(composer); + return composer; } } -class $$MessagesTableTableManager extends RootTableManager< +class $$DraftMessagesTableTableManager extends RootTableManager< _$DriftChatDatabase, - $MessagesTable, - MessageEntity, - $$MessagesTableFilterComposer, - $$MessagesTableOrderingComposer, - $$MessagesTableAnnotationComposer, - $$MessagesTableCreateCompanionBuilder, - $$MessagesTableUpdateCompanionBuilder, - (MessageEntity, $$MessagesTableReferences), - MessageEntity, - PrefetchHooks Function( - {bool channelCid, bool draftMessagesRefs, bool reactionsRefs})> { - $$MessagesTableTableManager(_$DriftChatDatabase db, $MessagesTable table) + $DraftMessagesTable, + DraftMessageEntity, + $$DraftMessagesTableFilterComposer, + $$DraftMessagesTableOrderingComposer, + $$DraftMessagesTableAnnotationComposer, + $$DraftMessagesTableCreateCompanionBuilder, + $$DraftMessagesTableUpdateCompanionBuilder, + (DraftMessageEntity, $$DraftMessagesTableReferences), + DraftMessageEntity, + PrefetchHooks Function({bool parentId, bool channelCid})> { + $$DraftMessagesTableTableManager( + _$DriftChatDatabase db, $DraftMessagesTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$MessagesTableFilterComposer($db: db, $table: table), + $$DraftMessagesTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$MessagesTableOrderingComposer($db: db, $table: table), + $$DraftMessagesTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$MessagesTableAnnotationComposer($db: db, $table: table), + $$DraftMessagesTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), Value messageText = const Value.absent(), Value> attachments = const Value.absent(), - Value state = const Value.absent(), Value type = const Value.absent(), Value> mentionedUsers = const Value.absent(), - Value?> reactionGroups = - const Value.absent(), Value parentId = const Value.absent(), Value quotedMessageId = const Value.absent(), Value pollId = const Value.absent(), - Value replyCount = const Value.absent(), Value showInChannel = const Value.absent(), - Value shadowed = const Value.absent(), Value command = const Value.absent(), - Value localCreatedAt = const Value.absent(), - Value remoteCreatedAt = const Value.absent(), - Value localUpdatedAt = const Value.absent(), - Value remoteUpdatedAt = const Value.absent(), - Value localDeletedAt = const Value.absent(), - Value remoteDeletedAt = const Value.absent(), - Value messageTextUpdatedAt = const Value.absent(), - Value userId = const Value.absent(), - Value pinned = const Value.absent(), - Value pinnedAt = const Value.absent(), - Value pinExpires = const Value.absent(), - Value pinnedByUserId = const Value.absent(), + Value silent = const Value.absent(), + Value createdAt = const Value.absent(), Value channelCid = const Value.absent(), - Value?> i18n = const Value.absent(), - Value?> restrictedVisibility = const Value.absent(), Value?> extraData = const Value.absent(), Value rowid = const Value.absent(), }) => - MessagesCompanion( + DraftMessagesCompanion( id: id, messageText: messageText, attachments: attachments, - state: state, type: type, mentionedUsers: mentionedUsers, - reactionGroups: reactionGroups, parentId: parentId, quotedMessageId: quotedMessageId, pollId: pollId, - replyCount: replyCount, showInChannel: showInChannel, - shadowed: shadowed, command: command, - localCreatedAt: localCreatedAt, - remoteCreatedAt: remoteCreatedAt, - localUpdatedAt: localUpdatedAt, - remoteUpdatedAt: remoteUpdatedAt, - localDeletedAt: localDeletedAt, - remoteDeletedAt: remoteDeletedAt, - messageTextUpdatedAt: messageTextUpdatedAt, - userId: userId, - pinned: pinned, - pinnedAt: pinnedAt, - pinExpires: pinExpires, - pinnedByUserId: pinnedByUserId, + silent: silent, + createdAt: createdAt, channelCid: channelCid, - i18n: i18n, - restrictedVisibility: restrictedVisibility, extraData: extraData, rowid: rowid, ), @@ -9721,83 +11087,46 @@ class $$MessagesTableTableManager extends RootTableManager< required String id, Value messageText = const Value.absent(), required List attachments, - required String state, Value type = const Value.absent(), required List mentionedUsers, - Value?> reactionGroups = - const Value.absent(), Value parentId = const Value.absent(), Value quotedMessageId = const Value.absent(), Value pollId = const Value.absent(), - Value replyCount = const Value.absent(), Value showInChannel = const Value.absent(), - Value shadowed = const Value.absent(), Value command = const Value.absent(), - Value localCreatedAt = const Value.absent(), - Value remoteCreatedAt = const Value.absent(), - Value localUpdatedAt = const Value.absent(), - Value remoteUpdatedAt = const Value.absent(), - Value localDeletedAt = const Value.absent(), - Value remoteDeletedAt = const Value.absent(), - Value messageTextUpdatedAt = const Value.absent(), - Value userId = const Value.absent(), - Value pinned = const Value.absent(), - Value pinnedAt = const Value.absent(), - Value pinExpires = const Value.absent(), - Value pinnedByUserId = const Value.absent(), + Value silent = const Value.absent(), + Value createdAt = const Value.absent(), required String channelCid, - Value?> i18n = const Value.absent(), - Value?> restrictedVisibility = const Value.absent(), Value?> extraData = const Value.absent(), Value rowid = const Value.absent(), }) => - MessagesCompanion.insert( + DraftMessagesCompanion.insert( id: id, messageText: messageText, attachments: attachments, - state: state, type: type, mentionedUsers: mentionedUsers, - reactionGroups: reactionGroups, parentId: parentId, quotedMessageId: quotedMessageId, pollId: pollId, - replyCount: replyCount, showInChannel: showInChannel, - shadowed: shadowed, command: command, - localCreatedAt: localCreatedAt, - remoteCreatedAt: remoteCreatedAt, - localUpdatedAt: localUpdatedAt, - remoteUpdatedAt: remoteUpdatedAt, - localDeletedAt: localDeletedAt, - remoteDeletedAt: remoteDeletedAt, - messageTextUpdatedAt: messageTextUpdatedAt, - userId: userId, - pinned: pinned, - pinnedAt: pinnedAt, - pinExpires: pinExpires, - pinnedByUserId: pinnedByUserId, + silent: silent, + createdAt: createdAt, channelCid: channelCid, - i18n: i18n, - restrictedVisibility: restrictedVisibility, extraData: extraData, rowid: rowid, ), withReferenceMapper: (p0) => p0 - .map((e) => - (e.readTable(table), $$MessagesTableReferences(db, table, e))) + .map((e) => ( + e.readTable(table), + $$DraftMessagesTableReferences(db, table, e) + )) .toList(), - prefetchHooksCallback: ( - {channelCid = false, - draftMessagesRefs = false, - reactionsRefs = false}) { + prefetchHooksCallback: ({parentId = false, channelCid = false}) { return PrefetchHooks( db: db, - explicitlyWatchedTables: [ - if (draftMessagesRefs) db.draftMessages, - if (reactionsRefs) db.reactions - ], + explicitlyWatchedTables: [], addJoins: < T extends TableManagerState< dynamic, @@ -9811,205 +11140,152 @@ class $$MessagesTableTableManager extends RootTableManager< dynamic, dynamic, dynamic>>(state) { + if (parentId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.parentId, + referencedTable: + $$DraftMessagesTableReferences._parentIdTable(db), + referencedColumn: + $$DraftMessagesTableReferences._parentIdTable(db).id, + ) as T; + } if (channelCid) { state = state.withJoin( currentTable: table, currentColumn: table.channelCid, referencedTable: - $$MessagesTableReferences._channelCidTable(db), + $$DraftMessagesTableReferences._channelCidTable(db), referencedColumn: - $$MessagesTableReferences._channelCidTable(db).cid, + $$DraftMessagesTableReferences._channelCidTable(db).cid, ) as T; } return state; }, getPrefetchedDataCallback: (items) async { - return [ - if (draftMessagesRefs) - await $_getPrefetchedData( - currentTable: table, - referencedTable: $$MessagesTableReferences - ._draftMessagesRefsTable(db), - managerFromTypedResult: (p0) => - $$MessagesTableReferences(db, table, p0) - .draftMessagesRefs, - referencedItemsForCurrentItem: (item, - referencedItems) => - referencedItems.where((e) => e.parentId == item.id), - typedResults: items), - if (reactionsRefs) - await $_getPrefetchedData( - currentTable: table, - referencedTable: - $$MessagesTableReferences._reactionsRefsTable(db), - managerFromTypedResult: (p0) => - $$MessagesTableReferences(db, table, p0) - .reactionsRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems - .where((e) => e.messageId == item.id), - typedResults: items) - ]; + return []; }, ); }, )); } -typedef $$MessagesTableProcessedTableManager = ProcessedTableManager< +typedef $$DraftMessagesTableProcessedTableManager = ProcessedTableManager< _$DriftChatDatabase, - $MessagesTable, - MessageEntity, - $$MessagesTableFilterComposer, - $$MessagesTableOrderingComposer, - $$MessagesTableAnnotationComposer, - $$MessagesTableCreateCompanionBuilder, - $$MessagesTableUpdateCompanionBuilder, - (MessageEntity, $$MessagesTableReferences), - MessageEntity, - PrefetchHooks Function( - {bool channelCid, bool draftMessagesRefs, bool reactionsRefs})>; -typedef $$DraftMessagesTableCreateCompanionBuilder = DraftMessagesCompanion - Function({ - required String id, - Value messageText, - required List attachments, - Value type, - required List mentionedUsers, - Value parentId, - Value quotedMessageId, - Value pollId, - Value showInChannel, - Value command, - Value silent, + $DraftMessagesTable, + DraftMessageEntity, + $$DraftMessagesTableFilterComposer, + $$DraftMessagesTableOrderingComposer, + $$DraftMessagesTableAnnotationComposer, + $$DraftMessagesTableCreateCompanionBuilder, + $$DraftMessagesTableUpdateCompanionBuilder, + (DraftMessageEntity, $$DraftMessagesTableReferences), + DraftMessageEntity, + PrefetchHooks Function({bool parentId, bool channelCid})>; +typedef $$LocationsTableCreateCompanionBuilder = LocationsCompanion Function({ + Value channelCid, + Value messageId, + Value userId, + required double latitude, + required double longitude, + Value createdByDeviceId, + Value endAt, Value createdAt, - required String channelCid, - Value?> extraData, + Value updatedAt, Value rowid, }); -typedef $$DraftMessagesTableUpdateCompanionBuilder = DraftMessagesCompanion - Function({ - Value id, - Value messageText, - Value> attachments, - Value type, - Value> mentionedUsers, - Value parentId, - Value quotedMessageId, - Value pollId, - Value showInChannel, - Value command, - Value silent, +typedef $$LocationsTableUpdateCompanionBuilder = LocationsCompanion Function({ + Value channelCid, + Value messageId, + Value userId, + Value latitude, + Value longitude, + Value createdByDeviceId, + Value endAt, Value createdAt, - Value channelCid, - Value?> extraData, + Value updatedAt, Value rowid, }); -final class $$DraftMessagesTableReferences extends BaseReferences< - _$DriftChatDatabase, $DraftMessagesTable, DraftMessageEntity> { - $$DraftMessagesTableReferences( - super.$_db, super.$_table, super.$_typedResult); +final class $$LocationsTableReferences extends BaseReferences< + _$DriftChatDatabase, $LocationsTable, LocationEntity> { + $$LocationsTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $MessagesTable _parentIdTable(_$DriftChatDatabase db) => - db.messages.createAlias( - $_aliasNameGenerator(db.draftMessages.parentId, db.messages.id)); + static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => + db.channels.createAlias( + $_aliasNameGenerator(db.locations.channelCid, db.channels.cid)); - $$MessagesTableProcessedTableManager? get parentId { - final $_column = $_itemColumn('parent_id'); + $$ChannelsTableProcessedTableManager? get channelCid { + final $_column = $_itemColumn('channel_cid'); if ($_column == null) return null; - final manager = $$MessagesTableTableManager($_db, $_db.messages) - .filter((f) => f.id.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_parentIdTable($_db)); + final manager = $$ChannelsTableTableManager($_db, $_db.channels) + .filter((f) => f.cid.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_channelCidTable($_db)); if (item == null) return manager; return ProcessedTableManager( manager.$state.copyWith(prefetchedData: [item])); } - static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => - db.channels.createAlias( - $_aliasNameGenerator(db.draftMessages.channelCid, db.channels.cid)); - - $$ChannelsTableProcessedTableManager get channelCid { - final $_column = $_itemColumn('channel_cid')!; + static $MessagesTable _messageIdTable(_$DriftChatDatabase db) => + db.messages.createAlias( + $_aliasNameGenerator(db.locations.messageId, db.messages.id)); - final manager = $$ChannelsTableTableManager($_db, $_db.channels) - .filter((f) => f.cid.sqlEquals($_column)); - final item = $_typedResult.readTableOrNull(_channelCidTable($_db)); + $$MessagesTableProcessedTableManager? get messageId { + final $_column = $_itemColumn('message_id'); + if ($_column == null) return null; + final manager = $$MessagesTableTableManager($_db, $_db.messages) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_messageIdTable($_db)); if (item == null) return manager; return ProcessedTableManager( manager.$state.copyWith(prefetchedData: [item])); } } -class $$DraftMessagesTableFilterComposer - extends Composer<_$DriftChatDatabase, $DraftMessagesTable> { - $$DraftMessagesTableFilterComposer({ +class $$LocationsTableFilterComposer + extends Composer<_$DriftChatDatabase, $LocationsTable> { + $$LocationsTableFilterComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); - - ColumnFilters get messageText => $composableBuilder( - column: $table.messageText, builder: (column) => ColumnFilters(column)); - - ColumnWithTypeConverterFilters, List, String> - get attachments => $composableBuilder( - column: $table.attachments, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnFilters(column)); - ColumnFilters get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnFilters(column)); + ColumnFilters get latitude => $composableBuilder( + column: $table.latitude, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters, List, String> - get mentionedUsers => $composableBuilder( - column: $table.mentionedUsers, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get longitude => $composableBuilder( + column: $table.longitude, builder: (column) => ColumnFilters(column)); - ColumnFilters get quotedMessageId => $composableBuilder( - column: $table.quotedMessageId, + ColumnFilters get createdByDeviceId => $composableBuilder( + column: $table.createdByDeviceId, builder: (column) => ColumnFilters(column)); - ColumnFilters get pollId => $composableBuilder( - column: $table.pollId, builder: (column) => ColumnFilters(column)); - - ColumnFilters get showInChannel => $composableBuilder( - column: $table.showInChannel, builder: (column) => ColumnFilters(column)); - - ColumnFilters get command => $composableBuilder( - column: $table.command, builder: (column) => ColumnFilters(column)); - - ColumnFilters get silent => $composableBuilder( - column: $table.silent, builder: (column) => ColumnFilters(column)); + ColumnFilters get endAt => $composableBuilder( + column: $table.endAt, builder: (column) => ColumnFilters(column)); ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, Map, - String> - get extraData => $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnFilters(column)); - $$MessagesTableFilterComposer get parentId { - final $$MessagesTableFilterComposer composer = $composerBuilder( + $$ChannelsTableFilterComposer get channelCid { + final $$ChannelsTableFilterComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.parentId, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.id, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$MessagesTableFilterComposer( + $$ChannelsTableFilterComposer( $db: $db, - $table: $db.messages, + $table: $db.channels, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -10018,18 +11294,18 @@ class $$DraftMessagesTableFilterComposer return composer; } - $$ChannelsTableFilterComposer get channelCid { - final $$ChannelsTableFilterComposer composer = $composerBuilder( + $$MessagesTableFilterComposer get messageId { + final $$MessagesTableFilterComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableFilterComposer( + $$MessagesTableFilterComposer( $db: $db, - $table: $db.channels, + $table: $db.messages, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -10039,66 +11315,49 @@ class $$DraftMessagesTableFilterComposer } } -class $$DraftMessagesTableOrderingComposer - extends Composer<_$DriftChatDatabase, $DraftMessagesTable> { - $$DraftMessagesTableOrderingComposer({ +class $$LocationsTableOrderingComposer + extends Composer<_$DriftChatDatabase, $LocationsTable> { + $$LocationsTableOrderingComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get messageText => $composableBuilder( - column: $table.messageText, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get attachments => $composableBuilder( - column: $table.attachments, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get mentionedUsers => $composableBuilder( - column: $table.mentionedUsers, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get quotedMessageId => $composableBuilder( - column: $table.quotedMessageId, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get latitude => $composableBuilder( + column: $table.latitude, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pollId => $composableBuilder( - column: $table.pollId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get longitude => $composableBuilder( + column: $table.longitude, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get showInChannel => $composableBuilder( - column: $table.showInChannel, + ColumnOrderings get createdByDeviceId => $composableBuilder( + column: $table.createdByDeviceId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get command => $composableBuilder( - column: $table.command, builder: (column) => ColumnOrderings(column)); - - ColumnOrderings get silent => $composableBuilder( - column: $table.silent, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get endAt => $composableBuilder( + column: $table.endAt, builder: (column) => ColumnOrderings(column)); ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - $$MessagesTableOrderingComposer get parentId { - final $$MessagesTableOrderingComposer composer = $composerBuilder( + $$ChannelsTableOrderingComposer get channelCid { + final $$ChannelsTableOrderingComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.parentId, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.id, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$MessagesTableOrderingComposer( + $$ChannelsTableOrderingComposer( $db: $db, - $table: $db.messages, + $table: $db.channels, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -10107,18 +11366,18 @@ class $$DraftMessagesTableOrderingComposer return composer; } - $$ChannelsTableOrderingComposer get channelCid { - final $$ChannelsTableOrderingComposer composer = $composerBuilder( + $$MessagesTableOrderingComposer get messageId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableOrderingComposer( + $$MessagesTableOrderingComposer( $db: $db, - $table: $db.channels, + $table: $db.messages, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -10128,66 +11387,48 @@ class $$DraftMessagesTableOrderingComposer } } -class $$DraftMessagesTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $DraftMessagesTable> { - $$DraftMessagesTableAnnotationComposer({ +class $$LocationsTableAnnotationComposer + extends Composer<_$DriftChatDatabase, $LocationsTable> { + $$LocationsTableAnnotationComposer({ required super.$db, required super.$table, super.joinBuilder, super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); - - GeneratedColumn get messageText => $composableBuilder( - column: $table.messageText, builder: (column) => column); - - GeneratedColumnWithTypeConverter, String> get attachments => - $composableBuilder( - column: $table.attachments, builder: (column) => column); - - GeneratedColumn get type => - $composableBuilder(column: $table.type, builder: (column) => column); - - GeneratedColumnWithTypeConverter, String> get mentionedUsers => - $composableBuilder( - column: $table.mentionedUsers, builder: (column) => column); - - GeneratedColumn get quotedMessageId => $composableBuilder( - column: $table.quotedMessageId, builder: (column) => column); + GeneratedColumn get userId => + $composableBuilder(column: $table.userId, builder: (column) => column); - GeneratedColumn get pollId => - $composableBuilder(column: $table.pollId, builder: (column) => column); + GeneratedColumn get latitude => + $composableBuilder(column: $table.latitude, builder: (column) => column); - GeneratedColumn get showInChannel => $composableBuilder( - column: $table.showInChannel, builder: (column) => column); + GeneratedColumn get longitude => + $composableBuilder(column: $table.longitude, builder: (column) => column); - GeneratedColumn get command => - $composableBuilder(column: $table.command, builder: (column) => column); + GeneratedColumn get createdByDeviceId => $composableBuilder( + column: $table.createdByDeviceId, builder: (column) => column); - GeneratedColumn get silent => - $composableBuilder(column: $table.silent, builder: (column) => column); + GeneratedColumn get endAt => + $composableBuilder(column: $table.endAt, builder: (column) => column); GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> - get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => column); + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); - $$MessagesTableAnnotationComposer get parentId { - final $$MessagesTableAnnotationComposer composer = $composerBuilder( + $$ChannelsTableAnnotationComposer get channelCid { + final $$ChannelsTableAnnotationComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.parentId, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.id, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$MessagesTableAnnotationComposer( + $$ChannelsTableAnnotationComposer( $db: $db, - $table: $db.messages, + $table: $db.channels, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -10196,18 +11437,18 @@ class $$DraftMessagesTableAnnotationComposer return composer; } - $$ChannelsTableAnnotationComposer get channelCid { - final $$ChannelsTableAnnotationComposer composer = $composerBuilder( + $$MessagesTableAnnotationComposer get messageId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableAnnotationComposer( + $$MessagesTableAnnotationComposer( $db: $db, - $table: $db.channels, + $table: $db.messages, $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -10217,104 +11458,83 @@ class $$DraftMessagesTableAnnotationComposer } } -class $$DraftMessagesTableTableManager extends RootTableManager< +class $$LocationsTableTableManager extends RootTableManager< _$DriftChatDatabase, - $DraftMessagesTable, - DraftMessageEntity, - $$DraftMessagesTableFilterComposer, - $$DraftMessagesTableOrderingComposer, - $$DraftMessagesTableAnnotationComposer, - $$DraftMessagesTableCreateCompanionBuilder, - $$DraftMessagesTableUpdateCompanionBuilder, - (DraftMessageEntity, $$DraftMessagesTableReferences), - DraftMessageEntity, - PrefetchHooks Function({bool parentId, bool channelCid})> { - $$DraftMessagesTableTableManager( - _$DriftChatDatabase db, $DraftMessagesTable table) + $LocationsTable, + LocationEntity, + $$LocationsTableFilterComposer, + $$LocationsTableOrderingComposer, + $$LocationsTableAnnotationComposer, + $$LocationsTableCreateCompanionBuilder, + $$LocationsTableUpdateCompanionBuilder, + (LocationEntity, $$LocationsTableReferences), + LocationEntity, + PrefetchHooks Function({bool channelCid, bool messageId})> { + $$LocationsTableTableManager(_$DriftChatDatabase db, $LocationsTable table) : super(TableManagerState( db: db, table: table, createFilteringComposer: () => - $$DraftMessagesTableFilterComposer($db: db, $table: table), + $$LocationsTableFilterComposer($db: db, $table: table), createOrderingComposer: () => - $$DraftMessagesTableOrderingComposer($db: db, $table: table), + $$LocationsTableOrderingComposer($db: db, $table: table), createComputedFieldComposer: () => - $$DraftMessagesTableAnnotationComposer($db: db, $table: table), + $$LocationsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - Value id = const Value.absent(), - Value messageText = const Value.absent(), - Value> attachments = const Value.absent(), - Value type = const Value.absent(), - Value> mentionedUsers = const Value.absent(), - Value parentId = const Value.absent(), - Value quotedMessageId = const Value.absent(), - Value pollId = const Value.absent(), - Value showInChannel = const Value.absent(), - Value command = const Value.absent(), - Value silent = const Value.absent(), + Value channelCid = const Value.absent(), + Value messageId = const Value.absent(), + Value userId = const Value.absent(), + Value latitude = const Value.absent(), + Value longitude = const Value.absent(), + Value createdByDeviceId = const Value.absent(), + Value endAt = const Value.absent(), Value createdAt = const Value.absent(), - Value channelCid = const Value.absent(), - Value?> extraData = const Value.absent(), + Value updatedAt = const Value.absent(), Value rowid = const Value.absent(), }) => - DraftMessagesCompanion( - id: id, - messageText: messageText, - attachments: attachments, - type: type, - mentionedUsers: mentionedUsers, - parentId: parentId, - quotedMessageId: quotedMessageId, - pollId: pollId, - showInChannel: showInChannel, - command: command, - silent: silent, - createdAt: createdAt, + LocationsCompanion( channelCid: channelCid, - extraData: extraData, + messageId: messageId, + userId: userId, + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: endAt, + createdAt: createdAt, + updatedAt: updatedAt, rowid: rowid, ), createCompanionCallback: ({ - required String id, - Value messageText = const Value.absent(), - required List attachments, - Value type = const Value.absent(), - required List mentionedUsers, - Value parentId = const Value.absent(), - Value quotedMessageId = const Value.absent(), - Value pollId = const Value.absent(), - Value showInChannel = const Value.absent(), - Value command = const Value.absent(), - Value silent = const Value.absent(), + Value channelCid = const Value.absent(), + Value messageId = const Value.absent(), + Value userId = const Value.absent(), + required double latitude, + required double longitude, + Value createdByDeviceId = const Value.absent(), + Value endAt = const Value.absent(), Value createdAt = const Value.absent(), - required String channelCid, - Value?> extraData = const Value.absent(), + Value updatedAt = const Value.absent(), Value rowid = const Value.absent(), }) => - DraftMessagesCompanion.insert( - id: id, - messageText: messageText, - attachments: attachments, - type: type, - mentionedUsers: mentionedUsers, - parentId: parentId, - quotedMessageId: quotedMessageId, - pollId: pollId, - showInChannel: showInChannel, - command: command, - silent: silent, - createdAt: createdAt, + LocationsCompanion.insert( channelCid: channelCid, - extraData: extraData, + messageId: messageId, + userId: userId, + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: endAt, + createdAt: createdAt, + updatedAt: updatedAt, rowid: rowid, ), withReferenceMapper: (p0) => p0 .map((e) => ( e.readTable(table), - $$DraftMessagesTableReferences(db, table, e) + $$LocationsTableReferences(db, table, e) )) .toList(), - prefetchHooksCallback: ({parentId = false, channelCid = false}) { + prefetchHooksCallback: ({channelCid = false, messageId = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], @@ -10331,24 +11551,24 @@ class $$DraftMessagesTableTableManager extends RootTableManager< dynamic, dynamic, dynamic>>(state) { - if (parentId) { + if (channelCid) { state = state.withJoin( currentTable: table, - currentColumn: table.parentId, + currentColumn: table.channelCid, referencedTable: - $$DraftMessagesTableReferences._parentIdTable(db), + $$LocationsTableReferences._channelCidTable(db), referencedColumn: - $$DraftMessagesTableReferences._parentIdTable(db).id, + $$LocationsTableReferences._channelCidTable(db).cid, ) as T; } - if (channelCid) { + if (messageId) { state = state.withJoin( currentTable: table, - currentColumn: table.channelCid, + currentColumn: table.messageId, referencedTable: - $$DraftMessagesTableReferences._channelCidTable(db), + $$LocationsTableReferences._messageIdTable(db), referencedColumn: - $$DraftMessagesTableReferences._channelCidTable(db).cid, + $$LocationsTableReferences._messageIdTable(db).id, ) as T; } @@ -10362,18 +11582,18 @@ class $$DraftMessagesTableTableManager extends RootTableManager< )); } -typedef $$DraftMessagesTableProcessedTableManager = ProcessedTableManager< +typedef $$LocationsTableProcessedTableManager = ProcessedTableManager< _$DriftChatDatabase, - $DraftMessagesTable, - DraftMessageEntity, - $$DraftMessagesTableFilterComposer, - $$DraftMessagesTableOrderingComposer, - $$DraftMessagesTableAnnotationComposer, - $$DraftMessagesTableCreateCompanionBuilder, - $$DraftMessagesTableUpdateCompanionBuilder, - (DraftMessageEntity, $$DraftMessagesTableReferences), - DraftMessageEntity, - PrefetchHooks Function({bool parentId, bool channelCid})>; + $LocationsTable, + LocationEntity, + $$LocationsTableFilterComposer, + $$LocationsTableOrderingComposer, + $$LocationsTableAnnotationComposer, + $$LocationsTableCreateCompanionBuilder, + $$LocationsTableUpdateCompanionBuilder, + (LocationEntity, $$LocationsTableReferences), + LocationEntity, + PrefetchHooks Function({bool channelCid, bool messageId})>; typedef $$PinnedMessagesTableCreateCompanionBuilder = PinnedMessagesCompanion Function({ required String id, @@ -11816,20 +13036,24 @@ typedef $$PollVotesTableProcessedTableManager = ProcessedTableManager< PrefetchHooks Function({bool pollId})>; typedef $$PinnedMessageReactionsTableCreateCompanionBuilder = PinnedMessageReactionsCompanion Function({ - required String userId, - required String messageId, + Value userId, + Value messageId, required String type, + Value emojiCode, Value createdAt, + Value updatedAt, Value score, Value?> extraData, Value rowid, }); typedef $$PinnedMessageReactionsTableUpdateCompanionBuilder = PinnedMessageReactionsCompanion Function({ - Value userId, - Value messageId, + Value userId, + Value messageId, Value type, + Value emojiCode, Value createdAt, + Value updatedAt, Value score, Value?> extraData, Value rowid, @@ -11846,9 +13070,9 @@ final class $$PinnedMessageReactionsTableReferences extends BaseReferences< db.pinnedMessages.createAlias($_aliasNameGenerator( db.pinnedMessageReactions.messageId, db.pinnedMessages.id)); - $$PinnedMessagesTableProcessedTableManager get messageId { - final $_column = $_itemColumn('message_id')!; - + $$PinnedMessagesTableProcessedTableManager? get messageId { + final $_column = $_itemColumn('message_id'); + if ($_column == null) return null; final manager = $$PinnedMessagesTableTableManager($_db, $_db.pinnedMessages) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_messageIdTable($_db)); @@ -11873,9 +13097,15 @@ class $$PinnedMessageReactionsTableFilterComposer ColumnFilters get type => $composableBuilder( column: $table.type, builder: (column) => ColumnFilters(column)); + ColumnFilters get emojiCode => $composableBuilder( + column: $table.emojiCode, builder: (column) => ColumnFilters(column)); + ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get score => $composableBuilder( column: $table.score, builder: (column) => ColumnFilters(column)); @@ -11921,9 +13151,15 @@ class $$PinnedMessageReactionsTableOrderingComposer ColumnOrderings get type => $composableBuilder( column: $table.type, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get emojiCode => $composableBuilder( + column: $table.emojiCode, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get score => $composableBuilder( column: $table.score, builder: (column) => ColumnOrderings(column)); @@ -11966,9 +13202,15 @@ class $$PinnedMessageReactionsTableAnnotationComposer GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get emojiCode => + $composableBuilder(column: $table.emojiCode, builder: (column) => column); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + GeneratedColumn get score => $composableBuilder(column: $table.score, builder: (column) => column); @@ -12024,10 +13266,12 @@ class $$PinnedMessageReactionsTableTableManager extends RootTableManager< $$PinnedMessageReactionsTableAnnotationComposer( $db: db, $table: table), updateCompanionCallback: ({ - Value userId = const Value.absent(), - Value messageId = const Value.absent(), + Value userId = const Value.absent(), + Value messageId = const Value.absent(), Value type = const Value.absent(), + Value emojiCode = const Value.absent(), Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), Value score = const Value.absent(), Value?> extraData = const Value.absent(), Value rowid = const Value.absent(), @@ -12036,16 +13280,20 @@ class $$PinnedMessageReactionsTableTableManager extends RootTableManager< userId: userId, messageId: messageId, type: type, + emojiCode: emojiCode, createdAt: createdAt, + updatedAt: updatedAt, score: score, extraData: extraData, rowid: rowid, ), createCompanionCallback: ({ - required String userId, - required String messageId, + Value userId = const Value.absent(), + Value messageId = const Value.absent(), required String type, + Value emojiCode = const Value.absent(), Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), Value score = const Value.absent(), Value?> extraData = const Value.absent(), Value rowid = const Value.absent(), @@ -12054,7 +13302,9 @@ class $$PinnedMessageReactionsTableTableManager extends RootTableManager< userId: userId, messageId: messageId, type: type, + emojiCode: emojiCode, createdAt: createdAt, + updatedAt: updatedAt, score: score, extraData: extraData, rowid: rowid, @@ -12118,19 +13368,23 @@ typedef $$PinnedMessageReactionsTableProcessedTableManager PinnedMessageReactionEntity, PrefetchHooks Function({bool messageId})>; typedef $$ReactionsTableCreateCompanionBuilder = ReactionsCompanion Function({ - required String userId, - required String messageId, + Value userId, + Value messageId, required String type, + Value emojiCode, Value createdAt, + Value updatedAt, Value score, Value?> extraData, Value rowid, }); typedef $$ReactionsTableUpdateCompanionBuilder = ReactionsCompanion Function({ - Value userId, - Value messageId, + Value userId, + Value messageId, Value type, + Value emojiCode, Value createdAt, + Value updatedAt, Value score, Value?> extraData, Value rowid, @@ -12144,9 +13398,9 @@ final class $$ReactionsTableReferences extends BaseReferences< db.messages.createAlias( $_aliasNameGenerator(db.reactions.messageId, db.messages.id)); - $$MessagesTableProcessedTableManager get messageId { - final $_column = $_itemColumn('message_id')!; - + $$MessagesTableProcessedTableManager? get messageId { + final $_column = $_itemColumn('message_id'); + if ($_column == null) return null; final manager = $$MessagesTableTableManager($_db, $_db.messages) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_messageIdTable($_db)); @@ -12171,9 +13425,15 @@ class $$ReactionsTableFilterComposer ColumnFilters get type => $composableBuilder( column: $table.type, builder: (column) => ColumnFilters(column)); + ColumnFilters get emojiCode => $composableBuilder( + column: $table.emojiCode, builder: (column) => ColumnFilters(column)); + ColumnFilters get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get score => $composableBuilder( column: $table.score, builder: (column) => ColumnFilters(column)); @@ -12219,9 +13479,15 @@ class $$ReactionsTableOrderingComposer ColumnOrderings get type => $composableBuilder( column: $table.type, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get emojiCode => $composableBuilder( + column: $table.emojiCode, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdAt => $composableBuilder( column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get score => $composableBuilder( column: $table.score, builder: (column) => ColumnOrderings(column)); @@ -12264,9 +13530,15 @@ class $$ReactionsTableAnnotationComposer GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get emojiCode => + $composableBuilder(column: $table.emojiCode, builder: (column) => column); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + GeneratedColumn get score => $composableBuilder(column: $table.score, builder: (column) => column); @@ -12318,10 +13590,12 @@ class $$ReactionsTableTableManager extends RootTableManager< createComputedFieldComposer: () => $$ReactionsTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - Value userId = const Value.absent(), - Value messageId = const Value.absent(), + Value userId = const Value.absent(), + Value messageId = const Value.absent(), Value type = const Value.absent(), + Value emojiCode = const Value.absent(), Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), Value score = const Value.absent(), Value?> extraData = const Value.absent(), Value rowid = const Value.absent(), @@ -12330,16 +13604,20 @@ class $$ReactionsTableTableManager extends RootTableManager< userId: userId, messageId: messageId, type: type, + emojiCode: emojiCode, createdAt: createdAt, + updatedAt: updatedAt, score: score, extraData: extraData, rowid: rowid, ), createCompanionCallback: ({ - required String userId, - required String messageId, + Value userId = const Value.absent(), + Value messageId = const Value.absent(), required String type, + Value emojiCode = const Value.absent(), Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), Value score = const Value.absent(), Value?> extraData = const Value.absent(), Value rowid = const Value.absent(), @@ -12348,7 +13626,9 @@ class $$ReactionsTableTableManager extends RootTableManager< userId: userId, messageId: messageId, type: type, + emojiCode: emojiCode, createdAt: createdAt, + updatedAt: updatedAt, score: score, extraData: extraData, rowid: rowid, @@ -13711,6 +14991,8 @@ class $DriftChatDatabaseManager { $$MessagesTableTableManager(_db, _db.messages); $$DraftMessagesTableTableManager get draftMessages => $$DraftMessagesTableTableManager(_db, _db.draftMessages); + $$LocationsTableTableManager get locations => + $$LocationsTableTableManager(_db, _db.locations); $$PinnedMessagesTableTableManager get pinnedMessages => $$PinnedMessagesTableTableManager(_db, _db.pinnedMessages); $$PollsTableTableManager get polls => diff --git a/packages/stream_chat_persistence/lib/src/entity/entity.dart b/packages/stream_chat_persistence/lib/src/entity/entity.dart index 2ef87c5cb6..58fb6a164d 100644 --- a/packages/stream_chat_persistence/lib/src/entity/entity.dart +++ b/packages/stream_chat_persistence/lib/src/entity/entity.dart @@ -2,6 +2,7 @@ export 'channel_queries.dart'; export 'channels.dart'; export 'connection_events.dart'; export 'draft_messages.dart'; +export 'locations.dart'; export 'members.dart'; export 'messages.dart'; export 'pinned_message_reactions.dart'; diff --git a/packages/stream_chat_persistence/lib/src/entity/locations.dart b/packages/stream_chat_persistence/lib/src/entity/locations.dart new file mode 100644 index 0000000000..b12f448b8b --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/entity/locations.dart @@ -0,0 +1,43 @@ +// coverage:ignore-file +import 'package:drift/drift.dart'; +import 'package:stream_chat_persistence/src/entity/channels.dart'; +import 'package:stream_chat_persistence/src/entity/messages.dart'; + +/// Represents a [Locations] table in [DriftChatDatabase]. +@DataClassName('LocationEntity') +class Locations extends Table { + /// The channel CID where the location is shared + TextColumn get channelCid => text() + .nullable() + .references(Channels, #cid, onDelete: KeyAction.cascade)(); + + /// The ID of the message that contains this shared location + TextColumn get messageId => text() + .nullable() + .references(Messages, #id, onDelete: KeyAction.cascade)(); + + /// The ID of the user who shared the location + TextColumn get userId => text().nullable()(); + + /// The latitude of the shared location + RealColumn get latitude => real()(); + + /// The longitude of the shared location + RealColumn get longitude => real()(); + + /// The ID of the device that created the location + TextColumn get createdByDeviceId => text().nullable()(); + + /// The date at which the shared location will end (for live locations) + /// If null, this is a static location + DateTimeColumn get endAt => dateTime().nullable()(); + + /// The date at which the location was created + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + /// The date at which the location was last updated + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + @override + Set get primaryKey => {messageId}; +} diff --git a/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart b/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart index e4c9b06e58..6e490cf8fc 100644 --- a/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart +++ b/packages/stream_chat_persistence/lib/src/entity/pinned_message_reactions.dart @@ -8,6 +8,7 @@ import 'package:stream_chat_persistence/src/entity/reactions.dart'; class PinnedMessageReactions extends Reactions { /// The messageId to which the reaction belongs @override - TextColumn get messageId => - text().references(PinnedMessages, #id, onDelete: KeyAction.cascade)(); + TextColumn get messageId => text() + .nullable() + .references(PinnedMessages, #id, onDelete: KeyAction.cascade)(); } diff --git a/packages/stream_chat_persistence/lib/src/entity/reactions.dart b/packages/stream_chat_persistence/lib/src/entity/reactions.dart index 39bf42589f..64d23bf979 100644 --- a/packages/stream_chat_persistence/lib/src/entity/reactions.dart +++ b/packages/stream_chat_persistence/lib/src/entity/reactions.dart @@ -7,18 +7,25 @@ import 'package:stream_chat_persistence/src/entity/messages.dart'; @DataClassName('ReactionEntity') class Reactions extends Table { /// The id of the user that sent the reaction - TextColumn get userId => text()(); + TextColumn get userId => text().nullable()(); /// The messageId to which the reaction belongs - TextColumn get messageId => - text().references(Messages, #id, onDelete: KeyAction.cascade)(); + TextColumn get messageId => text() + .nullable() + .references(Messages, #id, onDelete: KeyAction.cascade)(); /// The type of the reaction TextColumn get type => text()(); + /// The emoji code for the reaction + TextColumn get emojiCode => text().nullable()(); + /// The DateTime on which the reaction is created DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + /// The DateTime on which the reaction was last updated + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + /// The score of the reaction (ie. number of reactions sent) IntColumn get score => integer().withDefault(const Constant(0))(); diff --git a/packages/stream_chat_persistence/lib/src/mapper/location_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/location_mapper.dart new file mode 100644 index 0000000000..bcbc18be1b --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/mapper/location_mapper.dart @@ -0,0 +1,40 @@ +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; + +/// Useful mapping functions for [LocationEntity] +extension LocationEntityX on LocationEntity { + /// Maps a [LocationEntity] into [Location] + Location toLocation({ + ChannelModel? channel, + Message? message, + }) => + Location( + channelCid: channelCid, + channel: channel, + messageId: messageId, + message: message, + userId: userId, + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: endAt, + createdAt: createdAt, + updatedAt: updatedAt, + ); +} + +/// Useful mapping functions for [Location] +extension LocationX on Location { + /// Maps a [Location] into [LocationEntity] + LocationEntity toEntity() => LocationEntity( + channelCid: channelCid, + messageId: messageId, + userId: userId, + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: endAt, + createdAt: createdAt, + updatedAt: updatedAt, + ); +} diff --git a/packages/stream_chat_persistence/lib/src/mapper/mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/mapper.dart index 742776f504..45b35dba81 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/mapper.dart @@ -1,6 +1,7 @@ export 'channel_mapper.dart'; export 'draft_message_mapper.dart'; export 'event_mapper.dart'; +export 'location_mapper.dart'; export 'member_mapper.dart'; export 'message_mapper.dart'; export 'pinned_message_mapper.dart'; diff --git a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart index 3235925165..4640c0f325 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart @@ -14,6 +14,7 @@ extension MessageEntityX on MessageEntity { Message? quotedMessage, Poll? poll, Draft? draft, + Location? sharedLocation, }) => Message( shadowed: shadowed, @@ -54,6 +55,7 @@ extension MessageEntityX on MessageEntity { i18n: i18n, restrictedVisibility: restrictedVisibility, draft: draft, + sharedLocation: sharedLocation, ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart index c6cbf19414..fbd168c717 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart @@ -14,6 +14,7 @@ extension PinnedMessageEntityX on PinnedMessageEntity { Message? quotedMessage, Poll? poll, Draft? draft, + Location? sharedLocation, }) => Message( shadowed: shadowed, @@ -54,6 +55,7 @@ extension PinnedMessageEntityX on PinnedMessageEntity { i18n: i18n, restrictedVisibility: restrictedVisibility, draft: draft, + sharedLocation: sharedLocation, ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart index ecab7cb40e..00deda65d2 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_reaction_mapper.dart @@ -5,13 +5,15 @@ import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; extension PinnedMessageReactionEntityX on PinnedMessageReactionEntity { /// Maps a [PinnedMessageReactionEntity] into [Reaction] Reaction toReaction({User? user}) => Reaction( - extraData: extraData ?? {}, type: type, - createdAt: createdAt, userId: userId, user: user, messageId: messageId, score: score, + emojiCode: emojiCode, + createdAt: createdAt, + updatedAt: updatedAt, + extraData: extraData ?? {}, ); } @@ -19,11 +21,13 @@ extension PinnedMessageReactionEntityX on PinnedMessageReactionEntity { extension PReactionX on Reaction { /// Maps a [Reaction] into [ReactionEntity] PinnedMessageReactionEntity toPinnedEntity() => PinnedMessageReactionEntity( - extraData: extraData, type: type, - createdAt: createdAt, - userId: userId!, - messageId: messageId!, + userId: userId ?? user?.id, + messageId: messageId, score: score, + emojiCode: emojiCode, + createdAt: createdAt, + updatedAt: updatedAt, + extraData: extraData, ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart index a524fafe1c..81b2bd4419 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart @@ -5,13 +5,15 @@ import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; extension ReactionEntityX on ReactionEntity { /// Maps a [ReactionEntity] into [Reaction] Reaction toReaction({User? user}) => Reaction( - extraData: extraData ?? {}, type: type, - createdAt: createdAt, userId: userId, user: user, messageId: messageId, score: score, + emojiCode: emojiCode, + createdAt: createdAt, + updatedAt: updatedAt, + extraData: extraData ?? {}, ); } @@ -19,11 +21,13 @@ extension ReactionEntityX on ReactionEntity { extension ReactionX on Reaction { /// Maps a [Reaction] into [ReactionEntity] ReactionEntity toEntity() => ReactionEntity( - extraData: extraData, type: type, - createdAt: createdAt, - userId: userId!, - messageId: messageId!, + userId: userId ?? user?.id, + messageId: messageId, score: score, + emojiCode: emojiCode, + createdAt: createdAt, + updatedAt: updatedAt, + extraData: extraData, ); } diff --git a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart index f136a9fe50..93ff648f1a 100644 --- a/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart +++ b/packages/stream_chat_persistence/lib/src/stream_chat_persistence_client.dart @@ -224,6 +224,20 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { ); } + @override + Future> getLocationsByCid(String cid) async { + assert(_debugIsConnected, ''); + _logger.info('getLocationsByCid'); + return db!.locationDao.getLocationsByCid(cid); + } + + @override + Future getLocationByMessageId(String messageId) async { + assert(_debugIsConnected, ''); + _logger.info('getLocationByMessageId'); + return db!.locationDao.getLocationByMessageId(messageId); + } + @override Future> getReadsByCid(String cid) async { assert(_debugIsConnected, ''); @@ -394,6 +408,13 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { return db!.userDao.updateUsers(users); } + @override + Future updateLocations(List locations) async { + assert(_debugIsConnected, ''); + _logger.info('updateLocations'); + return db!.locationDao.updateLocations(locations); + } + @override Future deletePinnedMessageReactionsByMessageId( List messageIds, @@ -444,6 +465,20 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { ); } + @override + Future deleteLocationsByCid(String cid) { + assert(_debugIsConnected, ''); + _logger.info('deleteLocationsByCid'); + return db!.locationDao.deleteLocationsByCid(cid); + } + + @override + Future deleteLocationsByMessageIds(List messageIds) { + assert(_debugIsConnected, ''); + _logger.info('deleteLocationsByMessageIds'); + return db!.locationDao.deleteLocationsByMessageIds(messageIds); + } + @override Future updateChannelThreads( String cid, diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index 6cbdf386df..1a98c140b6 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_persistence homepage: https://github.com/GetStream/stream-chat-flutter description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. -version: 9.16.0 +version: 10.0.0-beta.5 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -30,7 +30,7 @@ dependencies: path: ^1.8.3 path_provider: ^2.1.3 sqlite3_flutter_libs: ^0.5.26 - stream_chat: ^9.16.0 + stream_chat: ^10.0.0-beta.5 dev_dependencies: build_runner: ^2.4.9 diff --git a/packages/stream_chat_persistence/test/mock_chat_database.dart b/packages/stream_chat_persistence/test/mock_chat_database.dart index 6f1f61af0d..cb0f06a541 100644 --- a/packages/stream_chat_persistence/test/mock_chat_database.dart +++ b/packages/stream_chat_persistence/test/mock_chat_database.dart @@ -63,6 +63,10 @@ class MockChatDatabase extends Mock implements DriftChatDatabase { _draftMessageDao ??= MockDraftMessageDao(); DraftMessageDao? _draftMessageDao; + @override + LocationDao get locationDao => _locationDao ??= MockLocationDao(); + LocationDao? _locationDao; + @override Future flush() => Future.value(); @@ -96,3 +100,5 @@ class MockPollDao extends Mock implements PollDao {} class MockPollVoteDao extends Mock implements PollVoteDao {} class MockDraftMessageDao extends Mock implements DraftMessageDao {} + +class MockLocationDao extends Mock implements LocationDao {} diff --git a/packages/stream_chat_persistence/test/src/dao/location_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/location_dao_test.dart new file mode 100644 index 0000000000..2311ac8d2d --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/location_dao_test.dart @@ -0,0 +1,318 @@ +// ignore_for_file: avoid_redundant_argument_values + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/dao/dao.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; + +import '../../stream_chat_persistence_client_test.dart'; + +void main() { + late LocationDao locationDao; + late DriftChatDatabase database; + + setUp(() { + database = testDatabaseProvider('testUserId'); + locationDao = database.locationDao; + }); + + Future> _prepareLocationData({ + required String cid, + int count = 3, + }) async { + final channels = [ChannelModel(cid: cid)]; + final users = List.generate(count, (index) => User(id: 'testUserId$index')); + final messages = List.generate( + count, + (index) => Message( + id: 'testMessageId$cid$index', + type: 'testType', + user: users[index], + createdAt: DateTime.now(), + text: 'Test message #$index', + ), + ); + + final locations = List.generate( + count, + (index) => Location( + channelCid: cid, + messageId: messages[index].id, + userId: users[index].id, + latitude: 37.7749 + index * 0.001, // San Francisco area + longitude: -122.4194 + index * 0.001, + createdByDeviceId: 'testDevice$index', + endAt: index.isEven + ? DateTime.now().add(const Duration(hours: 1)) + : null, // Some live, some static + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ), + ); + + await database.userDao.updateUsers(users); + await database.channelDao.updateChannels(channels); + await database.messageDao.updateMessages(cid, messages); + await locationDao.updateLocations(locations); + + return locations; + } + + test('getLocationsByCid', () async { + const cid = 'test:Cid'; + + // Should be empty initially + final locations = await locationDao.getLocationsByCid(cid); + expect(locations, isEmpty); + + // Adding sample locations + final insertedLocations = await _prepareLocationData(cid: cid); + expect(insertedLocations, isNotEmpty); + + // Fetched locations length should match inserted locations length + final fetchedLocations = await locationDao.getLocationsByCid(cid); + expect(fetchedLocations.length, insertedLocations.length); + + // Every location channelCid should match the provided cid + expect(fetchedLocations.every((it) => it.channelCid == cid), true); + }); + + test('updateLocations', () async { + const cid = 'test:Cid'; + + // Preparing test data + final insertedLocations = await _prepareLocationData(cid: cid); + + // Adding a new location + final newUser = User(id: 'newUserId'); + final newMessage = Message( + id: 'newMessageId', + type: 'testType', + user: newUser, + createdAt: DateTime.now(), + text: 'New test message', + ); + final newLocation = Location( + channelCid: cid, + messageId: newMessage.id, + userId: newUser.id, + latitude: 40.7128, // New York + longitude: -74.0060, + createdByDeviceId: 'newDevice', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); + + await database.userDao.updateUsers([newUser]); + await database.messageDao.updateMessages(cid, [newMessage]); + await locationDao.updateLocations([newLocation]); + + // Fetched locations length should be one more than inserted locations + // Fetched locations should contain the newLocation + final fetchedLocations = await locationDao.getLocationsByCid(cid); + expect(fetchedLocations.length, insertedLocations.length + 1); + expect( + fetchedLocations.any((it) => + it.messageId == newLocation.messageId && + it.latitude == newLocation.latitude && + it.longitude == newLocation.longitude), + isTrue, + ); + }); + + test('getLocationByMessageId', () async { + const cid = 'test:Cid'; + + // Preparing test data + final insertedLocations = await _prepareLocationData(cid: cid); + + // Fetched location should not be null + final locationToFetch = insertedLocations.first; + final fetchedLocation = + await locationDao.getLocationByMessageId(locationToFetch.messageId!); + expect(fetchedLocation, isNotNull); + expect(fetchedLocation!.messageId, locationToFetch.messageId); + expect(fetchedLocation.latitude, locationToFetch.latitude); + expect(fetchedLocation.longitude, locationToFetch.longitude); + }); + + test( + 'getLocationByMessageId should return null for non-existent messageId', + () async { + // Should return null for non-existent messageId + final fetchedLocation = + await locationDao.getLocationByMessageId('nonExistentMessageId'); + expect(fetchedLocation, isNull); + }, + ); + + test('deleteLocationsByCid', () async { + const cid = 'test:Cid'; + + // Preparing test data + final insertedLocations = await _prepareLocationData(cid: cid); + + // Verify locations exist + final locationsBeforeDelete = await locationDao.getLocationsByCid(cid); + expect(locationsBeforeDelete.length, insertedLocations.length); + + // Deleting all locations for the channel + await locationDao.deleteLocationsByCid(cid); + + // Fetched location list should be empty + final fetchedLocations = await locationDao.getLocationsByCid(cid); + expect(fetchedLocations, isEmpty); + }); + + test('deleteLocationsByMessageIds', () async { + const cid = 'test:Cid'; + + // Preparing test data + final insertedLocations = await _prepareLocationData(cid: cid); + + // Deleting the first two locations by their message IDs + final messageIdsToDelete = + insertedLocations.take(2).map((it) => it.messageId!).toList(); + await locationDao.deleteLocationsByMessageIds(messageIdsToDelete); + + // Fetched location list should be one less than inserted locations + final fetchedLocations = await locationDao.getLocationsByCid(cid); + expect(fetchedLocations.length, + insertedLocations.length - messageIdsToDelete.length); + + // Deleted locations should not exist in fetched locations + expect( + fetchedLocations.any((it) => messageIdsToDelete.contains(it.messageId)), + isFalse, + ); + }); + + group('deleteLocationsByMessageIds', () { + test('should delete locations for specific message IDs only', () async { + const cid1 = 'test:Cid1'; + const cid2 = 'test:Cid2'; + + // Preparing test data for two channels + final insertedLocations1 = + await _prepareLocationData(cid: cid1, count: 2); + final insertedLocations2 = + await _prepareLocationData(cid: cid2, count: 2); + + // Verify all locations exist + final locations1 = await locationDao.getLocationsByCid(cid1); + final locations2 = await locationDao.getLocationsByCid(cid2); + expect(locations1.length, insertedLocations1.length); + expect(locations2.length, insertedLocations2.length); + + // Delete only locations from the first channel + final messageIdsToDelete = + insertedLocations1.map((it) => it.messageId!).toList(); + await locationDao.deleteLocationsByMessageIds(messageIdsToDelete); + + // Only locations from cid1 should be deleted + final fetchedLocations1 = await locationDao.getLocationsByCid(cid1); + final fetchedLocations2 = await locationDao.getLocationsByCid(cid2); + expect(fetchedLocations1, isEmpty); + expect(fetchedLocations2.length, insertedLocations2.length); + }); + }); + + group('Location entity references', () { + test( + 'should delete locations when referenced channel is deleted', + () async { + const cid = 'test:channelRefCascade'; + + // Prepare test data + await _prepareLocationData(cid: cid, count: 2); + + // Verify locations exist before channel deletion + final locationsBeforeDelete = await locationDao.getLocationsByCid(cid); + expect(locationsBeforeDelete, isNotEmpty); + expect(locationsBeforeDelete.length, 2); + + // Delete the channel + await database.channelDao.deleteChannelByCids([cid]); + + // Verify locations have been deleted (cascade) + final locationsAfterDelete = await locationDao.getLocationsByCid(cid); + expect(locationsAfterDelete, isEmpty); + }, + ); + + test( + 'should delete locations when referenced message is deleted', + () async { + const cid = 'test:messageRefCascade'; + + // Prepare test data + final insertedLocations = + await _prepareLocationData(cid: cid, count: 3); + final messageToDelete = insertedLocations.first.messageId!; + + // Verify location exists before message deletion + final locationBeforeDelete = + await locationDao.getLocationByMessageId(messageToDelete); + expect(locationBeforeDelete, isNotNull); + expect(locationBeforeDelete!.messageId, messageToDelete); + + // Verify all locations exist + final allLocationsBeforeDelete = + await locationDao.getLocationsByCid(cid); + expect(allLocationsBeforeDelete.length, 3); + + // Delete the message + await database.messageDao.deleteMessageByIds([messageToDelete]); + + // Verify the specific location has been deleted (cascade) + final locationAfterDelete = + await locationDao.getLocationByMessageId(messageToDelete); + expect(locationAfterDelete, isNull); + + // Verify other locations still exist + final allLocationsAfterDelete = + await locationDao.getLocationsByCid(cid); + expect(allLocationsAfterDelete.length, 2); + expect( + allLocationsAfterDelete.any((it) => it.messageId == messageToDelete), + isFalse, + ); + }, + ); + + test( + 'should delete all locations when multiple messages are deleted', + () async { + const cid = 'test:multipleMessageRefCascade'; + + // Prepare test data + final insertedLocations = + await _prepareLocationData(cid: cid, count: 3); + final messageIdsToDelete = + insertedLocations.take(2).map((it) => it.messageId!).toList(); + + // Verify locations exist before message deletion + final allLocationsBeforeDelete = + await locationDao.getLocationsByCid(cid); + expect(allLocationsBeforeDelete.length, 3); + + // Delete multiple messages + await database.messageDao.deleteMessageByIds(messageIdsToDelete); + + // Verify corresponding locations have been deleted (cascade) + final allLocationsAfterDelete = + await locationDao.getLocationsByCid(cid); + expect(allLocationsAfterDelete.length, 1); + expect( + allLocationsAfterDelete + .any((it) => messageIdsToDelete.contains(it.messageId)), + isFalse, + ); + }, + ); + }); + + tearDown(() async { + await database.disconnect(); + }); +} diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart index 44abc4a644..57482c4317 100644 --- a/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart @@ -6,6 +6,7 @@ import 'package:stream_chat_persistence/src/dao/pinned_message_reaction_dao.dart import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import '../../stream_chat_persistence_client_test.dart'; +import '../utils/date_matcher.dart'; void main() { late PinnedMessageReactionDao pinnedMessageReactionDao; @@ -39,16 +40,23 @@ void main() { pinnedAt: DateTime.now(), pinnedBy: users.first, ); + + final now = DateTime.now(); final reactions = List.generate( count, - (index) => Reaction( - type: 'testType$index', - createdAt: DateTime.now(), - userId: userId ?? users[index].id, - messageId: message.id, - score: count + 3, - extraData: {'extra_test_field': 'extraTestData'}, - ), + (index) { + final createdAt = now.add(Duration(minutes: index)); + return Reaction( + type: 'testType$index', + createdAt: createdAt, + updatedAt: createdAt.add(const Duration(minutes: 5)), + userId: userId ?? users[index].id, + messageId: message.id, + score: count + 3, + emojiCode: '๐Ÿ˜‚$index', + extraData: const {'extra_test_field': 'extraTestData'}, + ); + }, ); await database.userDao.updateUsers(users); @@ -76,6 +84,14 @@ void main() { await pinnedMessageReactionDao.getReactions(messageId); expect(fetchedReactions.length, insertedReactions.length); expect(fetchedReactions.every((it) => it.messageId == messageId), true); + + // Verify score and emojiCode are preserved + for (var i = 0; i < fetchedReactions.length; i++) { + final inserted = insertedReactions[i]; + final fetched = fetchedReactions[i]; + expect(fetched.score, inserted.score); + expect(fetched.emojiCode, inserted.emojiCode); + } }); test('getReactionsByUserId', () async { @@ -100,6 +116,14 @@ void main() { expect(fetchedReactions.length, insertedReactions.length); expect(fetchedReactions.every((it) => it.messageId == messageId), true); expect(fetchedReactions.every((it) => it.userId == userId), true); + + // Verify score and emojiCode are preserved + for (var i = 0; i < fetchedReactions.length; i++) { + final inserted = insertedReactions[i]; + final fetched = fetchedReactions[i]; + expect(fetched.score, inserted.score); + expect(fetched.emojiCode, inserted.emojiCode); + } }); test('updateReactions', () async { @@ -109,37 +133,46 @@ void main() { final reactions = await _prepareReactionData(messageId); // Modifying one of the reaction and also adding one new - final copyReaction = reactions.first.copyWith(score: 33); + final now = DateTime.now(); + final copyReaction = reactions.first.copyWith( + score: 33, + emojiCode: '๐ŸŽ‰', + updatedAt: now, + ); final newReaction = Reaction( type: 'testType3', - createdAt: DateTime.now(), + createdAt: now, + updatedAt: now.add(const Duration(minutes: 5)), userId: 'testUserId3', messageId: messageId, score: 30, - extraData: {'extra_test_field': 'extraTestData'}, + emojiCode: '๐ŸŽˆ', + extraData: const {'extra_test_field': 'extraTestData'}, ); await pinnedMessageReactionDao.updateReactions([copyReaction, newReaction]); // Fetched reaction length should be one more than inserted reactions. - // copyReaction `score` modified field should be 33. + // copyReaction modified fields should match // Fetched reactions should contain the newReaction. final fetchedReactions = await pinnedMessageReactionDao.getReactions(messageId); expect(fetchedReactions.length, reactions.length + 1); - expect( - fetchedReactions - .firstWhere((it) => - it.userId == copyReaction.userId && it.type == copyReaction.type) - .score, - 33, + + final fetchedCopyReaction = fetchedReactions.firstWhere( + (it) => it.userId == copyReaction.userId && it.type == copyReaction.type, ); + expect(fetchedCopyReaction.score, 33); + expect(fetchedCopyReaction.emojiCode, '๐ŸŽ‰'); + expect(fetchedCopyReaction.updatedAt, isSameDateAs(now)); + + final fetchedNewReaction = fetchedReactions.firstWhere( + (it) => it.userId == newReaction.userId && it.type == newReaction.type, + ); + expect(fetchedNewReaction.emojiCode, '๐ŸŽˆ'); expect( - fetchedReactions - .where((it) => - it.userId == newReaction.userId && it.type == newReaction.type) - .isNotEmpty, - true, + fetchedNewReaction.updatedAt, + isSameDateAs(now.add(const Duration(minutes: 5))), ); }); @@ -171,7 +204,8 @@ void main() { expect(fetchedReactions1, isEmpty); expect(fetchedReactions2, isNotEmpty); }); - test('should delete all the messages of both message', () async { + + test('should delete all the reactions of both message', () async { // Preparing test data final insertedReactions1 = await _prepareReactionData(messageId1); final insertedReactions2 = await _prepareReactionData(messageId2); diff --git a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart index 7fa3569e4e..ddd01f09ff 100644 --- a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart @@ -6,6 +6,7 @@ import 'package:stream_chat_persistence/src/dao/reaction_dao.dart'; import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; import '../../stream_chat_persistence_client_test.dart'; +import '../utils/date_matcher.dart'; void main() { late ReactionDao reactionDao; @@ -39,16 +40,23 @@ void main() { pinnedAt: DateTime.now(), pinnedBy: users.first, ); + + final now = DateTime.now(); final reactions = List.generate( count, - (index) => Reaction( - type: 'testType$index', - createdAt: DateTime.now(), - userId: userId ?? users[index].id, - messageId: message.id, - score: count + 3, - extraData: {'extra_test_field': 'extraTestData'}, - ), + (index) { + final createdAt = now.add(Duration(minutes: index)); + return Reaction( + type: 'testType$index', + createdAt: createdAt, + updatedAt: createdAt.add(const Duration(minutes: 5)), + userId: userId ?? users[index].id, + messageId: message.id, + score: count + 3, + emojiCode: '๐Ÿ˜‚$index', + extraData: const {'extra_test_field': 'extraTestData'}, + ); + }, ); await database.userDao.updateUsers(users); @@ -75,6 +83,14 @@ void main() { final fetchedReactions = await reactionDao.getReactions(messageId); expect(fetchedReactions.length, insertedReactions.length); expect(fetchedReactions.every((it) => it.messageId == messageId), true); + + // Verify score and emojiCode are preserved + for (var i = 0; i < fetchedReactions.length; i++) { + final inserted = insertedReactions[i]; + final fetched = fetchedReactions[i]; + expect(fetched.score, inserted.score); + expect(fetched.emojiCode, inserted.emojiCode); + } }); test('getReactionsByUserId', () async { @@ -98,6 +114,14 @@ void main() { expect(fetchedReactions.length, insertedReactions.length); expect(fetchedReactions.every((it) => it.messageId == messageId), true); expect(fetchedReactions.every((it) => it.userId == userId), true); + + // Verify score and emojiCode are preserved + for (var i = 0; i < fetchedReactions.length; i++) { + final inserted = insertedReactions[i]; + final fetched = fetchedReactions[i]; + expect(fetched.score, inserted.score); + expect(fetched.emojiCode, inserted.emojiCode); + } }); test('updateReactions', () async { @@ -107,36 +131,45 @@ void main() { final reactions = await _prepareReactionData(messageId); // Modifying one of the reaction and also adding one new - final copyReaction = reactions.first.copyWith(score: 33); + final now = DateTime.now(); + final copyReaction = reactions.first.copyWith( + score: 33, + emojiCode: '๐ŸŽ‰', + updatedAt: now, + ); final newReaction = Reaction( type: 'testType3', - createdAt: DateTime.now(), + createdAt: now, + updatedAt: now.add(const Duration(minutes: 5)), userId: 'testUserId3', messageId: messageId, score: 30, - extraData: {'extra_test_field': 'extraTestData'}, + emojiCode: '๐ŸŽˆ', + extraData: const {'extra_test_field': 'extraTestData'}, ); await reactionDao.updateReactions([copyReaction, newReaction]); // Fetched reaction length should be one more than inserted reactions. - // copyReaction `score` modified field should be 33. + // copyReaction modified fields should match // Fetched reactions should contain the newReaction. final fetchedReactions = await reactionDao.getReactions(messageId); expect(fetchedReactions.length, reactions.length + 1); - expect( - fetchedReactions - .firstWhere((it) => - it.userId == copyReaction.userId && it.type == copyReaction.type) - .score, - 33, + + final fetchedCopyReaction = fetchedReactions.firstWhere( + (it) => it.userId == copyReaction.userId && it.type == copyReaction.type, ); + expect(fetchedCopyReaction.score, 33); + expect(fetchedCopyReaction.emojiCode, '๐ŸŽ‰'); + expect(fetchedCopyReaction.updatedAt, isSameDateAs(now)); + + final fetchedNewReaction = fetchedReactions.firstWhere( + (it) => it.userId == newReaction.userId && it.type == newReaction.type, + ); + expect(fetchedNewReaction.emojiCode, '๐ŸŽˆ'); expect( - fetchedReactions - .where((it) => - it.userId == newReaction.userId && it.type == newReaction.type) - .isNotEmpty, - true, + fetchedNewReaction.updatedAt, + isSameDateAs(now.add(const Duration(minutes: 5))), ); }); @@ -164,6 +197,7 @@ void main() { expect(fetchedReactions1, isEmpty); expect(fetchedReactions2, isNotEmpty); }); + test('should delete all the reactions of both message', () async { // Preparing test data final insertedReactions1 = await _prepareReactionData(messageId1); diff --git a/packages/stream_chat_persistence/test/src/mapper/location_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/location_mapper_test.dart new file mode 100644 index 0000000000..622a76dcb2 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/mapper/location_mapper_test.dart @@ -0,0 +1,141 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; +import 'package:stream_chat_persistence/src/mapper/location_mapper.dart'; + +void main() { + group('LocationMapper', () { + test('toLocation should map the entity into Location', () { + final createdAt = DateTime.now(); + final updatedAt = DateTime.now(); + final endAt = DateTime.now().add(const Duration(hours: 1)); + + final entity = LocationEntity( + channelCid: 'testCid', + userId: 'testUserId', + messageId: 'testMessageId', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'testDeviceId', + endAt: endAt, + createdAt: createdAt, + updatedAt: updatedAt, + ); + + final location = entity.toLocation(); + + expect(location, isA()); + expect(location.channelCid, entity.channelCid); + expect(location.userId, entity.userId); + expect(location.messageId, entity.messageId); + expect(location.latitude, entity.latitude); + expect(location.longitude, entity.longitude); + expect(location.createdByDeviceId, entity.createdByDeviceId); + expect(location.endAt, entity.endAt); + expect(location.createdAt, entity.createdAt); + expect(location.updatedAt, entity.updatedAt); + }); + + test('toEntity should map the Location into LocationEntity', () { + final createdAt = DateTime.now(); + final updatedAt = DateTime.now(); + final endAt = DateTime.now().add(const Duration(hours: 1)); + + final location = Location( + channelCid: 'testCid', + userId: 'testUserId', + messageId: 'testMessageId', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'testDeviceId', + endAt: endAt, + createdAt: createdAt, + updatedAt: updatedAt, + ); + + final entity = location.toEntity(); + + expect(entity, isA()); + expect(entity.channelCid, location.channelCid); + expect(entity.userId, location.userId); + expect(entity.messageId, location.messageId); + expect(entity.latitude, location.latitude); + expect(entity.longitude, location.longitude); + expect(entity.createdByDeviceId, location.createdByDeviceId); + expect(entity.endAt, location.endAt); + expect(entity.createdAt, location.createdAt); + expect(entity.updatedAt, location.updatedAt); + }); + + test('roundtrip conversion should preserve data', () { + final createdAt = DateTime.now(); + final updatedAt = DateTime.now(); + final endAt = DateTime.now().add(const Duration(hours: 1)); + + final originalLocation = Location( + channelCid: 'testCid', + userId: 'testUserId', + messageId: 'testMessageId', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'testDeviceId', + endAt: endAt, + createdAt: createdAt, + updatedAt: updatedAt, + ); + + final entity = originalLocation.toEntity(); + final convertedLocation = entity.toLocation(); + + expect(convertedLocation.channelCid, originalLocation.channelCid); + expect(convertedLocation.userId, originalLocation.userId); + expect(convertedLocation.messageId, originalLocation.messageId); + expect(convertedLocation.latitude, originalLocation.latitude); + expect(convertedLocation.longitude, originalLocation.longitude); + expect(convertedLocation.createdByDeviceId, + originalLocation.createdByDeviceId); + expect(convertedLocation.endAt, originalLocation.endAt); + expect(convertedLocation.createdAt, originalLocation.createdAt); + expect(convertedLocation.updatedAt, originalLocation.updatedAt); + }); + + test('should handle live location conversion', () { + final endAt = DateTime.now().add(const Duration(hours: 1)); + final location = Location( + channelCid: 'testCid', + userId: 'testUserId', + messageId: 'testMessageId', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'testDeviceId', + endAt: endAt, + ); + + final entity = location.toEntity(); + final convertedLocation = entity.toLocation(); + + expect(convertedLocation.isLive, isTrue); + expect(convertedLocation.isStatic, isFalse); + expect(convertedLocation.endAt, endAt); + }); + + test('should handle static location conversion', () { + final location = Location( + channelCid: 'testCid', + userId: 'testUserId', + messageId: 'testMessageId', + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'testDeviceId', + // No endAt = static location + ); + + final entity = location.toEntity(); + final convertedLocation = entity.toLocation(); + + expect(convertedLocation.isLive, isFalse); + expect(convertedLocation.isStatic, isTrue); + expect(convertedLocation.endAt, isNull); + }); + }); +} diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart index 2753f4f4ce..d2b4a67094 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart @@ -9,12 +9,15 @@ void main() { test('toReaction should map the entity into Reaction', () { final user = User(id: 'testUserId'); final message = Message(id: 'testMessageId'); + final now = DateTime.now(); final entity = PinnedMessageReactionEntity( userId: user.id, messageId: message.id, type: 'haha', score: 33, - createdAt: DateTime.now(), + emojiCode: '๐Ÿ˜‚', + createdAt: now, + updatedAt: now.add(const Duration(minutes: 5)), extraData: {'extra_test_data': 'extraData'}, ); @@ -24,20 +27,25 @@ void main() { expect(reaction.messageId, entity.messageId); expect(reaction.type, entity.type); expect(reaction.score, entity.score); + expect(reaction.emojiCode, entity.emojiCode); expect(reaction.createdAt, isSameDateAs(entity.createdAt)); + expect(reaction.updatedAt, isSameDateAs(entity.updatedAt)); expect(reaction.extraData, entity.extraData); }); test('toEntity should map reaction into PinnedMessageReactionEntity', () { final user = User(id: 'testUserId'); final message = Message(id: 'testMessageId'); + final now = DateTime.now(); final reaction = Reaction( userId: user.id, messageId: message.id, type: 'haha', score: 33, - createdAt: DateTime.now(), - extraData: {'extra_test_data': 'extraData'}, + emojiCode: '๐Ÿ˜‚', + createdAt: now, + updatedAt: now.add(const Duration(minutes: 5)), + extraData: const {'extra_test_data': 'extraData'}, ); final entity = reaction.toPinnedEntity(); @@ -46,7 +54,9 @@ void main() { expect(entity.messageId, reaction.messageId); expect(entity.type, reaction.type); expect(entity.score, reaction.score); + expect(entity.emojiCode, reaction.emojiCode); expect(entity.createdAt, isSameDateAs(reaction.createdAt)); + expect(entity.updatedAt, isSameDateAs(reaction.updatedAt)); expect(entity.extraData, reaction.extraData); }); } diff --git a/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart index 844eb3b86b..43a3b93cfb 100644 --- a/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart @@ -9,12 +9,15 @@ void main() { test('toReaction should map the entity into Reaction', () { final user = User(id: 'testUserId'); final message = Message(id: 'testMessageId'); + final now = DateTime.now(); final entity = ReactionEntity( userId: user.id, messageId: message.id, type: 'haha', score: 33, - createdAt: DateTime.now(), + emojiCode: '๐Ÿ˜‚', + createdAt: now, + updatedAt: now.add(const Duration(minutes: 5)), extraData: {'extra_test_data': 'extraData'}, ); @@ -24,20 +27,25 @@ void main() { expect(reaction.messageId, entity.messageId); expect(reaction.type, entity.type); expect(reaction.score, entity.score); + expect(reaction.emojiCode, entity.emojiCode); expect(reaction.createdAt, isSameDateAs(entity.createdAt)); + expect(reaction.updatedAt, isSameDateAs(entity.updatedAt)); expect(reaction.extraData, entity.extraData); }); test('toEntity should map reaction into ReactionEntity', () { final user = User(id: 'testUserId'); final message = Message(id: 'testMessageId'); + final now = DateTime.now(); final reaction = Reaction( userId: user.id, messageId: message.id, type: 'haha', score: 33, - createdAt: DateTime.now(), - extraData: {'extra_test_data': 'extraData'}, + emojiCode: '๐Ÿ˜‚', + createdAt: now, + updatedAt: now.add(const Duration(minutes: 5)), + extraData: const {'extra_test_data': 'extraData'}, ); final entity = reaction.toEntity(); @@ -46,7 +54,9 @@ void main() { expect(entity.messageId, reaction.messageId); expect(entity.type, reaction.type); expect(entity.score, reaction.score); + expect(entity.emojiCode, reaction.emojiCode); expect(entity.createdAt, isSameDateAs(reaction.createdAt)); + expect(entity.updatedAt, isSameDateAs(reaction.updatedAt)); expect(entity.extraData, reaction.extraData); }); } diff --git a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart index 3b0cb7314e..566e6f0313 100644 --- a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -699,6 +699,98 @@ void main() { .deleteDraftMessageByCid(cid, parentId: parentId)).called(1); }); + test('getLocationsByCid', () async { + const cid = 'testCid'; + final locations = List.generate( + 3, + (index) => Location( + channelCid: cid, + messageId: 'testMessageId$index', + userId: 'testUserId$index', + latitude: 37.7749 + index * 0.001, + longitude: -122.4194 + index * 0.001, + createdByDeviceId: 'testDevice$index', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ), + ); + + when(() => mockDatabase.locationDao.getLocationsByCid(cid)) + .thenAnswer((_) async => locations); + + final fetchedLocations = await client.getLocationsByCid(cid); + expect(fetchedLocations.length, locations.length); + verify(() => mockDatabase.locationDao.getLocationsByCid(cid)).called(1); + }); + + test('getLocationByMessageId', () async { + const messageId = 'testMessageId'; + final location = Location( + channelCid: 'testCid', + messageId: messageId, + userId: 'testUserId', + latitude: 37.7749, + longitude: -122.4194, + createdByDeviceId: 'testDevice', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); + + when(() => mockDatabase.locationDao.getLocationByMessageId(messageId)) + .thenAnswer((_) async => location); + + final fetchedLocation = await client.getLocationByMessageId(messageId); + expect(fetchedLocation, isNotNull); + expect(fetchedLocation!.messageId, messageId); + verify(() => mockDatabase.locationDao.getLocationByMessageId(messageId)) + .called(1); + }); + + test('updateLocations', () async { + final locations = List.generate( + 3, + (index) => Location( + channelCid: 'testCid$index', + messageId: 'testMessageId$index', + userId: 'testUserId$index', + latitude: 37.7749 + index * 0.001, + longitude: -122.4194 + index * 0.001, + createdByDeviceId: 'testDevice$index', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ), + ); + + when(() => mockDatabase.locationDao.updateLocations(locations)) + .thenAnswer((_) async {}); + + await client.updateLocations(locations); + verify(() => mockDatabase.locationDao.updateLocations(locations)) + .called(1); + }); + + test('deleteLocationsByCid', () async { + const cid = 'testCid'; + when(() => mockDatabase.locationDao.deleteLocationsByCid(cid)) + .thenAnswer((_) async {}); + + await client.deleteLocationsByCid(cid); + verify(() => mockDatabase.locationDao.deleteLocationsByCid(cid)) + .called(1); + }); + + test('deleteLocationsByMessageIds', () async { + final messageIds = ['testMessageId1', 'testMessageId2']; + when( + () => mockDatabase.locationDao.deleteLocationsByMessageIds(messageIds), + ).thenAnswer((_) async {}); + + await client.deleteLocationsByMessageIds(messageIds); + verify( + () => mockDatabase.locationDao.deleteLocationsByMessageIds(messageIds), + ).called(1); + }); + tearDown(() async { await client.disconnect(flush: true); }); diff --git a/sample_app/android/app/src/main/AndroidManifest.xml b/sample_app/android/app/src/main/AndroidManifest.xml index 8ff8f0abb5..6fb866b633 100644 --- a/sample_app/android/app/src/main/AndroidManifest.xml +++ b/sample_app/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,13 @@ + + + + + + + ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAppleMusicUsageDescription Used to send message attachments NSCameraUsageDescription Used to send message attachments + NSLocationWhenInUseUsageDescription + We need access to your location to share it in the chat. + NSLocationAlwaysUsageDescription + We need access to your location to share it in the chat. NSMicrophoneUsageDescription Used to send message attachments NSPhotoLibraryUsageDescription @@ -43,6 +54,7 @@ fetch remote-notification + location UILaunchStoryboardName LaunchScreen @@ -65,12 +77,5 @@ UIViewControllerBasedStatusBarAppearance - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - ITSAppUsesNonExemptEncryption - diff --git a/sample_app/lib/app.dart b/sample_app/lib/app.dart index 7fc8ba93b8..59b097cf55 100644 --- a/sample_app/lib/app.dart +++ b/sample_app/lib/app.dart @@ -441,6 +441,8 @@ class _StreamChatSampleAppState extends State final GlobalKey _navigatorKey = GlobalKey(); LocalNotificationObserver? localNotificationObserver; + GoRouter? router; + /// Conditionally sets up the router and adding an observer for the /// current chat client. GoRouter _setupRouter() { @@ -450,7 +452,7 @@ class _StreamChatSampleAppState extends State localNotificationObserver = LocalNotificationObserver( _initNotifier.initData!.client, _navigatorKey); - return GoRouter( + return router ??= GoRouter( refreshListenable: _initNotifier, initialLocation: Routes.CHANNEL_LIST_PAGE.path, navigatorKey: _navigatorKey, diff --git a/sample_app/lib/pages/channel_file_display_screen.dart b/sample_app/lib/pages/channel_file_display_screen.dart index 727dd6bb87..a3228ca68f 100644 --- a/sample_app/lib/pages/channel_file_display_screen.dart +++ b/sample_app/lib/pages/channel_file_display_screen.dart @@ -31,10 +31,7 @@ class _ChannelFileDisplayScreenState extends State { const ['file'], ), sort: [ - const SortOption( - 'created_at', - direction: SortOption.ASC, - ), + const SortOption.asc('created_at'), ], limit: 20, ); diff --git a/sample_app/lib/pages/channel_list_page.dart b/sample_app/lib/pages/channel_list_page.dart index b756e231f6..8355dd0413 100644 --- a/sample_app/lib/pages/channel_list_page.dart +++ b/sample_app/lib/pages/channel_list_page.dart @@ -13,6 +13,7 @@ import 'package:sample_app/pages/thread_list_page.dart'; import 'package:sample_app/pages/user_mentions_page.dart'; import 'package:sample_app/routes/routes.dart'; import 'package:sample_app/utils/localizations.dart'; +import 'package:sample_app/utils/shared_location_service.dart'; import 'package:sample_app/widgets/channel_list.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; @@ -43,7 +44,7 @@ class _ChannelListPageState extends State { ? StreamChatTheme.of(context).colorTheme.textHighEmphasis : Colors.grey, ), - PositionedDirectional( + const PositionedDirectional( top: -4, start: 12, child: StreamUnreadIndicator(), @@ -101,6 +102,10 @@ class _ChannelListPageState extends State { ]; } + late final _locationService = SharedLocationService( + client: StreamChat.of(context).client, + ); + @override Widget build(BuildContext context) { final user = StreamChat.of(context).currentUser; @@ -152,6 +157,7 @@ class _ChannelListPageState extends State { @override void initState() { + super.initState(); if (!kIsWeb) { badgeListener = StreamChat.of(context) .client @@ -165,12 +171,14 @@ class _ChannelListPageState extends State { } }); } - super.initState(); + + _locationService.initialize(); } @override void dispose() { badgeListener?.cancel(); + _locationService.dispose(); super.dispose(); } } diff --git a/sample_app/lib/pages/channel_media_display_screen.dart b/sample_app/lib/pages/channel_media_display_screen.dart index d7b7a472d1..2504e3ae00 100644 --- a/sample_app/lib/pages/channel_media_display_screen.dart +++ b/sample_app/lib/pages/channel_media_display_screen.dart @@ -33,10 +33,7 @@ class _ChannelMediaDisplayScreenState extends State { const ['image', 'video'], ), sort: [ - const SortOption( - 'created_at', - direction: SortOption.ASC, - ), + const SortOption.asc('created_at'), ], limit: 20, ); diff --git a/sample_app/lib/pages/channel_page.dart b/sample_app/lib/pages/channel_page.dart index 846aad04d0..988ac96959 100644 --- a/sample_app/lib/pages/channel_page.dart +++ b/sample_app/lib/pages/channel_page.dart @@ -5,6 +5,10 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:sample_app/pages/thread_page.dart'; import 'package:sample_app/routes/routes.dart'; +import 'package:sample_app/widgets/location/location_attachment.dart'; +import 'package:sample_app/widgets/location/location_detail_dialog.dart'; +import 'package:sample_app/widgets/location/location_picker_dialog.dart'; +import 'package:sample_app/widgets/location/location_picker_option.dart'; import 'package:sample_app/widgets/reminder_dialog.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -25,8 +29,7 @@ class ChannelPage extends StatefulWidget { class _ChannelPageState extends State { FocusNode? _focusNode; - final StreamMessageInputController _messageInputController = - StreamMessageInputController(); + final _messageInputController = StreamMessageInputController(); @override void initState() { @@ -53,6 +56,9 @@ class _ChannelPageState extends State { final textTheme = theme.textTheme; final colorTheme = theme.colorTheme; + final channel = StreamChannel.of(context).channel; + final config = channel.config; + return Scaffold( backgroundColor: colorTheme.appBg, appBar: StreamChannelHeader( @@ -124,12 +130,73 @@ class _ChannelPageState extends State { messageInputController: _messageInputController, onQuotedMessageCleared: _messageInputController.clearQuotedMessage, enableVoiceRecording: true, + allowedAttachmentPickerTypes: [ + ...AttachmentPickerType.values, + if (config?.sharedLocations == true && channel.canShareLocation) + const LocationPickerType(), + ], + onCustomAttachmentPickerResult: (result) { + return _onCustomAttachmentPickerResult(channel, result).ignore(); + }, + customAttachmentPickerOptions: [ + TabbedAttachmentPickerOption( + key: 'location-picker', + icon: const Icon(Icons.near_me_rounded), + supportedTypes: [const LocationPickerType()], + isEnabled: (value) { + // Enable if nothing has been selected yet. + if (value.isEmpty) return true; + + // Otherwise, enable only if there is a location. + return value.extraData['location'] != null; + }, + optionViewBuilder: (context, controller) => LocationPicker( + onLocationPicked: (locationResult) { + if (locationResult == null) return Navigator.pop(context); + + controller.extraData = { + ...controller.value.extraData, + 'location': locationResult, + }; + + final result = LocationPicked(location: locationResult); + return Navigator.pop(context, result); + }, + ), + ), + ], ), ], ), ); } + Future _onCustomAttachmentPickerResult( + Channel channel, + CustomAttachmentPickerResult result, + ) async { + final response = switch (result) { + LocationPicked() => _onShareLocationPicked(channel, result.location), + _ => null, + }; + + return response?.ignore(); + } + + Future _onShareLocationPicked( + Channel channel, + LocationPickerResult result, + ) async { + if (result.endSharingAt case final endSharingAt?) { + return channel.startLiveLocationSharing( + endSharingAt: endSharingAt, + location: result.coordinates, + ); + } + + return channel.sendStaticLocation(location: result.coordinates); + } + Widget customMessageBuilder( BuildContext context, MessageDetails details, @@ -142,7 +209,8 @@ class _ChannelPageState extends State { final message = details.message; final reminder = message.reminder; - final channelConfig = StreamChannel.of(context).channel.config; + final channel = StreamChannel.of(context).channel; + final channelConfig = channel.config; final customOptions = [ if (channelConfig?.userMessageReminders == true) ...[ @@ -153,23 +221,7 @@ class _ChannelPageState extends State { color: colorTheme.textLowEmphasis, ), title: const Text('Edit Reminder'), - onTap: (message) async { - Navigator.of(context).pop(); - - final option = await showDialog( - context: context, - builder: (_) => EditReminderDialog( - isBookmarkReminder: reminder.remindAt == null, - ), - ); - - if (option == null) return; - final client = StreamChat.of(context).client; - final messageId = message.id; - final remindAt = option.remindAt; - - client.updateReminder(messageId, remindAt: remindAt).ignore(); - }, + action: EditReminder(message: message, reminder: reminder), ), StreamMessageAction( leading: StreamSvgIcon( @@ -177,14 +229,7 @@ class _ChannelPageState extends State { color: colorTheme.textLowEmphasis, ), title: const Text('Remove from later'), - onTap: (message) { - Navigator.of(context).pop(); - - final client = StreamChat.of(context).client; - final messageId = message.id; - - client.deleteReminder(messageId).ignore(); - }, + action: RemoveReminder(message: message, reminder: reminder), ), ] else ...[ StreamMessageAction( @@ -193,21 +238,7 @@ class _ChannelPageState extends State { color: colorTheme.textLowEmphasis, ), title: const Text('Remind me'), - onTap: (message) async { - Navigator.of(context).pop(); - - final reminder = await showDialog( - context: context, - builder: (_) => const CreateReminderDialog(), - ); - - if (reminder == null) return; - final client = StreamChat.of(context).client; - final messageId = message.id; - final remindAt = reminder.remindAt; - - client.createReminder(messageId, remindAt: remindAt).ignore(); - }, + action: CreateReminder(message: message), ), StreamMessageAction( leading: Icon( @@ -215,19 +246,19 @@ class _ChannelPageState extends State { color: colorTheme.textLowEmphasis, ), title: const Text('Save for later'), - onTap: (message) { - Navigator.of(context).pop(); - - final client = StreamChat.of(context).client; - final messageId = message.id; - - client.createReminder(messageId).ignore(); - }, + action: CreateBookmark(message: message), ), ], ] ]; + final locationAttachmentBuilder = LocationAttachmentBuilder( + onAttachmentTap: (location) => showLocationDetailDialog( + context: context, + location: location, + ), + ); + return Container( color: reminder != null ? colorTheme.accentPrimary.withOpacity(.1) : null, child: Column( @@ -264,6 +295,15 @@ class _ChannelPageState extends State { defaultMessageWidget.copyWith( onReplyTap: _reply, customActions: customOptions, + showEditMessage: message.sharedLocation == null, + onCustomActionTap: (it) async => await switch (it) { + CreateReminder() => _createReminder(it.message), + CreateBookmark() => _createBookmark(it.message), + EditReminder() => _editReminder(it.message, it.reminder), + RemoveReminder() => _removeReminder(it.message, it.reminder), + _ => null, + }, + attachmentBuilders: [locationAttachmentBuilder], onShowMessage: (message, channel) => GoRouter.of(context).goNamed( Routes.CHANNEL_PAGE.name, pathParameters: Routes.CHANNEL_PAGE.params(channel), @@ -284,6 +324,56 @@ class _ChannelPageState extends State { ); } + Future _editReminder( + Message message, + MessageReminder reminder, + ) async { + final option = await showDialog( + context: context, + builder: (_) => EditReminderDialog( + isBookmarkReminder: reminder.remindAt == null, + ), + ); + + if (option == null) return; + final client = StreamChat.of(context).client; + final messageId = message.id; + final remindAt = option.remindAt; + + return client.updateReminder(messageId, remindAt: remindAt).ignore(); + } + + Future _removeReminder( + Message message, + MessageReminder reminder, + ) async { + final client = StreamChat.of(context).client; + final messageId = message.id; + + return client.deleteReminder(messageId).ignore(); + } + + Future _createReminder(Message message) async { + final reminder = await showDialog( + context: context, + builder: (_) => const CreateReminderDialog(), + ); + + if (reminder == null) return; + final client = StreamChat.of(context).client; + final messageId = message.id; + final remindAt = reminder.remindAt; + + return client.createReminder(messageId, remindAt: remindAt).ignore(); + } + + Future _createBookmark(Message message) async { + final client = StreamChat.of(context).client; + final messageId = message.id; + + return client.createReminder(messageId).ignore(); + } + bool defaultFilter(Message m) { final currentUser = StreamChat.of(context).currentUser; final isMyMessage = m.user?.id == currentUser?.id; @@ -292,3 +382,40 @@ class _ChannelPageState extends State { return true; } } + +class ReminderMessageAction extends CustomMessageAction { + const ReminderMessageAction({ + required super.message, + this.reminder, + }); + + final MessageReminder? reminder; +} + +final class CreateReminder extends ReminderMessageAction { + const CreateReminder({required super.message}); +} + +final class CreateBookmark extends ReminderMessageAction { + const CreateBookmark({required super.message}); +} + +final class EditReminder extends ReminderMessageAction { + const EditReminder({ + required super.message, + required this.reminder, + }) : super(reminder: reminder); + + @override + final MessageReminder reminder; +} + +final class RemoveReminder extends ReminderMessageAction { + const RemoveReminder({ + required super.message, + required this.reminder, + }) : super(reminder: reminder); + + @override + final MessageReminder reminder; +} diff --git a/sample_app/lib/pages/chat_info_screen.dart b/sample_app/lib/pages/chat_info_screen.dart index 6caacbdfc4..81ac6dbfe5 100644 --- a/sample_app/lib/pages/chat_info_screen.dart +++ b/sample_app/lib/pages/chat_info_screen.dart @@ -51,8 +51,7 @@ class _ChatInfoScreenState extends State { height: 8, color: StreamChatTheme.of(context).colorTheme.disabled, ), - if (channel.ownCapabilities.contains(PermissionType.deleteChannel)) - _buildDeleteListTile(), + if (channel.canDeleteChannel) _buildDeleteListTile(), ], ), ); diff --git a/sample_app/lib/pages/group_info_screen.dart b/sample_app/lib/pages/group_info_screen.dart index a3ffef7d48..1a5e6997ad 100644 --- a/sample_app/lib/pages/group_info_screen.dart +++ b/sample_app/lib/pages/group_info_screen.dart @@ -105,10 +105,7 @@ class _GroupInfoScreenState extends State { ], ), sort: [ - const SortOption( - 'name', - direction: 1, - ), + const SortOption.asc(UserSortKey.name), ], ); super.didChangeDependencies(); @@ -194,8 +191,7 @@ class _GroupInfoScreenState extends State { ), centerTitle: true, actions: [ - if (channel.ownCapabilities - .contains(PermissionType.updateChannelMembers)) + if (channel.canUpdateChannelMembers) StreamNeumorphicButton( child: InkWell( onTap: () { @@ -220,9 +216,7 @@ class _GroupInfoScreenState extends State { height: 8, color: StreamChatTheme.of(context).colorTheme.disabled, ), - if (channel.ownCapabilities - .contains(PermissionType.updateChannel)) - _buildNameTile(), + if (channel.canUpdateChannel) _buildNameTile(), _buildOptionListTiles(), ], ), @@ -415,8 +409,7 @@ class _GroupInfoScreenState extends State { ), Expanded( child: TextField( - enabled: channel.ownCapabilities - .contains(PermissionType.updateChannel), + enabled: channel.canUpdateChannel, focusNode: _focusNode, controller: _nameController, cursorColor: @@ -482,7 +475,7 @@ class _GroupInfoScreenState extends State { Widget _buildOptionListTiles() { return Column( children: [ - if (channel.ownCapabilities.contains(PermissionType.muteChannel)) + if (channel.canMuteChannel) _GroupInfoToggle( title: AppLocalizations.of(context).muteGroup, icon: StreamSvgIcons.mute, @@ -568,8 +561,7 @@ class _GroupInfoScreenState extends State { ); }, ), - if (!channel.isDistinct && - channel.ownCapabilities.contains(PermissionType.leaveChannel)) + if (!channel.isDistinct && channel.canLeaveChannel) StreamOptionListTile( tileColor: StreamChatTheme.of(context).colorTheme.appBg, separatorColor: StreamChatTheme.of(context).colorTheme.disabled, diff --git a/sample_app/lib/pages/new_chat_screen.dart b/sample_app/lib/pages/new_chat_screen.dart index dfa5daaaf0..fa162f28aa 100644 --- a/sample_app/lib/pages/new_chat_screen.dart +++ b/sample_app/lib/pages/new_chat_screen.dart @@ -29,10 +29,7 @@ class _NewChatScreenState extends State { Filter.notEqual('id', StreamChat.of(context).currentUser!.id), ]), sort: [ - const SortOption( - 'name', - direction: 1, - ), + const SortOption.asc(UserSortKey.name), ], ); diff --git a/sample_app/lib/pages/pinned_messages_screen.dart b/sample_app/lib/pages/pinned_messages_screen.dart index 2bcc792fc6..01c9ac5075 100644 --- a/sample_app/lib/pages/pinned_messages_screen.dart +++ b/sample_app/lib/pages/pinned_messages_screen.dart @@ -25,10 +25,7 @@ class _PinnedMessagesScreenState extends State { true, ), sort: [ - const SortOption( - 'created_at', - direction: SortOption.ASC, - ), + const SortOption.asc('created_at'), ], limit: 20, ); diff --git a/sample_app/lib/pages/thread_page.dart b/sample_app/lib/pages/thread_page.dart index 622b5772ca..3e8376eb44 100644 --- a/sample_app/lib/pages/thread_page.dart +++ b/sample_app/lib/pages/thread_page.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:sample_app/widgets/location/location_attachment.dart'; +import 'package:sample_app/widgets/location/location_detail_dialog.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class ThreadPage extends StatefulWidget { @@ -43,6 +45,13 @@ class _ThreadPageState extends State { @override Widget build(BuildContext context) { + final locationAttachmentBuilder = LocationAttachmentBuilder( + onAttachmentTap: (location) => showLocationDetailDialog( + context: context, + location: location, + ), + ); + return Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, appBar: StreamThreadHeader( @@ -59,9 +68,18 @@ class _ThreadPageState extends State { messageFilter: defaultFilter, showScrollToBottom: false, highlightInitialMessage: true, + parentMessageBuilder: (context, message, defaultMessage) { + return defaultMessage.copyWith( + attachmentBuilders: [locationAttachmentBuilder], + ); + }, messageBuilder: (context, details, messages, defaultMessage) { + final message = details.message; + return defaultMessage.copyWith( onReplyTap: _reply, + showEditMessage: message.sharedLocation == null, + attachmentBuilders: [locationAttachmentBuilder], bottomRowBuilderWithDefaultWidget: ( context, message, diff --git a/sample_app/lib/utils/location_provider.dart b/sample_app/lib/utils/location_provider.dart new file mode 100644 index 0000000000..fbf7168028 --- /dev/null +++ b/sample_app/lib/utils/location_provider.dart @@ -0,0 +1,98 @@ +// ignore_for_file: close_sinks + +import 'dart:async'; + +import 'package:geolocator/geolocator.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +const notificationTitle = 'Live Location Tracking'; +const notificationText = 'Your location is being tracked live.'; + +class LocationProvider { + factory LocationProvider() => _instance; + LocationProvider._(); + + static final LocationProvider _instance = LocationProvider._(); + + Stream get positionStream => _positionStreamController.stream; + final _positionStreamController = StreamController.broadcast(); + + StreamSubscription? _positionSubscription; + + /// Opens the device's location settings page. + /// + /// Returns [true] if the location settings page could be opened, otherwise + /// [false] is returned. + Future openLocationSettings() => Geolocator.openLocationSettings(); + + /// Get current static location + Future getCurrentLocation() async { + final hasPermission = await _handlePermission(); + if (!hasPermission) return null; + + return Geolocator.getCurrentPosition(); + } + + /// Start live tracking + Future startTracking({ + int distanceFilter = 10, + LocationAccuracy accuracy = LocationAccuracy.high, + ActivityType activityType = ActivityType.automotiveNavigation, + }) async { + final hasPermission = await _handlePermission(); + if (!hasPermission) return; + + final settings = switch (CurrentPlatform.type) { + PlatformType.android => AndroidSettings( + accuracy: accuracy, + distanceFilter: distanceFilter, + foregroundNotificationConfig: const ForegroundNotificationConfig( + setOngoing: true, + notificationText: notificationText, + notificationTitle: notificationTitle, + notificationIcon: AndroidResource(name: 'ic_notification'), + ), + ), + PlatformType.ios || PlatformType.macOS => AppleSettings( + accuracy: accuracy, + activityType: activityType, + distanceFilter: distanceFilter, + showBackgroundLocationIndicator: true, + pauseLocationUpdatesAutomatically: true, + ), + _ => LocationSettings( + accuracy: accuracy, + distanceFilter: distanceFilter, + ) + }; + + _positionSubscription?.cancel(); // avoid duplicate subscriptions + _positionSubscription = Geolocator.getPositionStream( + locationSettings: settings, + ).listen( + _positionStreamController.safeAdd, + onError: _positionStreamController.safeAddError, + ); + } + + /// Stop live tracking + void stopTracking() { + _positionSubscription?.cancel(); + _positionSubscription = null; + } + + Future _handlePermission() async { + final serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) return false; + + var permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + + return switch (permission) { + LocationPermission.denied || LocationPermission.deniedForever => false, + _ => true, + }; + } +} diff --git a/sample_app/lib/utils/shared_location_service.dart b/sample_app/lib/utils/shared_location_service.dart new file mode 100644 index 0000000000..1d7a7128c6 --- /dev/null +++ b/sample_app/lib/utils/shared_location_service.dart @@ -0,0 +1,88 @@ +import 'dart:async'; + +import 'package:geolocator/geolocator.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:sample_app/utils/location_provider.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class SharedLocationService { + SharedLocationService({ + required StreamChatClient client, + LocationProvider? locationProvider, + }) : _client = client, + _locationProvider = locationProvider ?? LocationProvider(); + + final StreamChatClient _client; + final LocationProvider _locationProvider; + + StreamSubscription? _positionSubscription; + StreamSubscription>? _activeLiveLocationsSubscription; + + Future initialize() async { + _activeLiveLocationsSubscription?.cancel(); + _activeLiveLocationsSubscription = _client.state.activeLiveLocationsStream + .distinct((prev, curr) => prev.length == curr.length) + .listen((locations) async { + // If there are no more active locations to update, stop tracking. + if (locations.isEmpty) return _stopTrackingLocation(); + + // Otherwise, start tracking the user's location. + return _startTrackingLocation(); + }); + + return _client.getActiveLiveLocations().ignore(); + } + + Future _startTrackingLocation() async { + if (_positionSubscription != null) return; + + // Start listening to the position stream. + _positionSubscription = _locationProvider.positionStream + .throttleTime(const Duration(seconds: 3)) + .listen(_onPositionUpdate); + + return _locationProvider.startTracking(); + } + + void _stopTrackingLocation() { + _locationProvider.stopTracking(); + + // Stop tracking the user's location + _positionSubscription?.cancel(); + _positionSubscription = null; + } + + void _onPositionUpdate(Position position) { + // Handle location updates, e.g., update the UI or send to server + final activeLiveLocations = _client.state.activeLiveLocations; + if (activeLiveLocations.isEmpty) return _stopTrackingLocation(); + + // Update all active live locations + for (final location in activeLiveLocations) { + // Skip if the location is not live or has expired + if (location.isLive && location.isExpired) continue; + + // Skip if the location does not have a messageId + final messageId = location.messageId; + if (messageId == null) continue; + + // Update the live location with the new position + _client.updateLiveLocation( + messageId: messageId, + createdByDeviceId: location.createdByDeviceId, + location: LocationCoordinates( + latitude: position.latitude, + longitude: position.longitude, + ), + ); + } + } + + /// Clean up resources + Future dispose() async { + _stopTrackingLocation(); + + _activeLiveLocationsSubscription?.cancel(); + _activeLiveLocationsSubscription = null; + } +} diff --git a/sample_app/lib/widgets/channel_list.dart b/sample_app/lib/widgets/channel_list.dart index c2dab2bb85..a87e51b012 100644 --- a/sample_app/lib/widgets/channel_list.dart +++ b/sample_app/lib/widgets/channel_list.dart @@ -61,7 +61,7 @@ class _ChannelList extends State { ChannelSortKey.pinnedAt, nullOrdering: NullOrdering.nullsLast, ), - const SortOption.desc(ChannelSortKey.lastMessageAt), + const SortOption.desc(ChannelSortKey.lastUpdated), ], limit: 30, ); diff --git a/sample_app/lib/widgets/location/location_attachment.dart b/sample_app/lib/widgets/location/location_attachment.dart new file mode 100644 index 0000000000..405f97b7df --- /dev/null +++ b/sample_app/lib/widgets/location/location_attachment.dart @@ -0,0 +1,235 @@ +import 'package:flutter/material.dart'; +import 'package:sample_app/widgets/location/location_user_marker.dart'; +import 'package:sample_app/widgets/simple_map_view.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +const _defaultLocationConstraints = BoxConstraints( + maxWidth: 270, + maxHeight: 180, +); + +/// {@template locationAttachmentBuilder} +/// A builder for creating a location attachment widget. +/// {@endtemplate} +class LocationAttachmentBuilder extends StreamAttachmentWidgetBuilder { + /// {@macro locationAttachmentBuilder} + const LocationAttachmentBuilder({ + this.constraints = _defaultLocationConstraints, + this.padding = const EdgeInsets.all(4), + this.onAttachmentTap, + }); + + /// The constraints to apply to the file attachment widget. + final BoxConstraints constraints; + + /// The padding to apply to the file attachment widget. + final EdgeInsetsGeometry padding; + + /// Optional callback to handle tap events on the attachment. + final ValueSetter? onAttachmentTap; + + @override + bool canHandle(Message message, _) => message.sharedLocation != null; + + @override + Widget build(BuildContext context, Message message, _) { + assert(debugAssertCanHandle(message, _), ''); + + final user = message.user; + final location = message.sharedLocation!; + return LocationAttachment( + user: user, + sharedLocation: location, + constraints: constraints, + padding: padding, + onLocationTap: switch (onAttachmentTap) { + final onTap? => () => onTap(location), + _ => null, + }, + ); + } +} + +/// Displays a location attachment with a map view and optional footer. +class LocationAttachment extends StatelessWidget { + /// Creates a new [LocationAttachment]. + const LocationAttachment({ + super.key, + required this.user, + required this.sharedLocation, + this.constraints = _defaultLocationConstraints, + this.padding = const EdgeInsets.all(2), + this.onLocationTap, + }); + + /// The user who shared the location. + final User? user; + + /// The shared location data. + final Location sharedLocation; + + /// The constraints to apply to the file attachment widget. + final BoxConstraints constraints; + + /// The padding to apply to the file attachment widget. + final EdgeInsetsGeometry padding; + + /// Optional callback to handle tap events on the location attachment. + final VoidCallback? onLocationTap; + + @override + Widget build(BuildContext context) { + final currentUser = StreamChat.of(context).currentUser; + final sharedLocationEndAt = sharedLocation.endAt; + + return Padding( + padding: padding, + child: ConstrainedBox( + constraints: constraints, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Material( + clipBehavior: Clip.antiAlias, + type: MaterialType.transparency, + borderRadius: BorderRadius.circular(14), + child: InkWell( + onTap: onLocationTap, + child: IgnorePointer( + child: SimpleMapView( + markerSize: 40, + showLocateMeButton: false, + coordinates: sharedLocation.coordinates, + markerBuilder: (_, __, size) => LocationUserMarker( + user: user, + markerSize: size, + sharedLocation: sharedLocation, + ), + ), + ), + ), + ), + ), + if (sharedLocationEndAt != null && currentUser != null) + LocationAttachmentFooter( + currentUser: currentUser, + sharingEndAt: sharedLocationEndAt, + sharedLocation: sharedLocation, + onStopSharingPressed: () { + final client = StreamChat.of(context).client; + + final location = sharedLocation; + final messageId = location.messageId; + if (messageId == null) return; + + client.stopLiveLocation( + messageId: messageId, + createdByDeviceId: location.createdByDeviceId, + ); + }, + ), + ], + ), + ), + ); + } +} + +class LocationAttachmentFooter extends StatelessWidget { + const LocationAttachmentFooter({ + super.key, + required this.currentUser, + required this.sharingEndAt, + required this.sharedLocation, + this.onStopSharingPressed, + }); + + final User currentUser; + final DateTime sharingEndAt; + final Location sharedLocation; + final VoidCallback? onStopSharingPressed; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final textTheme = theme.textTheme; + final colorTheme = theme.colorTheme; + + const maximumSize = Size(double.infinity, 40); + + // If the location sharing has ended, show a message indicating that. + if (sharingEndAt.isBefore(DateTime.now())) { + return SizedBox.fromSize( + size: maximumSize, + child: Row( + spacing: 8, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.near_me_disabled_rounded, + color: colorTheme.textLowEmphasis, + ), + Text( + 'Live location ended', + style: textTheme.bodyBold.copyWith( + color: colorTheme.textLowEmphasis, + ), + ), + ], + ), + ); + } + + final currentUserId = currentUser.id; + final sharedLocationUserId = sharedLocation.userId; + + // If the shared location is not shared by the current user, show the + // "Live until" duration text. + if (sharedLocationUserId != currentUserId) { + final liveUntil = Jiffy.parseFromDateTime(sharingEndAt.toLocal()); + + return SizedBox.fromSize( + size: maximumSize, + child: Row( + spacing: 8, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.near_me_rounded, + color: colorTheme.accentPrimary, + ), + Text( + 'Live until ${liveUntil.jm}', + style: textTheme.bodyBold.copyWith( + color: colorTheme.accentPrimary, + ), + ), + ], + ), + ); + } + + // Otherwise, show the "Stop Sharing" button. + final buttonStyle = TextButton.styleFrom( + maximumSize: maximumSize, + textStyle: textTheme.bodyBold, + visualDensity: VisualDensity.compact, + foregroundColor: colorTheme.accentError, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + ); + + return TextButton.icon( + style: buttonStyle, + onPressed: onStopSharingPressed, + icon: Icon( + Icons.near_me_disabled_rounded, + color: colorTheme.accentError, + ), + label: const Text('Stop Sharing'), + ); + } +} diff --git a/sample_app/lib/widgets/location/location_detail_dialog.dart b/sample_app/lib/widgets/location/location_detail_dialog.dart new file mode 100644 index 0000000000..c5082bf365 --- /dev/null +++ b/sample_app/lib/widgets/location/location_detail_dialog.dart @@ -0,0 +1,312 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:sample_app/widgets/location/location_user_marker.dart'; +import 'package:sample_app/widgets/simple_map_view.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +Future showLocationDetailDialog({ + required BuildContext context, + required Location location, +}) async { + final navigator = Navigator.of(context); + return navigator.push( + MaterialPageRoute( + fullscreenDialog: true, + builder: (_) => StreamChannel( + channel: StreamChannel.of(context).channel, + child: LocationDetailDialog(sharedLocation: location), + ), + ), + ); +} + +Stream _findLocationMessageStream( + Channel channel, + Location location, +) { + final messageId = location.messageId; + if (messageId == null) return Stream.value(null); + + final channelState = channel.state; + if (channelState == null) return Stream.value(null); + + return channelState.messagesStream.map((messages) { + return messages.firstWhereOrNull((message) => message.id == messageId); + }); +} + +class LocationDetailDialog extends StatelessWidget { + const LocationDetailDialog({ + super.key, + required this.sharedLocation, + }); + + final Location sharedLocation; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final colorTheme = theme.colorTheme; + + final channel = StreamChannel.of(context).channel; + final locationStream = _findLocationMessageStream(channel, sharedLocation); + + return Scaffold( + backgroundColor: colorTheme.appBg, + appBar: AppBar( + backgroundColor: colorTheme.barsBg, + title: const Text('Shared Location'), + ), + body: BetterStreamBuilder( + stream: locationStream, + errorBuilder: (_, __) => const Center(child: LocationNotFound()), + noDataBuilder: (_) => const Center(child: CircularProgressIndicator()), + builder: (context, message) { + final sharedLocation = message.sharedLocation; + if (sharedLocation == null) { + return const Center(child: LocationNotFound()); + } + + return Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + SimpleMapView( + cameraZoom: 16, + markerSize: 48, + coordinates: sharedLocation.coordinates, + markerBuilder: (_, __, size) => LocationUserMarker( + user: message.user, + markerSize: size, + sharedLocation: sharedLocation, + ), + ), + if (sharedLocation.isLive) + LocationDetailBottomSheet( + sharedLocation: sharedLocation, + onStopSharingPressed: () { + final client = StreamChat.of(context).client; + + final messageId = sharedLocation.messageId; + if (messageId == null) return; + + client.stopLiveLocation( + messageId: messageId, + createdByDeviceId: sharedLocation.createdByDeviceId, + ); + }, + ), + ], + ); + }, + ), + ); + } +} + +class LocationNotFound extends StatelessWidget { + const LocationNotFound({super.key}); + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + final colorTheme = chatThemeData.colorTheme; + final textTheme = chatThemeData.textTheme; + + return Column( + spacing: 8, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + size: 48, + Icons.near_me_disabled_rounded, + color: colorTheme.accentError, + ), + Text( + 'Location not found', + style: textTheme.headline.copyWith( + color: colorTheme.textHighEmphasis, + ), + ), + Text( + 'The location you are looking for is not available.', + style: textTheme.body.copyWith( + color: colorTheme.textLowEmphasis, + ), + ), + ], + ); + } +} + +class LocationDetailBottomSheet extends StatelessWidget { + const LocationDetailBottomSheet({ + super.key, + required this.sharedLocation, + this.onStopSharingPressed, + }); + + final Location sharedLocation; + final VoidCallback? onStopSharingPressed; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final colorTheme = theme.colorTheme; + + return Material( + color: colorTheme.barsBg, + borderRadius: const BorderRadiusDirectional.only( + topEnd: Radius.circular(14), + topStart: Radius.circular(14), + ), + child: SafeArea( + minimum: const EdgeInsets.all(8), + child: LocationDetail( + sharedLocation: sharedLocation, + onStopSharingPressed: onStopSharingPressed, + ), + ), + ); + } +} + +class LocationDetail extends StatelessWidget { + const LocationDetail({ + super.key, + required this.sharedLocation, + this.onStopSharingPressed, + }); + + final Location sharedLocation; + final VoidCallback? onStopSharingPressed; + + @override + Widget build(BuildContext context) { + assert( + sharedLocation.isLive, + 'Footer should only be shown for live locations', + ); + + final theme = StreamChatTheme.of(context); + final textTheme = theme.textTheme; + final colorTheme = theme.colorTheme; + + final updatedAt = sharedLocation.updatedAt; + final sharingEndAt = sharedLocation.endAt!; + const maximumButtonSize = Size(double.infinity, 40); + + if (sharingEndAt.isBefore(DateTime.now())) { + final jiffyUpdatedAt = Jiffy.parseFromDateTime(updatedAt.toLocal()); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox.fromSize( + size: maximumButtonSize, + child: Row( + spacing: 8, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.near_me_disabled_rounded, + color: colorTheme.accentError, + ), + Text( + 'Live location ended', + style: textTheme.headlineBold.copyWith( + color: colorTheme.accentError, + ), + ), + ], + ), + ), + Text( + 'Location last updated at ${jiffyUpdatedAt.jm}', + style: textTheme.body.copyWith( + color: colorTheme.textLowEmphasis, + ), + ), + ], + ); + } + + final sharedLocationUserId = sharedLocation.userId; + final currentUserId = StreamChat.of(context).currentUser?.id; + + // If the shared location is not shared by the current user, show the + // "Live until" duration text. + if (sharedLocationUserId != currentUserId) { + final jiffySharingEndAt = Jiffy.parseFromDateTime(sharingEndAt.toLocal()); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox.fromSize( + size: maximumButtonSize, + child: Row( + spacing: 8, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.near_me_rounded, + color: colorTheme.accentPrimary, + ), + Text( + 'Live Location', + style: textTheme.headlineBold.copyWith( + color: colorTheme.accentPrimary, + ), + ), + ], + ), + ), + Text( + 'Live until ${jiffySharingEndAt.jm}', + style: textTheme.body.copyWith( + color: colorTheme.textLowEmphasis, + ), + ), + ], + ); + } + + // Otherwise, show the "Stop Sharing" button. + final buttonStyle = TextButton.styleFrom( + maximumSize: maximumButtonSize, + textStyle: textTheme.headlineBold, + visualDensity: VisualDensity.compact, + foregroundColor: colorTheme.accentError, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + ); + + final jiffySharingEndAt = Jiffy.parseFromDateTime(sharingEndAt.toLocal()); + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: TextButton.icon( + style: buttonStyle, + onPressed: onStopSharingPressed, + icon: Icon( + Icons.near_me_disabled_rounded, + color: colorTheme.accentError, + ), + label: const Text('Stop Sharing'), + ), + ), + Center( + child: Text( + 'Live until ${jiffySharingEndAt.jm}', + style: textTheme.body.copyWith( + color: colorTheme.textLowEmphasis, + ), + ), + ), + ], + ); + } +} diff --git a/sample_app/lib/widgets/location/location_picker_dialog.dart b/sample_app/lib/widgets/location/location_picker_dialog.dart new file mode 100644 index 0000000000..1abaaf9e1c --- /dev/null +++ b/sample_app/lib/widgets/location/location_picker_dialog.dart @@ -0,0 +1,362 @@ +import 'package:avatar_glow/avatar_glow.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:sample_app/utils/location_provider.dart'; +import 'package:sample_app/widgets/simple_map_view.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class LocationPickerResult { + const LocationPickerResult({ + this.endSharingAt, + required this.coordinates, + }); + + final DateTime? endSharingAt; + final LocationCoordinates coordinates; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is LocationPickerResult && + runtimeType == other.runtimeType && + endSharingAt == other.endSharingAt && + coordinates == other.coordinates; + } + + @override + int get hashCode => endSharingAt.hashCode ^ coordinates.hashCode; +} + +Future showLocationPickerDialog({ + required BuildContext context, + bool barrierDismissible = true, + Color? barrierColor, + String? barrierLabel, + bool useSafeArea = true, + bool useRootNavigator = false, + RouteSettings? routeSettings, + Offset? anchorPoint, + EdgeInsets padding = const EdgeInsets.all(16), + TraversalEdgeBehavior? traversalEdgeBehavior, +}) { + final navigator = Navigator.of(context, rootNavigator: useRootNavigator); + return navigator.push( + MaterialPageRoute( + fullscreenDialog: true, + barrierDismissible: barrierDismissible, + builder: (context) => const LocationPickerDialog(), + ), + ); +} + +class LocationPickerDialog extends StatefulWidget { + const LocationPickerDialog({super.key}); + + @override + State createState() => _LocationPickerDialogState(); +} + +class _LocationPickerDialogState extends State { + LocationCoordinates? _currentLocation; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final colorTheme = theme.colorTheme; + + return Scaffold( + backgroundColor: colorTheme.appBg, + appBar: AppBar( + backgroundColor: colorTheme.barsBg, + title: const Text('Share Location'), + ), + body: Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + FutureBuilder( + future: LocationProvider().getCurrentLocation(), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + } + + final position = snapshot.data; + if (snapshot.hasError || position == null) { + return const Center(child: LocationNotFound()); + } + + final coordinates = _currentLocation = LocationCoordinates( + latitude: position.latitude, + longitude: position.longitude, + ); + + return SimpleMapView( + cameraZoom: 18, + markerSize: 24, + coordinates: coordinates, + markerBuilder: (context, _, size) => AvatarGlow( + glowColor: colorTheme.accentPrimary, + child: Material( + elevation: 2, + shape: CircleBorder( + side: BorderSide( + width: 4, + color: colorTheme.barsBg, + ), + ), + child: CircleAvatar( + radius: size / 2, + backgroundColor: colorTheme.accentPrimary, + ), + ), + ), + ); + }, + ), + // Location picker options + LocationPickerOptionList( + onOptionSelected: (option) { + final currentLocation = _currentLocation; + if (currentLocation == null) return Navigator.pop(context); + + final result = LocationPickerResult( + endSharingAt: switch (option) { + ShareStaticLocation() => null, + ShareLiveLocation() => option.endSharingAt, + }, + coordinates: currentLocation, + ); + + return Navigator.pop(context, result); + }, + ), + ], + ), + ); + } +} + +class LocationNotFound extends StatelessWidget { + const LocationNotFound({super.key}); + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + final colorTheme = chatThemeData.colorTheme; + final textTheme = chatThemeData.textTheme; + + return Column( + spacing: 8, + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + size: 48, + Icons.near_me_disabled_rounded, + color: colorTheme.accentError, + ), + Text( + 'Something went wrong', + style: textTheme.headline.copyWith( + color: colorTheme.textHighEmphasis, + ), + ), + Text( + 'Please check your location settings and try again.', + style: textTheme.body.copyWith( + color: colorTheme.textLowEmphasis, + ), + ), + ], + ); + } +} + +class LocationPickerOptionList extends StatelessWidget { + const LocationPickerOptionList({ + super.key, + required this.onOptionSelected, + }); + + final ValueSetter onOptionSelected; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final colorTheme = theme.colorTheme; + + return Material( + color: colorTheme.barsBg, + borderRadius: const BorderRadiusDirectional.only( + topEnd: Radius.circular(14), + topStart: Radius.circular(14), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 14, + ), + child: Column( + spacing: 8, + mainAxisSize: MainAxisSize.min, + children: [ + LocationPickerOptionItem( + icon: const Icon(Icons.share_location_rounded), + title: 'Share Live Location', + subtitle: 'Your location will update in real-time', + onTap: () async { + final duration = await showCupertinoModalPopup( + context: context, + builder: (_) => const LiveLocationDurationDialog(), + ); + + if (duration == null) return; + final endSharingAt = DateTime.timestamp().add(duration); + + return onOptionSelected( + ShareLiveLocation(endSharingAt: endSharingAt), + ); + }, + ), + LocationPickerOptionItem( + icon: const Icon(Icons.my_location), + title: 'Share Static Location', + subtitle: 'Send your current location only', + onTap: () => onOptionSelected(const ShareStaticLocation()), + ), + ], + ), + ), + ), + ); + } +} + +sealed class LocationPickerOption { + const LocationPickerOption(); +} + +final class ShareLiveLocation extends LocationPickerOption { + const ShareLiveLocation({required this.endSharingAt}); + final DateTime endSharingAt; +} + +final class ShareStaticLocation extends LocationPickerOption { + const ShareStaticLocation(); +} + +class LocationPickerOptionItem extends StatelessWidget { + const LocationPickerOptionItem({ + super.key, + required this.icon, + required this.title, + required this.subtitle, + required this.onTap, + }); + + final Widget icon; + final String title; + final String subtitle; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final textTheme = theme.textTheme; + final colorTheme = theme.colorTheme; + + return OutlinedButton( + onPressed: onTap, + style: OutlinedButton.styleFrom( + backgroundColor: colorTheme.barsBg, + foregroundColor: colorTheme.accentPrimary, + side: BorderSide(color: colorTheme.borders, width: 1.2), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), + ), + child: IconTheme( + data: IconTheme.of(context).copyWith( + size: 24, + color: colorTheme.accentPrimary, + ), + child: Row( + spacing: 16, + children: [ + icon, + Expanded( + child: Column( + spacing: 2, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textTheme.bodyBold.copyWith( + color: colorTheme.textHighEmphasis, + ), + ), + Text( + subtitle, + style: textTheme.footnote.copyWith( + color: colorTheme.textLowEmphasis, + ), + ), + ], + ), + ), + StreamSvgIcon( + size: 24, + icon: StreamSvgIcons.right, + color: colorTheme.textLowEmphasis, + ), + ], + ), + ), + ); + } +} + +class LiveLocationDurationDialog extends StatelessWidget { + const LiveLocationDurationDialog({super.key}); + + static const _endAtDurations = [ + Duration(minutes: 15), + Duration(hours: 1), + Duration(hours: 8), + ]; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + + return CupertinoTheme( + data: CupertinoTheme.of(context).copyWith( + primaryColor: theme.colorTheme.accentPrimary, + ), + child: CupertinoActionSheet( + title: const Text('Share Live Location'), + message: Text( + 'Select the duration for sharing your live location.', + style: theme.textTheme.footnote.copyWith( + color: theme.colorTheme.textLowEmphasis, + ), + ), + actions: [ + ..._endAtDurations.map((duration) { + final endAt = Jiffy.now().addDuration(duration); + return CupertinoActionSheetAction( + onPressed: () => Navigator.of(context).pop(duration), + child: Text(endAt.fromNow(withPrefixAndSuffix: false)), + ); + }), + ], + cancelButton: CupertinoActionSheetAction( + isDestructiveAction: true, + onPressed: Navigator.of(context).pop, + child: const Text('Cancel'), + ), + ), + ); + } +} diff --git a/sample_app/lib/widgets/location/location_picker_option.dart b/sample_app/lib/widgets/location/location_picker_option.dart new file mode 100644 index 0000000000..e32ba43ee7 --- /dev/null +++ b/sample_app/lib/widgets/location/location_picker_option.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:sample_app/utils/location_provider.dart'; +import 'package:sample_app/widgets/location/location_picker_dialog.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +final class LocationPickerType extends CustomAttachmentPickerType { + const LocationPickerType(); +} + +final class LocationPicked extends CustomAttachmentPickerResult { + const LocationPicked({required this.location}); + final LocationPickerResult location; +} + +class LocationPicker extends StatelessWidget { + const LocationPicker({ + super.key, + this.onLocationPicked, + }); + + final ValueSetter? onLocationPicked; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final colorTheme = theme.colorTheme; + + return OptionDrawer( + child: EndOfFrameCallbackWidget( + child: Icon( + size: 148, + Icons.near_me_rounded, + color: colorTheme.disabled, + ), + onEndOfFrame: (context) async { + final result = await runInPermissionRequestLock(() { + return showLocationPickerDialog(context: context); + }); + + onLocationPicked?.call(result); + }, + errorBuilder: (context, error, stacktrace) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + size: 148, + Icons.near_me_rounded, + color: theme.colorTheme.disabled, + ), + Text( + 'Please enable access to your location', + style: theme.textTheme.body.copyWith( + color: theme.colorTheme.textLowEmphasis, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + TextButton( + onPressed: LocationProvider().openLocationSettings, + child: Text( + 'Allow Location Access', + style: theme.textTheme.bodyBold.copyWith( + color: theme.colorTheme.accentPrimary, + ), + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/sample_app/lib/widgets/location/location_user_marker.dart b/sample_app/lib/widgets/location/location_user_marker.dart new file mode 100644 index 0000000000..102e16876c --- /dev/null +++ b/sample_app/lib/widgets/location/location_user_marker.dart @@ -0,0 +1,56 @@ +import 'package:avatar_glow/avatar_glow.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +class LocationUserMarker extends StatelessWidget { + const LocationUserMarker({ + super.key, + this.user, + this.markerSize = 40, + required this.sharedLocation, + }); + + final User? user; + final double markerSize; + final Location sharedLocation; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final colorTheme = theme.colorTheme; + + if (user case final user? when sharedLocation.isLive) { + const borderWidth = 4.0; + + final avatar = Material( + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, + color: colorTheme.overlayDark, + child: Padding( + padding: const EdgeInsets.all(borderWidth), + child: StreamUserAvatar( + user: user, + constraints: BoxConstraints.tightFor( + width: markerSize, + height: markerSize, + ), + showOnlineStatus: false, + ), + ), + ); + + if (sharedLocation.isExpired) return avatar; + + return AvatarGlow( + glowColor: colorTheme.accentPrimary, + child: avatar, + ); + } + + return Icon( + size: markerSize, + Icons.person_pin, + color: colorTheme.accentPrimary, + ); + } +} diff --git a/sample_app/lib/widgets/simple_map_view.dart b/sample_app/lib/widgets/simple_map_view.dart new file mode 100644 index 0000000000..0faadc11f7 --- /dev/null +++ b/sample_app/lib/widgets/simple_map_view.dart @@ -0,0 +1,153 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_animations/flutter_map_animations.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +typedef MarkerBuilder = Widget Function( + BuildContext context, + Animation animation, + double markerSize, +); + +class SimpleMapView extends StatefulWidget { + const SimpleMapView({ + super.key, + this.cameraZoom = 15, + this.markerSize = 30, + required this.coordinates, + this.showLocateMeButton = true, + this.markerBuilder = _defaultMarkerBuilder, + }); + + final double cameraZoom; + + final double markerSize; + + final LocationCoordinates coordinates; + + final bool showLocateMeButton; + + final MarkerBuilder markerBuilder; + static Widget _defaultMarkerBuilder(BuildContext context, _, double size) { + final theme = StreamChatTheme.of(context); + final iconColor = theme.colorTheme.accentPrimary; + return Icon(size: size, Icons.person_pin, color: iconColor); + } + + @override + State createState() => _SimpleMapViewState(); +} + +class _SimpleMapViewState extends State + with TickerProviderStateMixin { + late final _mapController = AnimatedMapController(vsync: this); + late final _initialCenter = widget.coordinates.toLatLng(); + + @override + void didUpdateWidget(covariant SimpleMapView oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.coordinates != widget.coordinates) { + _mapController.animateTo( + dest: widget.coordinates.toLatLng(), + zoom: widget.cameraZoom, + curve: Curves.easeInOut, + ); + } + } + + @override + Widget build(BuildContext context) { + final brightness = Theme.of(context).brightness; + const baseMapTemplate = 'https://{s}.basemaps.cartocdn.com'; + const mapTemplate = '$baseMapTemplate/rastertiles/voyager/{z}/{x}/{y}.png'; + const fallbackTemplate = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; + + return FlutterMap( + mapController: _mapController.mapController, + options: MapOptions( + keepAlive: true, + initialCenter: _initialCenter, + initialZoom: widget.cameraZoom, + ), + children: [ + TileLayer( + urlTemplate: mapTemplate, + fallbackUrl: fallbackTemplate, + tileBuilder: (context, tile, __) => switch (brightness) { + Brightness.light => tile, + Brightness.dark => darkModeTilesContainerBuilder(context, tile), + }, + userAgentPackageName: switch (CurrentPlatform.type) { + PlatformType.ios => 'io.getstream.flutter', + PlatformType.android => 'io.getstream.chat.android.flutter.sample', + _ => 'unknown', + }, + ), + AnimatedMarkerLayer( + markers: [ + AnimatedMarker( + height: widget.markerSize, + width: widget.markerSize, + point: widget.coordinates.toLatLng(), + builder: (context, animation) => widget.markerBuilder( + context, + animation, + widget.markerSize, + ), + ), + ], + ), + if (widget.showLocateMeButton) + SimpleMapLocateMeButton( + onPressed: () => _mapController.animateTo( + zoom: widget.cameraZoom, + curve: Curves.easeInOut, + dest: widget.coordinates.toLatLng(), + ), + ), + ], + ); + } + + @override + void dispose() { + _mapController.dispose(); + super.dispose(); + } +} + +class SimpleMapLocateMeButton extends StatelessWidget { + const SimpleMapLocateMeButton({ + super.key, + this.onPressed, + this.alignment = AlignmentDirectional.topEnd, + }); + + final AlignmentGeometry alignment; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final colorTheme = theme.colorTheme; + + return Align( + alignment: alignment, + child: Padding( + padding: const EdgeInsets.all(8), + child: FloatingActionButton.small( + onPressed: onPressed, + shape: const CircleBorder(), + foregroundColor: colorTheme.accentPrimary, + backgroundColor: colorTheme.barsBg, + child: const Icon(Icons.near_me_rounded), + ), + ), + ); + } +} + +extension on LocationCoordinates { + LatLng toLatLng() => LatLng(latitude, longitude); +} diff --git a/sample_app/macos/Flutter/Flutter-Debug.xcconfig b/sample_app/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 4b81f9b2d2..0000000000 --- a/sample_app/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/sample_app/macos/Flutter/Flutter-Release.xcconfig b/sample_app/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5caa9d1579..0000000000 --- a/sample_app/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/sample_app/macos/Runner/DebugProfile.entitlements b/sample_app/macos/Runner/DebugProfile.entitlements index 0eaccf1418..6bc96b3bc4 100644 --- a/sample_app/macos/Runner/DebugProfile.entitlements +++ b/sample_app/macos/Runner/DebugProfile.entitlements @@ -12,5 +12,7 @@ com.apple.security.network.server + com.apple.security.personal-information.location + diff --git a/sample_app/macos/Runner/Info.plist b/sample_app/macos/Runner/Info.plist index 4789daa6a4..49bb9bb13c 100644 --- a/sample_app/macos/Runner/Info.plist +++ b/sample_app/macos/Runner/Info.plist @@ -28,5 +28,7 @@ MainMenu NSPrincipalClass NSApplication + NSLocationUsageDescription + We need access to your location to share it in the chat. diff --git a/sample_app/macos/Runner/Release.entitlements b/sample_app/macos/Runner/Release.entitlements index a0463869a9..731447a00b 100644 --- a/sample_app/macos/Runner/Release.entitlements +++ b/sample_app/macos/Runner/Release.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.client + com.apple.security.personal-information.location + diff --git a/sample_app/pubspec.yaml b/sample_app/pubspec.yaml index 28d36a6282..72ab324fca 100644 --- a/sample_app/pubspec.yaml +++ b/sample_app/pubspec.yaml @@ -20,6 +20,7 @@ environment: flutter: ">=3.27.4" dependencies: + avatar_glow: ^3.0.0 collection: ^1.17.2 firebase_core: ^3.0.0 firebase_messaging: ^15.0.0 @@ -27,16 +28,21 @@ dependencies: sdk: flutter flutter_app_badger: ^1.5.0 flutter_local_notifications: ^18.0.1 + flutter_map: ^8.1.1 + flutter_map_animations: ^0.9.0 flutter_secure_storage: ^9.2.2 flutter_slidable: ^3.1.1 flutter_svg: ^2.0.10+1 + geolocator: ^13.0.0 go_router: ^14.6.2 + latlong2: ^0.9.1 lottie: ^3.1.2 provider: ^6.0.5 + rxdart: ^0.28.0 sentry_flutter: ^8.3.0 - stream_chat_flutter: ^9.16.0 - stream_chat_localizations: ^9.16.0 - stream_chat_persistence: ^9.16.0 + stream_chat_flutter: ^10.0.0-beta.5 + stream_chat_localizations: ^10.0.0-beta.5 + stream_chat_persistence: ^10.0.0-beta.5 streaming_shared_preferences: ^2.0.0 uuid: ^4.4.0 video_player: ^2.8.7