diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a640f740b2..7a351526bb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -25,7 +25,6 @@ updates: # All packages grouped into a single configuration using multi-directory support - package-ecosystem: "pub" directories: - - "/sample_app" - "/packages/stream_chat" - "/packages/stream_chat_flutter_core" - "/packages/stream_chat_flutter" diff --git a/.github/workflows/distribute_external.yml b/.github/workflows/distribute_external.yml index dde97d8fdd..7fae497785 100644 --- a/.github/workflows/distribute_external.yml +++ b/.github/workflows/distribute_external.yml @@ -71,7 +71,6 @@ jobs: with: flutter-version: ${{ env.FLUTTER_VERSION }} channel: stable - cache: true cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} - name: Setup Ruby @@ -103,13 +102,16 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '26.3' - name: "Install Flutter" uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} channel: stable - cache: true cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} - name: Setup Ruby diff --git a/.github/workflows/distribute_internal.yml b/.github/workflows/distribute_internal.yml index 19969d27cc..11da883f75 100644 --- a/.github/workflows/distribute_internal.yml +++ b/.github/workflows/distribute_internal.yml @@ -4,8 +4,8 @@ on: push: branches: - master - # TODO: Remove feat/design-refresh once merged to master - - feat/design-refresh + # TODO: Remove once merged to master + - v10.0.0 workflow_dispatch: inputs: platform: @@ -75,7 +75,6 @@ jobs: with: flutter-version: ${{ env.FLUTTER_VERSION }} channel: stable - cache: true cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} - name: "Install Tools" @@ -113,13 +112,16 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '26.3' - name: "Install Flutter" uses: subosito/flutter-action@v2 with: flutter-version: ${{ env.FLUTTER_VERSION }} channel: stable - cache: true cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} - name: "Install Tools" @@ -144,3 +146,50 @@ jobs: MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }} run: bundle exec fastlane distribute_to_firebase + + # TODO: Remove once feat/design-refresh is merged to master + ios_testflight: + needs: determine_platforms + if: ${{ needs.determine_platforms.outputs.run_ios == 'true' }} + runs-on: macos-15 # Requires xcode 15 or later + timeout-minutes: 30 + steps: + - name: Connect Bot + uses: webfactory/ssh-agent@v0.9.1 + with: + ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }} + + - name: "Git Checkout" + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '26.3' + + - name: "Install Flutter" + uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: stable + cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + + - name: "Install Tools" + run: flutter pub global activate melos + + - name: "Bootstrap Workspace" + run: melos bootstrap + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + working-directory: sample_app/ios + + - name: Distribute to TestFlight Internal + working-directory: sample_app/ios + env: + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }} + run: bundle exec fastlane distribute_to_testflight_internal diff --git a/.github/workflows/legacy_version_analyze.yml b/.github/workflows/legacy_version_analyze.yml index 7705c53b8b..443dcc7560 100644 --- a/.github/workflows/legacy_version_analyze.yml +++ b/.github/workflows/legacy_version_analyze.yml @@ -3,7 +3,7 @@ name: legacy_version_analyze env: # Note: The versions below should be manually updated after a new stable # version comes out. - flutter_version: "3.27.4" + flutter_version: "3.38.1" on: push: @@ -44,7 +44,6 @@ jobs: with: flutter-version: ${{ env.flutter_version }} channel: stable - cache: true cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} - name: 📊 Analyze and test packages diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 5debb88dba..f7606bf510 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -38,7 +38,6 @@ jobs: with: flutter-version: ${{ env.flutter_version }} channel: stable - cache: true cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} - name: "Install Tools" run: | @@ -67,7 +66,6 @@ jobs: with: flutter-version: ${{ env.flutter_version }} channel: stable - cache: true cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} - name: "Install Tools" run: | @@ -94,7 +92,6 @@ jobs: with: flutter-version: ${{ env.flutter_version }} channel: stable - cache: true cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} # This step is needed due to https://github.com/actions/runner-images/issues/11279 - name: Install SQLite3 @@ -168,8 +165,11 @@ jobs: with: flutter-version: ${{ env.flutter_version }} channel: stable - cache: true cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} + - uses: maxim-lobanov/setup-xcode@v1 + if: matrix.platform == 'ios' + with: + xcode-version: '26.3' - name: "Install Tools" run: flutter pub global activate melos - name: "Bootstrap Workspace" diff --git a/.github/workflows/update_goldens.yml b/.github/workflows/update_goldens.yml index af6a6cff61..c81affdc59 100644 --- a/.github/workflows/update_goldens.yml +++ b/.github/workflows/update_goldens.yml @@ -16,7 +16,6 @@ jobs: with: flutter-version: "3.x" channel: stable - cache: true cache-key: flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }} - name: 📦 Install Tools diff --git a/.gitignore b/.gitignore index 981d572c14..06a0c25816 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ GeneratedPluginRegistrant.* **/ios/**/xcuserdata **/ios/.generated/ **/ios/Flutter/App.framework +**/ios/Flutter/ephemeral **/ios/Flutter/Flutter.framework **/ios/Flutter/Flutter.podspec **/ios/Flutter/Generated.xcconfig diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..7d2892bd2b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,148 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +This is a Dart/Flutter monorepo for Stream Chat's official Flutter SDK, managed with [Melos](https://pub.dev/packages/melos). All packages live under `packages/`. + +## Common Commands + +### Setup +```bash +melos bootstrap # Fetch and link all dependencies (equivalent to pub get for all packages) +``` + +### Testing +```bash +melos run test:all # Run all Dart & Flutter tests +melos run test:dart # Run Dart-only tests +melos run test:flutter # Run Flutter tests +# Run tests in a specific package: +cd packages/stream_chat_flutter && flutter test +cd packages/stream_chat_flutter && flutter test test/src/path/to/test_file.dart +``` + +### Golden Tests +```bash +melos run update:goldens # Regenerate all golden image files +# In CI, CI goldens are used; locally, platform goldens are used (configured via alchemist) +``` + +### Linting & Formatting +```bash +melos run lint:all # Run analyze + format +melos run analyze # Run dart analyze --fatal-infos on all packages +melos run format # Check formatting (page width: 120) +``` + +### Code Generation +```bash +melos run generate:all # Run build_runner for all packages +melos run generate:flutter # Run build_runner for Flutter packages only +melos run generate:dart # Run build_runner for Dart packages only +``` + +### Versioning +```bash +melos run version:update # Regenerate version.dart from pubspec.yaml (runs automatically after bootstrap) +``` + +## Package Architecture + +The SDK is layered — each package builds on top of the previous: + +``` +stream_chat # Pure Dart, no Flutter dependency + └── stream_chat_persistence # Local disk cache using Drift (optional) + └── stream_chat_flutter_core # Flutter business logic, no UI + └── stream_chat_flutter # Full UI component library + └── stream_chat_localizations # i18n for UI components +``` + +### `stream_chat` +Low-level Dart client. Key types: +- `StreamChatClient` — central API client, manages WebSocket, REST, and state +- `Channel` — represents a chat channel, has its own state and streaming APIs +- Models in `lib/src/core/models/`: `Message`, `User`, `Member`, `Reaction`, `Poll`, `Event`, `Attachment`, `Draft`, etc. +- Generated code (`.g.dart`, `.freezed.dart`) for JSON serialization/immutable models — do not edit manually + +### `stream_chat_flutter_core` +Business logic layer. Key types: +- `StreamChatCore` — root widget, manages app lifecycle, WebSocket reconnection, and connectivity +- `StreamChannel` — provides a `Channel` to the widget tree via `StreamChannel.of(context)` +- `PagedValueNotifier` — base class for all list controllers +- Controllers: `StreamChannelListController`, `StreamMessageListController` (via `MessageListCore`), `StreamUserListController`, `StreamMemberListController`, `StreamThreadListController`, `StreamDraftListController`, `StreamMessageReminderListController`, `StreamPollController` +- `BetterStreamBuilder` — efficient StreamBuilder that only rebuilds when data changes + +### `stream_chat_flutter` +Full UI package. Key architectural points: + +**Root widget hierarchy:** +`StreamChat` → `StreamChatTheme` → `StreamChatConfiguration` → `StreamChatCore` → app content + +**Theming:** `StreamChatThemeData` (accessed via `StreamChatTheme.of(context)`) contains per-component theme data objects. Components read their theme from context. `StreamChatConfigurationData` holds non-theme UI config. + +**Widget tree integration pattern:** +- `StreamChat.of(context)` — get the chat state (current user, client) +- `StreamChannel.of(context)` — get the current channel state +- `StreamChatTheme.of(context)` — get current theme data + +**Key UI components:** +- `StreamChannelListView` + `StreamChannelListTile` — channel list using `StreamChannelListController` +- `StreamMessageListView` — message list with floating date dividers, unread indicators, thread separators +- `StreamMessageInput` (legacy) / `StreamChatMessageComposer` (new design system) — message composition +- `StreamMessageWidget` — renders individual messages with attachments, reactions, threads +- Scroll views in `lib/src/scroll_view/` — generic paged scroll views for channels, threads, members, users, drafts, polls + +**New design system components** (`lib/src/components/`): +- `StreamUserAvatar`, `StreamChannelAvatar`, `StreamUserAvatarGroup` — avatar components; these are chat-domain wrappers around the base components in `stream_core_flutter` +- `StreamChatMessageComposer` — new composer using `MessageComposerFactory` for custom layouts + +**Golden tests:** Use `alchemist` package. Platform goldens used locally, CI goldens used in CI (detected via `CI`/`GITHUB_ACTIONS` env vars). Goldens stored alongside tests in `goldens/` subdirectories. + +### `stream_chat_localizations` +Provides `StreamChatLocalizations` — Flutter localizations delegate with translations for all UI strings. + +### `stream_chat_persistence` +Optional local persistence using Drift (SQLite). Implements `ChatPersistenceClient` from `stream_chat`. + +## Code Style + +- Line length: **120 characters** (configured in `analysis_options.yaml`) +- Imports: always use package imports (`always_use_package_imports`), not relative imports +- All public APIs **must** have doc comments (`public_member_api_docs`) +- Sort constructors first, unnamed constructors before named +- Prefer `const` constructors, `final` locals, single quotes +- Trailing commas: `preserve` (formatter setting) +- Generated files (`.g.dart`, `.freezed.dart`) are excluded from analysis + +## PR & Commit Conventions + +PR titles follow [Conventional Commits](https://www.conventionalcommits.org/): +- `fix(scope): description` — bug fix +- `feat(scope): description` — new feature +- `refactor(scope)!: description` — breaking change +- `chore(scope): description`, `docs:`, `test:`, etc. + +After modifying any package, update its `CHANGELOG.md`. + +## Figma Designs + +UI designs for this SDK are in the **Chat SDK Design system** Figma project. Use the Figma MCP server to look up designs when implementing or updating UI components. + +## `stream_core_flutter` (external sibling repo) + +Basic UI components that can be shared across Stream products live in the `stream_core_flutter` package in the **stream-core-flutter** repository (a sibling repo, not inside this monorepo). These components: +- Never depend on chat domain models (`Channel`, `Message`, `User`, etc.) +- Provide primitive building blocks: avatars, layout primitives, theming tokens, etc. + +Components in this repo can be wrappers around those base components, adding chat domain models and extra logic on top. For example, `StreamChannelAvatar` wraps the base `StreamAvatarGroup` from `stream_core_flutter` and adds channel-specific member resolution logic. + +`stream_core_flutter` types used here are re-exported via a `show` allowlist in `lib/stream_chat_flutter.dart`. When adding a new type from `stream_core_flutter`, add it to that allowlist. + +## Dependency Management + +Dependencies are centrally managed in `melos.yaml` under `command.bootstrap.dependencies`. Do **not** edit version constraints directly in individual `pubspec.yaml` files — update `melos.yaml` and run `melos bootstrap`. + +> Note: `stream_chat_flutter` uses a local path dependency to `stream_core_flutter` (pointing to the sibling repo) when making changes to both repos together. Use a git dependency when no local changes to `stream_core_flutter` are needed. diff --git a/analysis_options.yaml b/analysis_options.yaml index d5ac4a48f4..6e7d2449c4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,6 +4,10 @@ analyzer: - packages/*/lib/scrollable_positioned_list/** - packages/*/lib/**/*.freezed.dart +formatter: + page_width: 120 + trailing_commas: preserve + linter: rules: # these rules are documented on and in the same order as @@ -19,7 +23,6 @@ linter: - control_flow_in_finally - empty_statements - hash_and_equals - - invariant_booleans - literal_only_boolean_expressions - no_adjacent_strings_in_list - no_duplicate_case_values @@ -40,7 +43,9 @@ linter: - avoid_null_checks_in_equality_operators - avoid_positional_boolean_parameters - avoid_private_typedef_functions - - avoid_redundant_argument_values + # Does not always make sense to remove them; it also makes it hard + # to notice future breaking changes. + # - avoid_redundant_argument_values - avoid_return_types_on_setters - avoid_returning_null_for_void - avoid_shadowing_type_parameters @@ -64,7 +69,6 @@ linter: - leading_newlines_in_multiline_strings - library_names - library_prefixes - - lines_longer_than_80_chars - missing_whitespace_between_adjacent_strings - non_constant_identifier_names - null_closures @@ -81,7 +85,6 @@ linter: - prefer_const_declarations - prefer_const_literals_to_create_immutables - prefer_contains - - prefer_equal_for_default_values - prefer_final_fields - prefer_final_in_for_each - prefer_final_locals diff --git a/melos.yaml b/melos.yaml index 2155166eb0..32a2c06b06 100644 --- a/melos.yaml +++ b/melos.yaml @@ -1,4 +1,4 @@ -name: stream_chat_flutter +name: stream_chat_flutter_workspace repository: https://github.com/GetStream/stream-chat-flutter packages: @@ -18,13 +18,14 @@ command: bootstrap: # Dart and Flutter environment used in the project. environment: - sdk: ^3.6.2 + sdk: ^3.10.0 # We are not using carat '^' syntax here because flutter don't follow semantic versioning. - flutter: ">=3.27.4" + flutter: ">=3.38.1" # 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 @@ -34,7 +35,7 @@ command: device_info_plus: '>=11.0.0 <13.0.0' diacritic: ^0.1.5 dio: ^5.4.3+1 - drift: ^2.22.1 + drift: ^2.28.0 equatable: ^2.0.5 ezanimation: ^0.6.0 firebase_core: ^3.0.0 @@ -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.2.2 @@ -68,24 +73,26 @@ command: package_info_plus: ">=8.3.0 <10.0.0" path: ^1.8.3 path_provider: ^2.1.3 - photo_manager: ^3.2.0 + photo_manager: ^3.8.3 photo_view: ^0.15.0 provider: ^6.0.5 rate_limiter: ^1.0.0 - record: ">=5.2.0 <7.0.0" + record: ^6.2.0 responsive_builder: ^0.7.0 rxdart: ^0.28.0 sentry_flutter: ^8.3.0 share_plus: ">=11.0.0 <13.0.0" shimmer: ^3.0.0 sqlite3_flutter_libs: ^0.5.26 - stream_chat: ^9.23.0 - stream_chat_flutter: ^9.23.0 - stream_chat_flutter_core: ^9.23.0 - stream_chat_localizations: ^9.23.0 - stream_chat_persistence: ^9.23.0 + stream_chat: ^10.0.0-beta.13 + stream_chat_flutter: ^10.0.0-beta.13 + stream_chat_flutter_core: ^10.0.0-beta.13 + stream_chat_localizations: ^10.0.0-beta.13 + stream_chat_persistence: ^10.0.0-beta.13 streaming_shared_preferences: ^2.0.0 svg_icon_widget: ^0.0.1 + # TODO: Replace with hosted version before merging PR + stream_core_flutter: ^0.1.0 synchronized: ^3.1.0+1 thumblr: ^0.0.4 url_launcher: ^6.3.0 @@ -95,10 +102,10 @@ command: # List of all the dev_dependencies used in the project. dev_dependencies: - alchemist: ">=0.11.0 <0.14.0" + alchemist: ^0.13.0 build_runner: ^2.4.9 connectivity_plus_platform_interface: ^2.0.0 - drift_dev: ^2.22.1 + drift_dev: ^2.28.0 fake_async: ^1.3.1 faker_dart: ^0.2.1 flutter_launcher_icons: ^0.14.2 @@ -109,6 +116,7 @@ command: path_provider_platform_interface: ^2.0.0 plugin_platform_interface: ^2.0.0 test: ^1.24.6 + theme_extensions_builder: ^7.2.0 hooks: # Updates the version.dart file after bootstrapping with the current version from pubspec.yaml diff --git a/migrations/redesign/README.md b/migrations/redesign/README.md new file mode 100644 index 0000000000..2997db07d5 --- /dev/null +++ b/migrations/redesign/README.md @@ -0,0 +1,139 @@ +# Stream Chat Flutter UI Redesign Migration Guide + +This folder contains migration guides for the redesigned UI components in Stream Chat Flutter SDK. + +## Overview + +The redesigned components aim to provide: +- Simplified and consistent APIs +- Better theme integration +- Improved developer experience +- Reduced boilerplate + +Each component migration guide contains specific details about the changes and how to migrate. + +## Theming + +The redesigned components use `StreamTheme` for theming. If no `StreamTheme` is provided, a default theme is automatically created based on `Theme.of(context).brightness` (light or dark mode). + +To customize the default theming, add `StreamTheme` as a theme extension to your `MaterialApp`: + +```dart +MaterialApp( + theme: ThemeData( + extensions: [ + StreamTheme( + brightness: Brightness.light, + colorScheme: StreamColorScheme.light().copyWith( + // Customize colors... + ), + avatarTheme: const StreamAvatarThemeData( + // Customize avatar defaults... + ), + ), + ], + ), + // ... +) +``` + +You can also use the convenience factories `StreamTheme.light()` or `StreamTheme.dark()` as a starting point. + + +## Component factories + +In the redesigned components we don't use builders in the constructors anymore, but have a centralized component factory. +The component factory contains product agnotic component builders, such as the button and the avatar, and also product specific component builders, such as the channel list item. +You can supply your component factory at any point in the widget tree, but you would usually wrap your full app around it. + +An example of a component factory with custom buttons and a custom channel list item: + +```dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return StreamComponentFactory( + builders: StreamComponentBuilders( + button: (context, props) => switch (props.type) { + StreamButtonType.solid => ElevatedButton( + onPressed: props.onTap, + child: Text(props.label ?? ''), + ), + StreamButtonType.outline => OutlinedButton(onPressed: props.onTap, child: Text(props.label ?? '')), + StreamButtonType.ghost => TextButton(onPressed: props.onTap, child: Text(props.label ?? '')), + }, + extensions: streamChatComponentBuilders( + channelListItem: (context, props) => StreamChannelListTile( + title: Text(props.channel.name ?? ''), + avatar: StreamChannelAvatar(channel: props.channel), + onTap: props.onTap, + onLongPress: props.onLongPress, + selected: props.selected, + ), + ), + ), + child: ... + ); + } +} +``` + +You should make the builder themselves as simple as possible by extracting this into separate widgets, such as this: + +```dart +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return StreamComponentFactory( + builders: StreamComponentBuilders( + button: (context, props) => MyCustomButton(props: props), + ), + child: ... + ); + } +} + +class MyCustomButton extends StatelessWidget { + const MyCustomButton({super.key, required this.props}); + + final StreamButtonProps props; + + @override + Widget build(BuildContext context) { + return switch (props.type) { + StreamButtonType.solid => ElevatedButton( + onPressed: props.onTap, + child: Text(props.label ?? ''), + ), + StreamButtonType.outline => OutlinedButton(onPressed: props.onTap, child: Text(props.label ?? '')), + StreamButtonType.ghost => TextButton(onPressed: props.onTap, child: Text(props.label ?? '')), + }; + } +} +``` + +## Components + +| Component | Migration Guide | +|-----------|-----------------| +| Stream Avatar | [stream_avatar.md](stream_avatar.md) | +| Channel List Item | [channel_list_item.md](channel_list_item.md) | +| Message Actions | [message_actions.md](message_actions.md) | +| Reaction Picker / Reactions | [reaction_picker.md](reaction_picker.md) | +| Image CDN & Thumbnails | [image_cdn.md](image_cdn.md) | +| Message Widget & Message List | [message_widget.md](message_widget.md) | +| Message Composer | [message_composer.md](message_composer.md) | +| Unread Indicator | [unread_indicator.md](unread_indicator.md) | +| Reaction List & Detail Sheet | [reaction_list.md](reaction_list.md) | +| Audio Waveform Theme | [audio_theme.md](audio_theme.md) | +| Attachments & Polls | [attachments_and_polls.md](attachments_and_polls.md) | +| Headers, Icons & Configuration | [headers_and_icons.md](headers_and_icons.md) | +| Localizations | [localizations.md](localizations.md) | + +## Need Help? + +If you encounter any issues during migration or have questions, please [open an issue](https://github.com/GetStream/stream-chat-flutter/issues) on GitHub. diff --git a/migrations/redesign/attachments_and_polls.md b/migrations/redesign/attachments_and_polls.md new file mode 100644 index 0000000000..d8af3a91c9 --- /dev/null +++ b/migrations/redesign/attachments_and_polls.md @@ -0,0 +1,385 @@ +# Attachments & Polls Migration Guide + +This guide covers the migration for the redesigned attachment components, voice recording player, and poll interactor theming in Stream Chat Flutter SDK. + +--- + +## Table of Contents + +- [Quick Reference](#quick-reference) +- [Architecture: Props + Component Factory](#architecture-props--component-factory) +- [StreamImageAttachment](#streamimageattachment) +- [StreamGalleryAttachment](#streamgalleryattachment) +- [StreamFileAttachment](#streamfileattachment) +- [StreamVideoAttachment](#streamvideoattachment) +- [StreamGiphyAttachment](#streamgiphyattachment) +- [StreamUrlAttachment → StreamLinkPreviewAttachment](#streamurlattachment--streamlinkpreviewattachment) +- [StreamVoiceRecordingAttachmentPlaylist](#streamvoicerecordingattachmentplaylist) +- [Attachment Builders](#attachment-builders) +- [StreamPollInteractorThemeData](#streampollinteractorthemedata) +- [StreamVoiceRecordingAttachmentThemeData](#streamvoicerecordingattachmentthemedata) +- [Migration Checklist](#migration-checklist) + +--- + +## Quick Reference + +| Symbol | Change | +|--------|--------| +| All attachment widgets | Adopt **Props + Component Factory** pattern (see below) | +| `StreamUrlAttachment` | **Renamed** to `StreamLinkPreviewAttachment` | +| `UrlAttachmentBuilder` | **Renamed** to `LinkPreviewAttachmentBuilder` | +| `shape` parameter | **Removed** from all attachment widgets | +| `constraints` parameter | Changed from required to optional on most attachments | +| `StreamImageAttachment` thumbnail params | `imageThumbnailSize`, `imageThumbnailResizeType`, `imageThumbnailCropType` replaced by `ImageResize? resize` | +| `StreamFileAttachment.backgroundColor` | **Removed** | +| `StreamPollInteractorThemeData` | Fully redesigned — old properties removed, new structured theme | +| `StreamVoiceRecordingAttachmentThemeData` | Fully redesigned — old properties removed, new design-token-based theme | + +--- + +## Architecture: Props + Component Factory + +All attachment widgets now follow a consistent **Props + Component Factory** pattern: + +1. **Public widget** (e.g. `StreamImageAttachment`) — thin wrapper that reads from `StreamComponentFactory` or falls back to a default implementation. +2. **Props class** (e.g. `StreamImageAttachmentProps`) — holds all configuration. Properties that were previously direct constructor parameters now live here. +3. **Default implementation** (e.g. `DefaultStreamImageAttachment`) — the built-in rendering. + +This means you can replace any attachment's rendering globally via `StreamComponentFactory`: + +```dart +StreamComponentFactory( + builders: StreamComponentBuilders( + extensions: streamChatComponentBuilders( + imageAttachment: (context, props) => MyCustomImageAttachment(props: props), + fileAttachment: (context, props) => MyCustomFileAttachment(props: props), + ), + ), + child: ... +) +``` + +For widget users, the public constructor API is largely unchanged — you still pass `message`, `image`, `constraints`, etc. directly. They are forwarded into the `props` object internally. + +--- + +## StreamImageAttachment + +### Breaking Changes: + +- `shape` parameter removed +- `constraints` changed from `BoxConstraints` (required) to `BoxConstraints?` (optional, auto-sized) +- `imageThumbnailSize`, `imageThumbnailResizeType`, and `imageThumbnailCropType` replaced by a single `ImageResize? resize` parameter + +### Migration: + +**Before:** +```dart +StreamImageAttachment( + message: message, + image: attachment, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + constraints: BoxConstraints.tight(const Size(300, 300)), + imageThumbnailSize: const Size(300, 300), + imageThumbnailResizeType: 'crop', + imageThumbnailCropType: 'center', +) +``` + +**After:** +```dart +StreamImageAttachment( + message: message, + image: attachment, + constraints: BoxConstraints.tight(const Size(300, 300)), + resize: ImageResize( + width: 300, + height: 300, + mode: ResizeMode.crop, + crop: CropMode.center, + ), +) +``` + +> **Note:** Shape customization is now handled via theming or the component factory pattern. When `resize` is null, the size is auto-calculated from layout constraints. + +--- + +## StreamGalleryAttachment + +### Breaking Changes: + +- `shape` parameter removed +- `constraints` is now optional + +### Migration: + +**Before:** +```dart +StreamGalleryAttachment( + message: message, + attachments: attachments, + shape: const RoundedRectangleBorder(...), + constraints: BoxConstraints(...), + itemBuilder: itemBuilder, +) +``` + +**After:** +```dart +StreamGalleryAttachment( + message: message, + attachments: attachments, + itemBuilder: itemBuilder, +) +``` + +--- + +## StreamFileAttachment + +### Breaking Changes: + +- `shape` parameter removed +- `backgroundColor` parameter removed +- `constraints` is now optional + +### Migration: + +**Before:** +```dart +StreamFileAttachment( + message: message, + file: attachment, + shape: const RoundedRectangleBorder(...), + backgroundColor: Colors.grey, + constraints: BoxConstraints(...), +) +``` + +**After:** +```dart +StreamFileAttachment( + message: message, + file: attachment, +) +``` + +--- + +## StreamVideoAttachment + +### Breaking Changes: + +- `shape` parameter removed +- `constraints` is now optional + +### Migration: + +**Before:** +```dart +StreamVideoAttachment( + message: message, + video: attachment, + shape: const RoundedRectangleBorder(...), + constraints: BoxConstraints.tight(const Size(300, 300)), +) +``` + +**After:** +```dart +StreamVideoAttachment( + message: message, + video: attachment, + constraints: BoxConstraints.tight(const Size(300, 300)), +) +``` + +--- + +## StreamGiphyAttachment + +### Breaking Changes: + +- `shape` parameter removed +- `constraints` is now optional + +--- + +## StreamUrlAttachment → StreamLinkPreviewAttachment + +### Breaking Changes: + +- **Renamed** from `StreamUrlAttachment` to `StreamLinkPreviewAttachment` +- `messageTheme` parameter removed +- `hostDisplayName` parameter removed +- `shape` parameter removed +- `constraints` is now optional + +### Migration: + +**Before:** +```dart +StreamUrlAttachment( + message: message, + urlAttachment: attachment, + messageTheme: theme.ownMessageTheme, + hostDisplayName: 'GitHub', +) +``` + +**After:** +```dart +StreamLinkPreviewAttachment( + message: message, + urlAttachment: attachment, +) +``` + +> **Note:** The corresponding builder was also renamed from `UrlAttachmentBuilder` to `LinkPreviewAttachmentBuilder`. + +--- + +## StreamVoiceRecordingAttachmentPlaylist + +### Breaking Changes: + +- `shape` parameter removed +- `constraints` is now optional +- New `itemDecorator` parameter for wrapping individual voice recording items +- New `voiceRecordingTitle` parameter + +### Migration: + +**Before:** +```dart +StreamVoiceRecordingAttachmentPlaylist( + message: message, + voiceRecordings: attachments, + shape: const RoundedRectangleBorder(...), + constraints: BoxConstraints(...), +) +``` + +**After:** +```dart +StreamVoiceRecordingAttachmentPlaylist( + message: message, + voiceRecordings: attachments, +) +``` + +--- + +## Attachment Builders + +| Old Builder | New Builder | +|------------|------------| +| `UrlAttachmentBuilder` | `LinkPreviewAttachmentBuilder` | +| All others | Same name, updated constructor signatures | + +All builders have had their `shape` and `padding` parameters removed. If you subclass any attachment builder, update to use the new Props-based attachment constructors. + +--- + +## StreamPollInteractorThemeData + +### Breaking Changes: + +The theme has been fully redesigned. All previous properties have been removed and replaced with a structured theme using design system tokens. + +**Removed properties:** +- `pollTitleStyle` → replaced by `titleTextStyle` +- `pollSubtitleStyle` → replaced by `subtitleTextStyle` +- `pollOptionTextStyle`, `pollOptionVoteCountTextStyle` → moved to `optionStyle` (`StreamPollOptionStyle`) +- `pollOptionCheckboxShape`, `pollOptionCheckboxCheckColor`, `pollOptionCheckboxActiveColor`, `pollOptionCheckboxBorderSide` → moved to `optionStyle.checkboxStyle` (`StreamCheckboxStyle`) +- `pollOptionVotesProgressBarMinHeight`, `pollOptionVotesProgressBarTrackColor`, `pollOptionVotesProgressBarValueColor`, `pollOptionVotesProgressBarWinnerColor`, `pollOptionVotesProgressBarBorderRadius` → moved to `optionStyle.progressBarStyle` (`StreamProgressBarStyle`) +- `pollActionButtonStyle` → replaced by `primaryActionStyle` and `secondaryActionStyle` (`StreamButtonThemeStyle`) +- `pollActionDialogTitleStyle`, `pollActionDialogTextFieldStyle`, `pollActionDialogTextFieldFillColor`, `pollActionDialogTextFieldBorderRadius` → removed + +### Migration: + +**Before:** +```dart +StreamPollInteractorThemeData( + pollTitleStyle: TextStyle(fontWeight: FontWeight.bold), + pollActionButtonStyle: ButtonStyle(...), + pollOptionVotesProgressBarValueColor: Colors.blue, +) +``` + +**After:** +```dart +StreamPollInteractorThemeData( + titleTextStyle: TextStyle(fontWeight: FontWeight.bold), + primaryActionStyle: StreamButtonThemeStyle.from( + borderColor: Colors.blue, + ), + optionStyle: StreamPollOptionStyle( + progressBarStyle: StreamProgressBarStyle( + fillColor: Colors.blue, + ), + ), +) +``` + +--- + +## StreamVoiceRecordingAttachmentThemeData + +### Breaking Changes: + +The theme has been fully redesigned using `theme_extensions_builder` code generation. + +**Removed properties:** +- `backgroundColor` → removed (handled by attachment container styling) +- `playIcon`, `pauseIcon`, `loadingIndicator` → removed (handled by `controlButtonStyle`) +- `audioControlButtonStyle` → replaced by `controlButtonStyle` (`StreamButtonThemeStyle`) +- `speedControlButtonStyle` → replaced by `speedToggleStyle` (`StreamPlaybackSpeedToggleStyle`) +- `audioWaveformSliderTheme` → replaced by `waveformStyle` (`StreamAudioWaveformThemeData`) + +**Retained (renamed):** +- `titleTextStyle` → `titleTextStyle` (unchanged) +- `durationTextStyle` → `durationTextStyle` (unchanged) + +**New properties:** +- `activeDurationTextStyle` — text style for duration while playing + +### Migration: + +**Before:** +```dart +StreamVoiceRecordingAttachmentThemeData( + backgroundColor: Colors.grey, + audioControlButtonStyle: ButtonStyle(...), + speedControlButtonStyle: ButtonStyle(...), + durationTextStyle: TextStyle(...), +) +``` + +**After:** +```dart +StreamVoiceRecordingAttachmentThemeData( + controlButtonStyle: StreamButtonThemeStyle.from(...), + speedToggleStyle: StreamPlaybackSpeedToggleStyle(...), + durationTextStyle: TextStyle(...), + activeDurationTextStyle: TextStyle(...), +) +``` + +--- + +## Migration Checklist + +- [ ] Remove `shape` parameter from all attachment widget usages +- [ ] Replace `StreamUrlAttachment` with `StreamLinkPreviewAttachment` +- [ ] Replace `UrlAttachmentBuilder` with `LinkPreviewAttachmentBuilder` +- [ ] Remove `messageTheme` and `hostDisplayName` from link preview usage +- [ ] Replace `imageThumbnailSize` / `imageThumbnailResizeType` / `imageThumbnailCropType` with `ImageResize? resize` on `StreamImageAttachment` +- [ ] Remove `backgroundColor` from `StreamFileAttachment` usage +- [ ] Remove `shape` and `padding` from attachment builder usages +- [ ] Update `StreamPollInteractorThemeData` — see property mapping above +- [ ] Update `StreamVoiceRecordingAttachmentThemeData` — see property mapping above +- [ ] If using custom attachment builders, update to new Props-based constructors +- [ ] If using component factory, register custom builders via `streamChatComponentBuilders` diff --git a/migrations/redesign/audio_theme.md b/migrations/redesign/audio_theme.md new file mode 100644 index 0000000000..c03430f7ac --- /dev/null +++ b/migrations/redesign/audio_theme.md @@ -0,0 +1,81 @@ +# Audio Waveform Theme Migration Guide + +This guide covers the migration for the audio waveform theming changes in the Stream Chat Flutter SDK design refresh. + +--- + +## Table of Contents + +- [Overview](#overview) +- [What Changed](#what-changed) +- [New Theming Approach](#new-theming-approach) +- [Migration Checklist](#migration-checklist) + +--- + +## Overview + +The audio waveform theme types and the `StreamAudioWaveform` / `StreamAudioWaveformSlider` widgets have moved from `stream_chat_flutter` to `stream_core_flutter`. The widgets are re-exported via `stream_chat_flutter` so import paths remain unchanged, but theming is no longer done through `StreamChatThemeData`. + +--- + +## What Changed + +| Item | Before | After | +|------|--------|-------| +| `StreamAudioWaveformTheme` | Defined in `stream_chat_flutter` | Moved to `stream_core_flutter`; no longer in `StreamChatThemeData` | +| `StreamAudioWaveformSliderTheme` | Defined in `stream_chat_flutter` | Moved to `stream_core_flutter`; no longer in `StreamChatThemeData` | +| `StreamAudioWaveform` widget | In `stream_chat_flutter` | Re-exported from `stream_core_flutter` via `stream_chat_flutter` | +| `StreamAudioWaveformSlider` widget | In `stream_chat_flutter` | Re-exported from `stream_core_flutter` via `stream_chat_flutter` | +| Theming entry point | `StreamChatThemeData.audioWaveformTheme` / `.audioWaveformSliderTheme` | `StreamTheme` (via `MaterialApp.theme.extensions`) | + +--- + +## New Theming Approach + +Audio waveform theming is now part of `StreamTheme` from `stream_core_flutter`. Configure it by adding `StreamTheme` as a theme extension to your `MaterialApp`: + +**Before:** +```dart +StreamChat( + client: client, + streamChatThemeData: StreamChatThemeData( + audioWaveformTheme: StreamAudioWaveformThemeData( + waveColor: Colors.blue, + playedWaveColor: Colors.blueAccent, + ), + audioWaveformSliderTheme: StreamAudioWaveformSliderThemeData( + thumbColor: Colors.blue, + ), + ), + child: ..., +) +``` + +**After:** +```dart +MaterialApp( + theme: ThemeData( + extensions: [ + StreamTheme( + brightness: Brightness.light, + // Audio waveform theming is now part of StreamTheme's component themes. + // Refer to StreamThemeData for available audio waveform properties. + ), + ], + ), + home: StreamChat(client: client, child: ...), +) +``` + +> **Note:** If no `StreamTheme` extension is provided, a default theme is automatically derived from `Theme.of(context).brightness`. + +--- + +## Migration Checklist + +- [ ] Remove any `StreamChatThemeData.audioWaveformTheme` usages +- [ ] Remove any `StreamChatThemeData.audioWaveformSliderTheme` usages +- [ ] Remove `audioWaveformTheme` and `audioWaveformSliderTheme` from any `StreamChatThemeData.copyWith()` calls — these parameters no longer exist and will cause a compile error +- [ ] Move audio waveform color / style customizations into a `StreamTheme` extension on `MaterialApp` +- [ ] Import paths for `StreamAudioWaveform` and `StreamAudioWaveformSlider` remain the same (`package:stream_chat_flutter/stream_chat_flutter.dart`) diff --git a/migrations/redesign/channel_list_item.md b/migrations/redesign/channel_list_item.md new file mode 100644 index 0000000000..919beccb8b --- /dev/null +++ b/migrations/redesign/channel_list_item.md @@ -0,0 +1,270 @@ +# Channel List Item Migration Guide + +This guide covers the migration for the redesigned channel list item components in Stream Chat Flutter SDK. + +--- + +## Table of Contents + +- [Quick Reference](#quick-reference) +- [StreamChannelListTile → StreamChannelListItem](#streamchannellisttile--streamchannellistitem) +- [Customizing Slots](#customizing-slots) +- [Low-level Presentational Component](#low-level-presentational-component) +- [Theme Migration](#theme-migration) +- [Migration Checklist](#migration-checklist) + +--- + +## Quick Reference + +| Old | New | +|-----|-----| +| `StreamChannelListTile` | `StreamChannelListItem` | +| Constructor props: `leading`, `title`, `subtitle`, `trailing` | `StreamChannelListItemProps` (via `StreamComponentFactory`) | +| `tileColor`, `visualDensity`, `contentPadding` | Removed — use `StreamChannelListItemThemeData` | +| `selectedTileColor` | Removed — use `StreamChannelListItemThemeData.backgroundColor` | +| `unreadIndicatorBuilder` | Removed | +| `StreamChannelPreviewThemeData` | `StreamChannelListItemThemeData` | +| `StreamChannelPreviewTheme.of(context)` | `StreamChannelListItemTheme.of(context)` | +| `StreamChatThemeData.channelPreviewTheme` | `StreamChatThemeData.channelListItemTheme` | + +--- + +## StreamChannelListTile → StreamChannelListItem + +The old `StreamChannelListTile` accepted all slot widgets directly in its constructor. The new `StreamChannelListItem` takes only the essential interaction properties. Slot customization is now done via `StreamChannelListItemProps` and the `StreamComponentFactory`. + +### Breaking Changes + +- `leading`, `title`, `subtitle`, `trailing` removed from constructor +- `tileColor` removed — use `StreamChannelListItemThemeData.backgroundColor` +- `visualDensity` removed +- `contentPadding` removed +- `selectedTileColor` removed +- `unreadIndicatorBuilder` removed +- `sendingIndicatorBuilder` removed from constructor — pass via `StreamChannelListItemProps` + +### Migration + +**Before:** +```dart +StreamChannelListTile( + channel: channel, + onTap: () => openChannel(channel), + onLongPress: () => showOptions(channel), + tileColor: Colors.white, + selectedTileColor: Colors.blue.shade50, + selected: isSelected, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + leading: StreamChannelAvatar(channel: channel), + title: StreamChannelName(channel: channel), + subtitle: ChannelListTileSubtitle(channel: channel), + trailing: ChannelLastMessageDate(channel: channel), +) +``` + +**After:** +```dart +StreamChannelListItem( + channel: channel, + onTap: () => openChannel(channel), + onLongPress: () => showOptions(channel), + selected: isSelected, +) +``` + +--- + +## Customizing Slots + +To customize the slot widgets (avatar, title, subtitle, timestamp), provide a custom builder via `StreamComponentFactory`: + +**Before:** +```dart +StreamChannelListTile( + channel: channel, + leading: MyCustomAvatar(channel: channel), + subtitle: MyCustomSubtitle(channel: channel), +) +``` + +**After:** +```dart +StreamComponentFactory( + builders: StreamComponentBuilders( + extensions: streamChatComponentBuilders( + channelListItem: (context, props) => StreamChannelListTile( + avatar: StreamChannelAvatar(channel: props.channel), + title: Text(props.channel.name ?? ''), + subtitle: Text(props.channel.lastMessageAt?.toString() ?? ''), + ), + ), + ), + child: ..., +) +``` + +--- + +## Low-level Presentational Component + +The new `StreamChannelListTile` is a low-level component that renders pre-resolved data without any channel-specific logic. Use this when you want to display a channel-shaped list item with fully controlled content (e.g., in a skeleton loader or a custom list): + +```dart +StreamChannelListTile( + avatar: StreamChannelAvatar(channel: channel), + title: Text('General'), + subtitle: Text('Last message preview'), + timestamp: Text('9:41 AM'), + unreadCount: 3, + isMuted: false, + onTap: () {}, +) +``` + +> **Note:** This widget does not subscribe to any streams — all values must be provided explicitly. + +--- + +## Theme Migration + +`StreamChannelPreviewThemeData` has been replaced by `StreamChannelListItemThemeData`. Additionally, the `StreamChannelPreviewTheme` inherited widget itself is deprecated — replace it with `StreamChannelListItemTheme`. + +### Property Mapping + +| Old (`StreamChannelPreviewThemeData`) | New (`StreamChannelListItemThemeData`) | +|---------------------------------------|----------------------------------------| +| `titleStyle` | `titleStyle` | +| `subtitleStyle` | `subtitleStyle` | +| `lastMessageAtStyle` | `timestampStyle` | +| `avatarTheme` | Removed — use `StreamAvatarThemeData` directly | +| `unreadCounterColor` | Removed — use `StreamBadgeNotificationThemeData` | +| `indicatorIconSize` | Removed | +| `lastMessageAtFormatter` | Removed from theme — pass to `ChannelLastMessageDate(formatter: ...)` | + +### New Properties + +| Property | Type | Description | +|----------|------|-------------| +| `backgroundColor` | `WidgetStateProperty?` | Background color resolved per state (default, hover, pressed, selected) | +| `borderColor` | `Color?` | Bottom border color | +| `muteIconPosition` | `MuteIconPosition?` | Whether the mute icon appears in `title` or `subtitle` row | + +### Global Theme Migration + +**Before:** +```dart +StreamChatTheme( + data: StreamChatThemeData( + channelPreviewTheme: StreamChannelPreviewThemeData( + titleStyle: TextStyle(fontWeight: FontWeight.bold), + subtitleStyle: TextStyle(color: Colors.grey), + lastMessageAtStyle: TextStyle(fontSize: 12), + unreadCounterColor: Colors.red, + ), + ), + child: ..., +) +``` + +**After:** +```dart +StreamChatTheme( + data: StreamChatThemeData( + channelListItemTheme: StreamChannelListItemThemeData( + titleStyle: TextStyle(fontWeight: FontWeight.bold), + subtitleStyle: TextStyle(color: Colors.grey), + timestampStyle: TextStyle(fontSize: 12), + // unreadCounterColor → customize via StreamBadgeNotificationThemeData + ), + ), + child: ..., +) +``` + +### Subtree Theme Override + +**Before:** +```dart +StreamChannelPreviewTheme( + data: StreamChannelPreviewThemeData( + titleStyle: TextStyle(color: Colors.blue), + ), + child: StreamChannelListView(...), +) +``` + +**After:** +```dart +StreamChannelListItemTheme( + data: StreamChannelListItemThemeData( + titleStyle: TextStyle(color: Colors.blue), + ), + child: StreamChannelListView(...), +) +``` + +### Background and Selected Color + +**Before:** +```dart +StreamChannelListTile( + channel: channel, + tileColor: Colors.white, + selectedTileColor: Colors.blue.shade50, + selected: isSelected, +) +``` + +**After:** +```dart +StreamChannelListItemTheme( + data: StreamChannelListItemThemeData( + backgroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) return Colors.blue.shade50; + return Colors.white; + }), + ), + child: StreamChannelListItem( + channel: channel, + selected: isSelected, + ), +) +``` + +### Custom Timestamp Formatter + +**Before:** +```dart +StreamChannelPreviewThemeData( + lastMessageAtFormatter: (context, date) { + return Jiffy.parseFromDateTime(date).format('d MMMM'); + }, +) +``` + +**After:** +```dart +// Pass directly to the widget — no longer a theme property +ChannelLastMessageDate( + channel: channel, + formatter: (context, date) { + return Jiffy.parseFromDateTime(date).format('d MMMM'); + }, +) +``` + +--- + +## Migration Checklist + +- [ ] Replace `StreamChannelListTile` with `StreamChannelListItem` +- [ ] Remove `tileColor`, `visualDensity`, `contentPadding`, `selectedTileColor`, `unreadIndicatorBuilder` parameters +- [ ] Move slot customization (`leading`, `title`, `subtitle`, `trailing`) to `StreamComponentFactory` +- [ ] Replace `StreamChannelPreviewTheme` inherited widget with `StreamChannelListItemTheme` — `StreamChannelPreviewTheme` is `@Deprecated` and will be removed in a future release +- [ ] Replace `StreamChatThemeData.channelPreviewTheme` with `StreamChatThemeData.channelListItemTheme` +- [ ] Rename `lastMessageAtStyle` to `timestampStyle` +- [ ] Move `lastMessageAtFormatter` from theme to `ChannelLastMessageDate(formatter: ...)` +- [ ] Replace `tileColor`/`selectedTileColor` with `StreamChannelListItemThemeData.backgroundColor` using `WidgetStateProperty` +- [ ] Replace `unreadCounterColor` with `StreamBadgeNotificationThemeData` +- [ ] Replace `avatarTheme` in `StreamChannelPreviewThemeData` with `StreamAvatarThemeData` on the avatar widget directly diff --git a/migrations/redesign/headers_and_icons.md b/migrations/redesign/headers_and_icons.md new file mode 100644 index 0000000000..4356f61eb1 --- /dev/null +++ b/migrations/redesign/headers_and_icons.md @@ -0,0 +1,231 @@ +# Headers, Icons & Configuration Migration Guide + +This guide covers several cross-cutting API changes in the Stream Chat Flutter SDK design refresh: the icon system migration, header widget defaults, the new `StreamChat.componentBuilders` parameter, and new `StreamChatConfigurationData` fields. + +--- + +## Table of Contents + +- [StreamSvgIcon Deprecated](#streamsvgicon-deprecated) +- [Header Widgets](#header-widgets) +- [StreamChat.componentBuilders](#streamchatcomponentbuilders) +- [StreamChatConfigurationData New Fields](#streamchatconfigurationdata-new-fields) +- [Migration Checklist](#migration-checklist) + +--- + +## StreamSvgIcon Deprecated + +`StreamSvgIcon` and `StreamSvgIcons` are now `@Deprecated`. Replace all usages with the standard Flutter `Icon` widget and the new `StreamIcons` token set accessed via `context.streamIcons`. + +### Breaking Change + +`StreamSvgIcon` is deprecated — usages will produce deprecation warnings. The class will be removed in a future release. + +### Migration + +**Before:** +```dart +StreamSvgIcon(icon: StreamSvgIcons.reply) +StreamSvgIcon(icon: StreamSvgIcons.copy, color: Colors.red, size: 24) +``` + +**After:** +```dart +Icon(context.streamIcons.reply20) +Icon(context.streamIcons.copy20, color: Colors.red, size: 24) +``` + +`context.streamIcons` is a `StreamIcons` extension on `BuildContext` — it reads from the nearest `StreamTheme` in the widget tree. + +### Icon Name Mapping + +| Old (`StreamSvgIcons.*`) | New (`context.streamIcons.*`) | +|--------------------------|-------------------------------| +| `arrowRight` | `arrowRight20` | +| `attach` | `attachment20` | +| `award` | `trophy20` | +| `camera` | `camera20` | +| `check` | `checkmark20` | +| `checkAll` | `checks20` | +| `checkSend` | `checkmark20` | +| `circleUp` | `arrowUp20` | +| `close` | `xmark20` | +| `closeSmall` | `xmark16` | +| `contacts` | `users20` | +| `copy` | `copy20` | +| `delete` | `delete20` | +| `down` | `chevronDown20` | +| `download` | `download20` | +| `edit` | `edit20` | +| `emptyCircleRight` | `chevronRight20` | +| `error` | `exclamationCircleFill20` | +| `eye` | `eyeFill20` | +| `files` | `file20` | +| `flag` | `flag20` | +| `grid` | `gallery20` | +| `group` | `users20` | +| `left` | `chevronLeft20` | +| `lightning` | `bolt20` | +| `link` | `link20` | +| `lock` | `lock20` | +| `mentions` | `mention20` | +| `menuPoint` | `more20` | +| `message` | `messageBubble20` | +| `messageUnread` | `notification20` | +| `mic` | `voice20` | +| `mute` | `mute20` | +| `notification` | `bell20` | +| `pause` | `pauseFill20` | +| `penWrite` | `edit20` | +| `pictures` | `image20` | +| `pin` | `pin20` | +| `play` | `playFill20` | +| `polls` | `poll20` | +| `record` | `video20` | +| `reload` | `refresh20` | +| `reply` | `reply20` | +| `retry` | `retry20` | +| `right` | `chevronRight20` | +| `save` | `save20` | +| `search` | `search20` | +| `send` | `send20` | +| `sendMessage` | `send20` | +| `share` | `export20` | +| `shareArrow` | `share20` | +| `smile` | `emoji20` | +| `stop` | `stopFill20` | +| `threadReply` | `thread20` | +| `time` | `clock20` | +| `up` | `chevronUp20` | +| `user` | `user20` | +| `userAdd` | `userAdd20` | +| `userDelete` | `userRemove20` | +| `userRemove` | `userRemove20` | +| `userSettings` | `userCheck20` | +| `videoCall` | `videoFill20` | +| `volumeUp` | `audio20` | + +The following icons have been **removed with no equivalent** in the new set: +`cloudDownload`, `lolReaction`, `loveReaction`, `moon`, `settings`, `thumbsDownReaction`, `thumbsUpReaction`, `wutReaction`. + +--- + +## Header Widgets + +`StreamChannelHeader`, `StreamChannelListHeader`, and `StreamThreadHeader` all received the same set of default-value changes. + +### Breaking Changes + +| Parameter | Old default | New default | Notes | +|-----------|-------------|-------------|-------| +| `centerTitle` | `bool?` (`null` → platform-adaptive) | `bool` (`true`) | Was `null` by default, which let Flutter centre on iOS and left-align on Android. Now always `true` — explicitly pass `centerTitle: false` to restore left-aligned titles. | +| `elevation` | `1` | `0` | Removes the drop shadow by default. Pass `elevation: 1` to restore the old appearance. | +| `scrolledUnderElevation` | — | `0` (new param) | Controls the elevation when content is scrolled under the header. | + +### Migration + +```dart +// Before: title was platform-adaptive, header had a shadow +StreamChannelHeader() +StreamChannelListHeader() +StreamThreadHeader(parent: parentMessage) + +// After: title always centred, no shadow +// If you relied on left-aligned titles on Android, pass centerTitle: false: +StreamChannelHeader(centerTitle: false) +StreamChannelListHeader(centerTitle: false) +StreamThreadHeader(parent: parentMessage, centerTitle: false) + +// If you relied on the elevation shadow, restore it: +StreamChannelHeader(elevation: 1) +``` + +--- + +## StreamChat.componentBuilders + +`StreamChat` now accepts an optional `componentBuilders` parameter. When provided, it automatically inserts a `StreamComponentFactory` into the widget tree below the theme, making app-wide component customization available without wrapping the app manually. + +### Migration + +**Before (manual wrapping required):** +```dart +StreamComponentFactory( + builders: StreamComponentBuilders( + extensions: streamChatComponentBuilders( + messageWidget: (context, props) => MyMessage(props: props), + ), + ), + child: StreamChat( + client: client, + child: MyApp(), + ), +) +``` + +**After (pass via `StreamChat` directly):** +```dart +StreamChat( + client: client, + componentBuilders: StreamComponentBuilders( + extensions: streamChatComponentBuilders( + messageWidget: (context, props) => MyMessage(props: props), + ), + ), + child: MyApp(), +) +``` + +Both approaches are equivalent. If you already have a `StreamComponentFactory` elsewhere in the tree it continues to work. Use `componentBuilders` when `StreamChat` is your natural customization entry point. + +--- + +## StreamChatConfigurationData New Fields + +Three new optional fields have been added to `StreamChatConfigurationData`. Existing code that does not pass them will use the defaults and requires no changes. + +### New Fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `attachmentBuilders` | `List?` | `null` | Custom attachment widget builders. When non-null, these are **prepended** to the SDK's built-in builders so your types are matched first. | +| `reactionType` | `StreamReactionsType?` | `null` | Controls the visual style of the reactions display (e.g. segmented). Falls back to the SDK default when `null`. | +| `reactionPosition` | `StreamReactionsPosition?` | `null` | Controls where reactions appear relative to the message bubble (e.g. header). Falls back to the SDK default when `null`. | + +> **Note:** The `imageCDN` field was also added to `StreamChatConfigurationData`. It is covered in the [Image CDN & Thumbnails](image_cdn.md) guide. + +### Migration + +```dart +// Before: no attachment builder or reaction customization on the config +StreamChat( + client: client, + streamChatConfigData: StreamChatConfigurationData( + enforceUniqueReactions: false, + ), + child: MyApp(), +) + +// After: optionally add the new fields +StreamChat( + client: client, + streamChatConfigData: StreamChatConfigurationData( + enforceUniqueReactions: false, + attachmentBuilders: [MyCustomAttachmentBuilder()], + reactionType: StreamReactionsType.segmented, + reactionPosition: StreamReactionsPosition.header, + ), + child: MyApp(), +) +``` + +--- + +## Migration Checklist + +- [ ] Replace all `StreamSvgIcon(icon: StreamSvgIcons.*)` with `Icon(context.streamIcons.*)` using the mapping table above +- [ ] If you relied on platform-adaptive `centerTitle` behaviour on `StreamChannelHeader` / `StreamChannelListHeader` / `StreamThreadHeader`, pass `centerTitle: false` explicitly for Android-style left-aligned titles +- [ ] If you relied on the `elevation: 1` shadow on headers, pass `elevation: 1` explicitly +- [ ] Optionally move `StreamComponentFactory` wrapping into the `componentBuilders` parameter on `StreamChat` +- [ ] Use the new `attachmentBuilders`, `reactionType`, and `reactionPosition` fields on `StreamChatConfigurationData` if you need custom attachment rendering or global reaction style control diff --git a/migrations/redesign/image_cdn.md b/migrations/redesign/image_cdn.md new file mode 100644 index 0000000000..bb341447a3 --- /dev/null +++ b/migrations/redesign/image_cdn.md @@ -0,0 +1,209 @@ +# Image CDN & Thumbnails Migration Guide + +This guide covers the migration for the redesigned image CDN handling and thumbnail resize parameters in Stream Chat Flutter SDK. + +--- + +## Table of Contents + +- [Quick Reference](#quick-reference) +- [StreamImageCDN](#streamimagecdn) +- [StreamImageAttachmentThumbnail](#streamimageattachmentthumbnail) +- [StreamMediaAttachmentThumbnail](#streammediaattachmentthumbnail) +- [StreamImageAttachment](#streamimageattachment) +- [Migration Checklist](#migration-checklist) + +--- + +## Quick Reference + +| Component | Key Changes | +|-----------|-------------| +| [**StreamImageCDN**](#streamimagecdn) | New class replacing `getResizedImageUrl` String extension (stable cache keys now via `StreamImageCDN.cacheKey()`) | +| [**StreamImageAttachmentThumbnail**](#streamimageattachmentthumbnail) | `thumbnailSize`, `thumbnailResizeType`, `thumbnailCropType` → single `resize` parameter | +| [**StreamMediaAttachmentThumbnail**](#streammediaattachmentthumbnail) | `thumbnailSize`, `thumbnailResizeType`, `thumbnailCropType` → single `resize` parameter | +| [**StreamImageAttachment**](#streamimageattachment) | `imageThumbnailSize`, `imageThumbnailResizeType`, `imageThumbnailCropType` → single `resize` parameter | + +--- + +## StreamImageCDN + +### What Changed: + +The `getResizedImageUrl` String extension has been replaced with a dedicated `StreamImageCDN` class. This class handles CDN URL resolution and stable cache key generation, preventing image reloads caused by expiring signed URL tokens. + +### Key Changes: + +- `getResizedImageUrl` String extension removed — use `StreamImageCDN.resolveUrl` instead +- New `StreamImageCDN.cacheKey` method generates stable cache keys that strip volatile signed URL tokens +- Raw `String` resize/crop type parameters replaced with `ResizeMode` and `CropMode` enums +- `StreamImageCDN` is injectable via `StreamChatConfigurationData` for custom CDN support + +### Migration: + +**Before:** +```dart +final resizedUrl = imageUrl.getResizedImageUrl( + width: 200, + height: 300, + resize: 'clip', + crop: 'center', +); +``` + +**After:** +```dart +const imageCDN = StreamImageCDN(); + +final resizedUrl = imageCDN.resolveUrl( + imageUrl, + resize: ImageResize(width: 200, height: 300), +); + +final cacheKey = imageCDN.cacheKey(resizedUrl); +``` + +### Custom CDN Support: + +Extend `StreamImageCDN` and inject it via configuration: + +```dart +class MyImageCDN extends StreamImageCDN { + @override + String cacheKey(String imageUrl) { + return Uri.parse(imageUrl).path; + } +} + +StreamChat( + client: client, + config: StreamChatConfigurationData( + imageCDN: MyImageCDN(), + ), + child: ..., +) +``` + +--- + +## StreamImageAttachmentThumbnail + +### Breaking Changes: + +- `thumbnailSize` parameter removed +- `thumbnailResizeType` parameter removed +- `thumbnailCropType` parameter removed +- New `resize` parameter (`ImageResize?`) replaces all three + +### Migration: + +**Before:** +```dart +StreamImageAttachmentThumbnail( + image: attachment, + thumbnailSize: const Size(200, 300), + thumbnailResizeType: 'clip', + thumbnailCropType: 'center', +) +``` + +**After:** +```dart +StreamImageAttachmentThumbnail( + image: attachment, + resize: ImageResize( + width: 200, + height: 300, + mode: ResizeMode.clip, + crop: CropMode.center, + ), +) +``` + +> **Note:** When `resize` is null, the size is auto-calculated from layout constraints and defaults to `ResizeMode.clip` and `CropMode.center`. + +--- + +## StreamMediaAttachmentThumbnail + +### Breaking Changes: + +- `thumbnailSize` parameter removed +- `thumbnailResizeType` parameter removed +- `thumbnailCropType` parameter removed +- New `resize` parameter (`ImageResize?`) replaces all three + +### Migration: + +**Before:** +```dart +StreamMediaAttachmentThumbnail( + media: attachment, + thumbnailSize: const Size(200, 300), + thumbnailResizeType: 'clip', + thumbnailCropType: 'center', +) +``` + +**After:** +```dart +StreamMediaAttachmentThumbnail( + media: attachment, + resize: ImageResize( + width: 200, + height: 300, + mode: ResizeMode.clip, + crop: CropMode.center, + ), +) +``` + +--- + +## StreamImageAttachment + +### Breaking Changes: + +- `imageThumbnailSize` parameter removed +- `imageThumbnailResizeType` parameter removed +- `imageThumbnailCropType` parameter removed +- New `resize` parameter (`ImageResize?`) replaces all three + +### Migration: + +**Before:** +```dart +StreamImageAttachment( + message: message, + image: attachment, + imageThumbnailSize: const Size(400, 600), + imageThumbnailResizeType: 'crop', + imageThumbnailCropType: 'center', +) +``` + +**After:** +```dart +StreamImageAttachment( + message: message, + image: attachment, + resize: ImageResize( + width: 400, + height: 600, + mode: ResizeMode.crop, + crop: CropMode.center, + ), +) +``` + +--- + +## Migration Checklist + +- [ ] Replace `getResizedImageUrl` String extension calls with `StreamImageCDN.resolveUrl` +- [ ] Use `StreamImageCDN.cacheKey` to generate stable cache keys for `CachedNetworkImage` +- [ ] Replace raw `String` resize/crop values (`'clip'`, `'crop'`, etc.) with `ResizeMode` and `CropMode` enums +- [ ] Update `StreamImageAttachmentThumbnail` to use `resize` parameter instead of `thumbnailSize`, `thumbnailResizeType`, `thumbnailCropType` +- [ ] Update `StreamMediaAttachmentThumbnail` to use `resize` parameter instead of `thumbnailSize`, `thumbnailResizeType`, `thumbnailCropType` +- [ ] Update `StreamImageAttachment` to use `resize` parameter instead of `imageThumbnailSize`, `imageThumbnailResizeType`, `imageThumbnailCropType` +- [ ] If using a custom CDN, extend `StreamImageCDN` and inject via `StreamChatConfigurationData` diff --git a/migrations/redesign/localizations.md b/migrations/redesign/localizations.md new file mode 100644 index 0000000000..1b93732bf4 --- /dev/null +++ b/migrations/redesign/localizations.md @@ -0,0 +1,110 @@ +# Localizations Migration Guide + +This guide covers the breaking changes to `Translations` and `StreamChatLocalizations` in the Stream Chat Flutter SDK design refresh. + +--- + +## Table of Contents + +- [New Required Abstract Members](#new-required-abstract-members) +- [Changed Default String Values](#changed-default-string-values) +- [Migration Checklist](#migration-checklist) + +--- + +## New Required Abstract Members + +If you have a custom `Translations` subclass (used to provide custom localization strings), it **will fail to compile** unless you add implementations for the following new abstract members. + +### New getters and methods + +Add these to your `Translations` subclass: + +```dart +// Channel/message list empty states +@override +String get noConversationsYetText => 'No conversations yet'; + +@override +String get replyToStartThreadText => 'Reply to a message to start a thread'; + +@override +String get sendMessageToStartConversationText => 'Send a message to start the conversation'; + +// Message annotation labels +@override +String get savedForLaterLabel => 'Saved for later'; + +@override +String get repliedToThreadAnnotationLabel => 'Replied to a thread'; + +@override +String get alsoSentInChannelAnnotationLabel => 'Also sent in channel'; + +@override +String get viewLabel => 'View'; + +// Reminder labels +@override +String get reminderSetLabel => 'Reminder set'; + +@override +String reminderAtText(String time) => 'Today at $time'; + +// Channel list attachment previews +@override +String get fileAttachmentText => 'File'; + +@override +String filesAttachmentCountText(int count) => count == 1 ? 'File' : '$count files'; + +@override +String photosAttachmentCountText(int count) => count == 1 ? 'Photo' : '$count photos'; + +@override +String videosAttachmentCountText(int count) => count == 1 ? 'Video' : '$count videos'; + +// Attachment picker labels +@override +String get createPollPromptLabel => 'Create a poll and let everyone vote!'; + +@override +String get takePhotoAndShareLabel => 'Take a photo and share'; + +@override +String get takeVideoAndShareLabel => 'Take a video and share'; + +@override +String get openCameraLabel => 'Open camera'; + +@override +String get selectFilesToShareLabel => 'Select files to share'; + +@override +String get openFilesLabel => 'Open files'; +``` + +> **Note:** The values shown above are the English defaults from `DefaultTranslations`. Provide your own translated strings in place of these. + +--- + +## Changed Default String Values + +The following strings changed their default English value in `DefaultTranslations`. If you have not overridden them in a custom `Translations` subclass you do not need to do anything, but you should review whether the new values are appropriate for your app. + +| Getter | Old default | New default | +|--------|-------------|-------------| +| `threadReplyLabel` | `'Thread Reply'` | `'Thread'` | +| `threadReplyCountText(int)` | `'$count Thread Replies'` | `count == 1 ? '1 reply' : '$count replies'` | +| `alsoSendAsDirectMessageLabel` | `'Also send as direct message'` | `'Also send in Channel'` | +| `addMoreFilesLabel` | `'Add more files'` | `'Add more'` | + +If your app overrides these in a `Translations` subclass, your custom values are unaffected. + +--- + +## Migration Checklist + +- [ ] Search your codebase for any class that `extends Translations` or `extends DefaultTranslations` +- [ ] Add implementations for all 19 new abstract members listed above — the compiler will flag missing ones +- [ ] Review the four changed default string values and decide whether to keep the new defaults or override them to preserve the old text diff --git a/migrations/redesign/message_actions.md b/migrations/redesign/message_actions.md new file mode 100644 index 0000000000..029e28e217 --- /dev/null +++ b/migrations/redesign/message_actions.md @@ -0,0 +1,577 @@ +# Message Actions Migration Guide + +This guide covers the migration for the redesigned message action components in Stream Chat Flutter SDK. + +--- + +## Table of Contents + +- [Quick Reference](#quick-reference) +- [StreamMessageAction → StreamContextMenuAction](#streammessageaction--streamcontextmenuaction) +- [StreamMessageActionItem](#streammessageactionitem) +- [StreamMessageActionsModal](#streammessageactionsmodal) +- [StreamMessageReactionsModal](#streammessagereactionsmodal) +- [ModeratedMessageActionsModal](#moderatedmessageactionsmodal) +- [StreamMessageWidget.customActions → actionsBuilder](#streammessagewidgetcustomactions) +- [StreamMessageActionsBuilder](#streammessageactionsbuilder) +- [New Components](#new-components) +- [Migration Checklist](#migration-checklist) + +--- + +## Quick Reference + +| Symbol | Change | +|--------|--------| +| `StreamMessageAction` | **Removed** — replaced by `StreamContextMenuAction` | +| `StreamMessageActionItem` | **Removed** — rendering built into `StreamContextMenuAction` | +| `StreamMessageActionsModal.onActionTap` | **Removed** — use `onTap` per-action or await the dialog return value | +| `StreamMessageActionsModal.messageActions` | **Type changed**: `List` → `List` | +| `StreamMessageActionsModal.reverse` | **Removed** — use `alignment: AlignmentGeometry?` | +| `StreamMessageActionsModal.reactionPickerBuilder` | **Removed** — use `showReactionPicker: bool` | +| `StreamMessageReactionsModal` | **Deleted** — use `ReactionDetailSheet` (see [reaction_list.md](reaction_list.md)) | +| `StreamMessageReactionsModal.onReactionPicked` | **Removed** — await the dialog return value (`SelectReaction`) | +| `ModeratedMessageActionsModal.onActionTap` | **Removed** — use `onTap` per-action or await the dialog return value | +| `ModeratedMessageActionsModal.messageActions` | **Type changed**: `List` → `List` | +| `StreamMessageWidget.customActions` | **Removed** — replaced by `actionsBuilder` (`MessageActionsBuilder?`) | +| `StreamMessageWidget.onCustomActionTap` | **Removed** — use `onTap` directly on each `StreamContextMenuAction` in `actionsBuilder` | +| `CustomMessageAction` | **Removed** — no longer needed; custom actions use `onTap` directly | +| `OnMessageActionTap` | **Removed** — no longer needed | +| `StreamMessageWidget.actionsBuilder` | **New** — `MessageActionsBuilder?` for the normal long-press menu | +| `StreamMessageActionsBuilder.buildActions` | **Changed**: return type `List`, `customActions` param **removed** | +| `StreamMessageActionsBuilder.buildBouncedErrorActions` | **Return type changed**: `List` → `List` | +| `MessageActionsBuilder` | **New typedef** — `List Function(BuildContext, List>)` | +| `StreamContextMenu` | **New** — exported from `stream_core_flutter` | +| `StreamContextMenuAction` | **New** — exported from `stream_core_flutter` | +| `StreamContextMenuSeparator` | **New** — exported from `stream_core_flutter` | + +> **Note:** `MessageAction` and all its built-in subclasses (`SelectReaction`, `CopyMessage`, `DeleteMessage`, etc.) are **unchanged**. `CustomMessageAction` (the escape-hatch subclass) has been **removed** — it was only needed for the old `onCustomActionTap` dispatch pattern. + +--- + +## StreamMessageAction → StreamContextMenuAction + +The `StreamMessageAction` data class has been removed. It was a pure data object that described how an action should look and which `MessageAction` it represents. It is replaced by `StreamContextMenuAction`, which is a self-rendering widget that carries a typed `value` and handles dispatch automatically. + +### Breaking Change + +`StreamMessageAction` no longer exists. Replace every usage with `StreamContextMenuAction`. + +### Tap dispatch behaviour + +`StreamContextMenuAction` has two complementary dispatch mechanisms: + +- **`value`** — when the action is tapped inside a popup route (dialog, bottom sheet, etc.) it calls `Navigator.pop(value)` first, then calls `onTap` if provided. The route is already closed when `onTap` runs. +- **`onTap`** — an optional `VoidCallback?`. When the action is used *inline* (outside any popup route) this is the only callback that fires. + +You can use `value`, `onTap`, or both together. + +### Migration + +**Before:** +```dart +StreamMessageAction( + action: QuotedReply(message: message), + leading: const StreamSvgIcon(icon: StreamSvgIcons.reply), + title: Text(context.translations.replyLabel), +) +``` + +**After (value-based — recommended for modals):** +```dart +StreamContextMenuAction( + value: QuotedReply(message: message), + leading: Icon(context.streamIcons.arrowShareLeft), + label: Text(context.translations.replyLabel), +) +// The caller receives QuotedReply via the Future returned by showStreamDialog. +``` + +**After (onTap-based — for inline usage or when you prefer callbacks):** +```dart +StreamContextMenuAction( + value: QuotedReply(message: message), + leading: Icon(context.streamIcons.arrowShareLeft), + label: Text(context.translations.replyLabel), + onTap: () => onReply(message), // called after the route is dismissed +) +``` + +### Property mapping + +| `StreamMessageAction` | `StreamContextMenuAction` | +|-----------------------|--------------------------| +| `action: T` | `value: T?` | +| `title: Widget?` | `label: Widget` (required) | +| `leading: Widget?` | `leading: Widget?` | +| `isDestructive: bool` | `isDestructive: bool` (or use `.destructive` constructor) | +| `iconColor: Color?` | Controlled via `StreamContextMenuActionTheme` | +| `titleTextColor: Color?` | Controlled via `StreamContextMenuActionTheme` | +| `titleTextStyle: TextStyle?` | Controlled via `StreamContextMenuActionTheme` | +| `backgroundColor: Color?` | Controlled via `StreamContextMenuActionTheme` | +| — | `onTap: VoidCallback?` (new) | +| — | `trailing: Widget?` (new) | +| — | `enabled: bool` (new) | + +> **Important:** +> - **`label` is now required** — `title: Widget?` was optional in `StreamMessageAction`; `label: Widget` is a required, non-nullable parameter in `StreamContextMenuAction`. Any call site that omitted `title` will fail to compile; you must supply a non-null `label` widget (typically a `Text`). +> - `onTap` signature changed from `void Function(MessageAction)` to `VoidCallback?` — capture data in a closure instead +> - Per-item colours and text styles are now unified via `StreamContextMenuActionTheme` rather than individual properties + +--- + +## StreamMessageActionItem + +The `StreamMessageActionItem` widget has been removed. `StreamContextMenuAction` is now a full self-rendering widget — no separate "item" wrapper is needed. + +### Breaking Change + +`StreamMessageActionItem` no longer exists. Remove all direct usages. + +### Migration + +**Before:** +```dart +StreamMessageActionItem( + action: StreamMessageAction( + action: CopyMessage(message: message), + leading: const StreamSvgIcon(icon: StreamSvgIcons.copy), + title: Text('Copy'), + ), + onTap: (action) => _handle(action), +) +``` + +**After:** +```dart +StreamContextMenuAction( + value: CopyMessage(message: message), + leading: Icon(context.streamIcons.copy), + label: Text('Copy'), + onTap: () => _handle(message), +) +``` + +--- + +## StreamMessageActionsModal + +### Breaking Changes + +- `onActionTap: OnMessageActionTap?` parameter **removed** — the modal no longer holds a top-level callback; use `onTap` on individual actions or await the dialog's return value +- `messageActions` parameter type changed from `List` to `List` +- `reverse: bool` parameter **removed** — use `alignment: AlignmentGeometry?` instead +- `reactionPickerBuilder: ReactionPickerBuilder` parameter **removed** — use `showReactionPicker: bool` instead +- New `leadingInset: double` parameter added (default `0`) + +### Migration + +**Before:** +```dart +StreamMessageActionsModal( + message: message, + messageWidget: messageWidget, + messageActions: [ + StreamMessageAction( + action: CopyMessage(message: message), + leading: const StreamSvgIcon(icon: StreamSvgIcons.copy), + title: Text(context.translations.copyMessageLabel), + ), + ], + onActionTap: (action) { + if (action is CopyMessage) _copyMessage(action.message); + }, +) +``` + +**After (onTap per-action):** +```dart +StreamMessageActionsModal( + message: message, + messageWidget: messageWidget, + messageActions: [ + StreamContextMenuAction( + value: CopyMessage(message: message), + leading: Icon(context.streamIcons.copy), + label: Text(context.translations.copyMessageLabel), + onTap: () => _copyMessage(message), // called after route dismissal + ), + ], +) +``` + +**After (await return value):** + +> `showStreamDialog` is a Stream-themed wrapper around `showGeneralDialog` — see [New Components](#showstreamdialogt) for details. + +```dart +final action = await showStreamDialog( + context: context, + builder: (_) => StreamMessageActionsModal( + message: message, + messageWidget: messageWidget, + messageActions: [ + StreamContextMenuAction( + value: CopyMessage(message: message), + leading: Icon(context.streamIcons.copy), + label: Text(context.translations.copyMessageLabel), + ), + ], + ), +); + +if (action is CopyMessage) _copyMessage(action.message); +``` + +> **Important:** +> - `onActionTap` on the modal is gone — move handling to `onTap` on each item or await the `Future` +> - Replace `StreamMessageAction` entries with `StreamContextMenuAction` + +--- + +## StreamMessageReactionsModal + +### Breaking Changes + +- `StreamMessageReactionsModal` class has been **deleted**. Any direct reference to the class will cause a compile error. Use `ReactionDetailSheet` instead — see [Reaction List & Detail Sheet](reaction_list.md). +- `onReactionPicked: OnMessageActionTap?` parameter **removed** — the modal now pops the route with a `SelectReaction`; await the dialog return value to handle it + +### Migration + +**Before:** +```dart +StreamMessageReactionsModal( + message: message, + messageWidget: messageWidget, + onReactionPicked: (SelectReaction action) { + _addReaction(action.reaction); + }, +) +``` + +**After:** +```dart +final action = await showStreamDialog( + context: context, + builder: (_) => StreamMessageReactionsModal( + message: message, + messageWidget: messageWidget, + ), +); + +if (action is SelectReaction) { + _addReaction(action.reaction); +} +``` + +> **Important:** +> - The old `onReactionPicked` already received a `SelectReaction`, not a raw `Reaction` — the migration only changes *where* you handle it (caller vs callback) + +--- + +## ModeratedMessageActionsModal + +### Breaking Changes + +- `onActionTap: OnMessageActionTap?` parameter **removed** — move handling to `onTap` on each action or await the dialog return value +- `messageActions` parameter type changed from `List` to `List` + +### Migration + +**Before:** +```dart +ModeratedMessageActionsModal( + message: message, + messageActions: [ + StreamMessageAction( + action: ResendMessage(message: message), + title: Text(context.translations.sendAnywayLabel), + ), + StreamMessageAction( + action: EditMessage(message: message), + title: Text(context.translations.editMessageLabel), + ), + StreamMessageAction( + isDestructive: true, + action: HardDeleteMessage(message: message), + title: Text(context.translations.deleteMessageLabel), + ), + ], + onActionTap: (action) { + if (action is ResendMessage) _resend(action.message); + }, +) +``` + +**After (onTap per-action):** +```dart +ModeratedMessageActionsModal( + message: message, + messageActions: [ + StreamContextMenuAction( + value: ResendMessage(message: message), + label: Text(context.translations.sendAnywayLabel), + onTap: () => _resend(message), + ), + StreamContextMenuAction( + value: EditMessage(message: message), + label: Text(context.translations.editMessageLabel), + ), + StreamContextMenuAction.destructive( + value: HardDeleteMessage(message: message), + label: Text(context.translations.deleteMessageLabel), + ), + ], +) +``` + +**After (await return value):** +```dart +final action = await showStreamDialog( + context: context, + builder: (_) => ModeratedMessageActionsModal( + message: message, + messageActions: [ + StreamContextMenuAction( + value: ResendMessage(message: message), + label: Text(context.translations.sendAnywayLabel), + ), + // ... + ], + ), +); + +if (action is ResendMessage) _resend(action.message); +``` + +--- + +## StreamMessageWidget.customActions + +### Breaking Change + +`customActions: List` has been **removed**. It is replaced by `actionsBuilder`: + +```dart +typedef MessageActionsBuilder = + List Function( + BuildContext context, + List> defaultActions, + ); +``` + +`StreamMessageWidget.actionsBuilder` is declared as `MessageActionsBuilder?` (i.e. `MessageActionsBuilder?`), so `defaultActions` is typed as `List>` — each item's `.props.value` is a `MessageAction?`. + +The `defaultActions` list passed into the builder is already filtered by the widget's `show*` flags, so callers always start from a clean, ready-to-render baseline. + +`actionsBuilder` returns `List` — any widget can be mixed in alongside the default `StreamContextMenuAction` items (e.g. `StreamContextMenuSeparator`). + +### Migration + +**Before (append a custom action):** +```dart +StreamMessageWidget( + message: message, + messageTheme: messageTheme, + customActions: [ + StreamMessageAction( + action: CustomMessageAction( + message: message, + extraData: const {'type': 'favourite'}, + ), + leading: const Icon(Icons.star), + title: Text('Favourite'), + ), + ], + onCustomActionTap: (CustomMessageAction action) { + _favourite(action.message); + }, +) +``` + +**After:** +```dart +StreamMessageWidget( + message: message, + messageTheme: messageTheme, + actionsBuilder: (context, defaultActions) => [ + ...defaultActions, + StreamContextMenuAction( + leading: const Icon(Icons.star), + label: Text('Favourite'), + onTap: () => _favourite(message), + ), + ], +) +``` + +**After (remove an existing action and add a custom one):** +```dart +StreamMessageWidget( + message: message, + messageTheme: messageTheme, + actionsBuilder: (context, defaultActions) => [ + ...defaultActions.where((a) => a.props.value is! DeleteMessage), + StreamContextMenuSeparator(), + StreamContextMenuAction( + leading: const Icon(Icons.star), + label: Text('Favourite'), + onTap: () => _favourite(message), + ), + ], +) +``` + +> **Important:** +> - `onCustomActionTap` is **removed** — put dispatch logic directly in `onTap` on each action +> - `actionsBuilder` receives the defaults **already** filtered by `show*` flags (e.g. `showDeleteMessage`) +> - When `actionsBuilder` is not provided, the default list is wrapped in `StreamContextMenuAction.partitioned` automatically + +--- + +## StreamMessageActionsBuilder + +### Breaking Changes + +Both static methods now return `List` instead of `List`. Additionally, the `customActions` parameter of `buildActions` has been **removed** — appending custom actions is now handled by `StreamMessageWidget.actionsBuilder`. + +| Method / Parameter | Old type | New type | +|--------------------|----------|----------| +| `buildActions` return | `List` | `List` | +| `buildBouncedErrorActions` return | `List` | `List` | +| `buildActions(customActions:)` | `Iterable?` | **Removed** | + +### Migration + +**Before:** +```dart +final List actions = + StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channel, + currentUser: currentUser, + customActions: myCustomStreamMessageActions, +); +``` + +**After:** +```dart +// buildActions no longer accepts customActions — add extras via actionsBuilder +final List> actions = + StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channel, + currentUser: currentUser, +); +``` + +--- + +## New Components + +### showStreamDialog\ + +A top-level function from `package:stream_chat_flutter/stream_chat_flutter.dart` that replaces direct calls to `showDialog` when presenting Stream modals. It wraps `showGeneralDialog` and: + +- **Re-wraps `StreamChatTheme`** across the route boundary so the theme is available inside the dialog even when `useRootNavigator: true` +- **Applies a blur + scale transition** for a consistent Stream look +- **Returns `Future`** — the value passed to `Navigator.pop` inside the dialog, which is how `StreamMessageActionsModal`, `StreamMessageReactionsModal`, and `ModeratedMessageActionsModal` deliver the selected action back to the caller + +```dart +// Replace showDialog with showStreamDialog when presenting Stream modals: +final action = await showStreamDialog( + context: context, + builder: (_) => StreamMessageActionsModal(/* … */), +); +``` + +> **Note:** If you were calling `showDialog` directly and passing Stream modals to it, switch to `showStreamDialog` to ensure theming works correctly across the route boundary. + +### StreamContextMenuAction + +A self-contained menu action widget from `stream_core_flutter` that replaces `StreamMessageAction` + `StreamMessageActionItem`. It renders itself and supports two dispatch mechanisms: + +- **Inside a popup route** (dialog/bottom sheet): pops `value` via `Navigator.pop` first, then calls `onTap` if provided. +- **Inline** (outside any popup route): only `onTap` fires. + +```dart +// Standard action — value-based (recommended for modals) +StreamContextMenuAction( + value: CopyMessage(message: message), + leading: Icon(context.streamIcons.copy), + label: Text('Copy'), +) + +// With optional onTap callback (called after route dismissal) +StreamContextMenuAction( + value: CopyMessage(message: message), + leading: Icon(context.streamIcons.copy), + label: Text('Copy'), + onTap: () => _copyMessage(message), +) + +// Destructive action +StreamContextMenuAction.destructive( + value: DeleteMessage(message: message), + leading: Icon(context.streamIcons.trashBin), + label: Text('Delete'), +) +``` + +#### Helper methods for grouping + +```dart +// Insert a separator between every item +StreamContextMenuAction.separated(items: actions) + +// Insert separators between logical groups (provide groups as separate lists) +StreamContextMenuAction.sectioned(sections: [normalActions, destructiveActions]) + +// Automatically partition into normal / destructive groups with a separator between them +StreamContextMenuAction.partitioned(items: actions) +``` + +All three methods return `List` because they interleave `StreamContextMenuSeparator` widgets. + +### StreamContextMenu + +A themed container that wraps a list of `StreamContextMenuAction` and `StreamContextMenuSeparator` widgets. + +```dart +StreamContextMenu( + children: StreamContextMenuAction.partitioned(items: actions), +) +``` + +### StreamContextMenuSeparator + +A thin horizontal divider for use inside `StreamContextMenu`. + +```dart +StreamContextMenu( + children: [ + StreamContextMenuAction(value: reply, label: Text('Reply')), + const StreamContextMenuSeparator(), + StreamContextMenuAction.destructive(value: delete, label: Text('Delete')), + ], +) +``` + +--- + +## Migration Checklist + +- [ ] Replace all `StreamMessageAction(action: ..., title: ..., leading: ...)` with `StreamContextMenuAction(value: ..., label: ..., leading: ...)` +- [ ] Add a non-null `label` widget wherever `title` was previously omitted — `label` is now a required parameter and code that relied on a null/omitted `title` will not compile +- [ ] Update `onTap` callsites: old type was `void Function(MessageAction)`, new type is `VoidCallback?` — capture needed data in a closure +- [ ] Remove all `StreamMessageActionItem` usages +- [ ] Remove `onActionTap` from `StreamMessageActionsModal`; handle via per-action `onTap` or await the dialog return value +- [ ] Remove `onReactionPicked` from `StreamMessageReactionsModal`; await a `SelectReaction` return value +- [ ] Remove `onActionTap` from `ModeratedMessageActionsModal`; handle via per-action `onTap` or await the dialog return value +- [ ] Replace `StreamMessageWidget.customActions` with `actionsBuilder` +- [ ] Update `StreamMessageActionsBuilder.buildActions` call sites — return type is now `List` and `customActions` parameter no longer exists +- [ ] Update `StreamMessageActionsBuilder.buildBouncedErrorActions` call sites — return type is now `List` +- [ ] Replace `StreamSvgIcon` leading widgets in custom actions with `Icon(context.streamIcons.*)` +- [ ] Replace per-action color/style properties (`iconColor`, `titleTextColor`, etc.) with `StreamContextMenuActionTheme` diff --git a/migrations/redesign/message_composer.md b/migrations/redesign/message_composer.md new file mode 100644 index 0000000000..8d1d55c47a --- /dev/null +++ b/migrations/redesign/message_composer.md @@ -0,0 +1,282 @@ +# Message Composer Migration Guide + +This guide covers the migration for the message composer components in the Stream Chat Flutter SDK design refresh. + +--- + +## Table of Contents + +- [Overview](#overview) +- [StreamMessageInput](#streammessageinput) +- [StreamChatMessageComposer (new)](#streamchatmessagecomposer-new) +- [Attachment Customization](#attachment-customization) +- [Migration Checklist](#migration-checklist) + +--- + +## Overview + +There are two distinct composer components with different responsibilities: + +| Component | Responsibility | +|-----------|---------------| +| `StreamMessageInput` | Full-featured widget: handles sending, editing, attachments, autocomplete, mentions, commands, OG previews, voice recording flow, etc. | +| `StreamChatMessageComposer` | UI-only component: renders the composer layout using design system primitives. No business logic. | + +`StreamMessageInput` wraps `StreamChatMessageComposer` for its visual layer. If you are using `StreamMessageInput` today, it remains the right choice — it is not deprecated. `StreamChatMessageComposer` exists for cases where you want to build your own message-sending logic and use the new design system UI. + +--- + +## StreamMessageInput + +`StreamMessageInput` handles all message composition logic. This section documents all breaking changes. + +### Breaking Change: `hideSendAsDm` renamed to `canAlsoSendToChannelFromThread` (logic inverted) + +| Old | New | +|-----|-----| +| `hideSendAsDm: true` | `canAlsoSendToChannelFromThread: false` | +| `hideSendAsDm: false` (old default) | `canAlsoSendToChannelFromThread: true` (new default) | + +The logic is **inverted**: the old parameter hid the "also send to channel" checkbox when `true`; the new parameter **shows** it when `true`. + +**Before:** +```dart +StreamMessageInput( + hideSendAsDm: true, // hide the "also send to channel" checkbox +) +``` + +**After:** +```dart +StreamMessageInput( + canAlsoSendToChannelFromThread: false, // hide the checkbox +) +``` + +> **Note:** `canAlsoSendToChannelFromThread` defaults to `true`, matching the old default of showing the checkbox when inside a thread. + +### Breaking Change: `attachmentLimit` is now optional + +`attachmentLimit` changed from a required `int` (default `10`) to an optional `int?`. When `null` (the new default), no attachment count limit is enforced. + +**Before:** +```dart +StreamMessageInput( + attachmentLimit: 5, +) +``` + +**After (with limit):** +```dart +StreamMessageInput( + attachmentLimit: 5, +) +``` + +**After (no limit — new default behaviour):** +```dart +StreamMessageInput( + // attachmentLimit not set — no limit applied +) +``` + +### Removed parameters + +Many parameters that existed in older versions of `StreamMessageInput` have been removed. The table below lists each removed parameter and the recommended migration path. + +#### Layout and visual parameters + +These parameters have been removed. The composer layout is now fully owned by `StreamChatMessageComposer` and its sub-components, customizable via `StreamComponentFactory`. + +| Removed parameter | Migration path | +|-------------------|---------------| +| `maxHeight` | No direct replacement. The text field grows to fit its content without a height cap. | +| `maxLines` | No direct replacement. | +| `minLines` | No direct replacement. | +| `padding` | No direct replacement. Layout is controlled by the design system. | +| `textInputMargin` | No direct replacement. | +| `elevation` | No direct replacement. Visual styling is controlled by the design system theme. | +| `shadow` | No direct replacement. | +| `enableActionAnimation` | Removed. Actions no longer animate in/out. | +| `contentInsertionConfiguration` | Removed. | +| `sendButtonLocation` | Removed. The send button is always placed in the trailing position by the design system layout. | + +#### Action and button parameters + +These parameters have been removed. To customize buttons and actions in the composer, override the relevant sub-component via `StreamComponentFactory`. + +| Removed parameter | Migration path | +|-------------------|---------------| +| `actionsBuilder` | Override `messageComposerLeading` or `messageComposerTrailing` in `StreamComponentFactory`. | +| `spaceBetweenActions` | No direct replacement. | +| `actionsLocation` | Removed. The design system defines a fixed layout. | +| `attachmentButtonBuilder` | Override `messageComposerLeading` in `StreamComponentFactory`. | +| `commandButtonBuilder` | Override `messageComposerInputTrailing` in `StreamComponentFactory`. | +| `sendButtonBuilder` | Override `messageComposerTrailing` in `StreamComponentFactory`. | +| `idleSendIcon` | Override `messageComposerTrailing` in `StreamComponentFactory`. | +| `activeSendIcon` | Override `messageComposerTrailing` in `StreamComponentFactory`. | +| `showCommandsButton` | Override `messageComposerInputTrailing` in `StreamComponentFactory`. | + +#### Attachment builder parameters + +These parameters have been removed. Attachment rendering in the composer input header is now customizable via `StreamComponentFactory` — see [Attachment Customization](#attachment-customization). + +| Removed parameter | Migration path | +|-------------------|---------------| +| `attachmentListBuilder` | Override `messageComposerAttachmentList` in `StreamComponentFactory`. | +| `fileAttachmentListBuilder` | Override `messageComposerAttachmentList` in `StreamComponentFactory`. | +| `mediaAttachmentListBuilder` | Override `messageComposerAttachmentList` in `StreamComponentFactory`. | +| `voiceRecordingAttachmentListBuilder` | Override `messageComposerAttachmentList` in `StreamComponentFactory`. | +| `fileAttachmentBuilder` | Override `messageComposerAttachment` in `StreamComponentFactory`. | +| `mediaAttachmentBuilder` | Override `messageComposerAttachment` in `StreamComponentFactory`. | +| `voiceRecordingAttachmentBuilder` | Override `messageComposerAttachment` in `StreamComponentFactory`. | +| `quotedMessageBuilder` | Override `messageComposerInputHeader` in `StreamComponentFactory`. | +| `quotedMessageAttachmentThumbnailBuilders` | Override `messageComposerInputHeader` or `messageComposerAttachment` in `StreamComponentFactory`. | + +### Attachment button visibility + +Previously, the attachment button was always rendered (though inactive) when `disableAttachments: true` was set. The button is now fully hidden (removed from the layout) when no attachment callback is wired up. When you pass `disableAttachments: true` to `StreamMessageInput`, the attachment button no longer appears at all. + +If you are using `StreamChatMessageComposer` directly, the button hides when `onAttachmentButtonPressed` is `null`. + +--- + +## StreamChatMessageComposer (new) + +`StreamChatMessageComposer` is a pure UI component from the new design system. It renders the composer layout but contains no message-sending logic — your code is responsible for wiring up the controller and callbacks. + +Use this when you want the new design system visuals with custom business logic. If you want the full out-of-the-box experience (send, edit, attachments, mentions, commands, etc.), use `StreamMessageInput` instead. + +### Constructor Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `onSendPressed` | `VoidCallback` | **required** | Called when the send button is pressed | +| `controller` | `StreamMessageInputController?` | `null` | Controller for the input; created internally if not provided | +| `onAttachmentButtonPressed` | `VoidCallback?` | `null` | Called when the attachment button is pressed. When `null`, the attachment button is hidden. | +| `isPickerOpen` | `bool` | `false` | Whether the inline attachment picker is currently open | +| `focusNode` | `FocusNode?` | `null` | Focus node for the text field | +| `currentUserId` | `String?` | `null` | Current user's ID | +| `placeholder` | `String` | `''` | Placeholder text for the input field | +| `audioRecorderController` | `StreamAudioRecorderController?` | `null` | Enables the voice recording UI when provided | +| `sendVoiceRecordingAutomatically` | `bool` | `false` | Sends the voice recording immediately on finish | +| `feedback` | `AudioRecorderFeedback` | `const AudioRecorderFeedback()` | Haptic/audio feedback callbacks for the recording flow | +| `canAlsoSendToChannel` | `bool` | `false` | Shows the "also send to channel" checkbox (used in threads) | +| `onQuotedMessageCleared` | `VoidCallback?` | `null` | Called when the user removes the quoted message in the input header | +| `textInputAction` | `TextInputAction?` | `null` | The keyboard action button type | +| `keyboardType` | `TextInputType?` | `null` | The keyboard type for the text field | +| `textCapitalization` | `TextCapitalization` | `sentences` | Text capitalization behaviour for the text field | +| `autofocus` | `bool` | `false` | Whether the text field should autofocus when built | +| `autocorrect` | `bool` | `true` | Whether autocorrect is enabled | + +### Sub-components + +The layout is composed of named default sub-widgets that can be replaced via the `StreamComponentFactory`: + +| Sub-component | Description | +|---------------|-------------| +| `DefaultMessageComposerLeading` | Left side of the composer row (e.g., attachment button) | +| `DefaultMessageComposerTrailing` | Right side of the composer row (e.g., send/mic button) | +| `DefaultMessageComposerInputLeading` | Left side inside the input area | +| `DefaultMessageComposerInputTrailing` | Right side inside the input area | +| `DefaultMessageComposerInputHeader` | Header above the input (e.g., reply/edit preview, attachment thumbnails) | + +### Customization via Component Factory + +To replace the entire composer UI, provide a builder for `MessageComposerProps` in your `StreamComponentFactory`: + +```dart +StreamComponentFactory( + builders: StreamComponentBuilders( + extensions: streamChatComponentBuilders( + messageComposer: (context, props) => MyCustomComposer(props: props), + ), + ), + child: ..., +) +``` + +--- + +## Attachment Customization + +The attachment thumbnails shown in the composer input header are now rendered by two new customizable widgets. Both integrate with `StreamComponentFactory`. + +### `StreamMessageComposerAttachmentList` + +Renders the full list of attachment thumbnails in the composer. The old `StreamMessageInputAttachmentList` class has been **deleted** — any direct reference to it will cause a compile error. Use `StreamMessageComposerAttachmentList` instead. + +**Props class:** `StreamMessageComposerAttachmentListProps` + +| Property | Type | Description | +|----------|------|-------------| +| `attachments` | `Iterable` | The attachments to display (OG link previews are filtered out before this widget receives them) | +| `onRemovePressed` | `ValueSetter?` | Called when the user removes an attachment | + +Override the whole list using the `messageComposerAttachmentList` builder key: + +```dart +streamChatComponentBuilders( + messageComposerAttachmentList: (context, props) { + return MyCustomAttachmentList( + attachments: props.attachments, + onRemovePressed: props.onRemovePressed, + ); + }, +) +``` + +### `StreamMessageComposerAttachment` + +Renders a single attachment thumbnail inside the list. Use this to customise how individual attachment types are displayed without replacing the whole list. + +**Props class:** `StreamMessageComposerAttachmentProps` + +| Property | Type | Description | +|----------|------|-------------| +| `attachment` | `Attachment` | The attachment to render | +| `onRemovePressed` | `ValueSetter?` | Called when the user taps the remove button | +| `audioPlaylistController` | `StreamAudioPlaylistController?` | Shared playlist controller for audio/voice-recording attachments | + +Override individual attachment items using the `messageComposerAttachment` builder key: + +```dart +streamChatComponentBuilders( + messageComposerAttachment: (context, props) { + // Render video attachments differently; fall back to default for everything else. + if (props.attachment.type == AttachmentType.video) { + return MyVideoAttachmentThumbnail( + attachment: props.attachment, + onRemovePressed: props.onRemovePressed, + ); + } + return DefaultMessageComposerAttachment(props: props); + }, +) +``` + +### Built-in attachment builder helpers + +The following public widgets are provided as building blocks for custom attachment renderers: + +| Widget | Description | +|--------|-------------| +| `StreamAudioAttachmentBuilder` | Renders an audio or voice-recording attachment with playback controls | +| `StreamFileAttachmentBuilder` | Renders a generic file attachment with file type icon, name, and size | +| `StreamMediaAttachmentBuilder` | Renders an image, video, or GIF attachment thumbnail with an optional media badge | +| `RemoveAttachmentButton` | The standard filled icon button used to dismiss an attachment | + +--- + +## Migration Checklist + +- [ ] Rename `hideSendAsDm` to `canAlsoSendToChannelFromThread` in all `StreamMessageInput` usages and invert the value +- [ ] Review usages of `attachmentLimit` — it is now `int?` and defaults to no limit; set an explicit value if you relied on the old default of `10` +- [ ] Remove any usage of `maxHeight`, `maxLines`, `minLines`, `padding`, `textInputMargin`, `elevation`, `shadow`, `enableActionAnimation`, `contentInsertionConfiguration`, `sendButtonLocation` +- [ ] Replace `actionsBuilder` / `actionsLocation` / button builder params (`attachmentButtonBuilder`, `commandButtonBuilder`, `sendButtonBuilder`, `idleSendIcon`, `activeSendIcon`, `showCommandsButton`) with sub-component overrides via `StreamComponentFactory` +- [ ] Replace attachment list builder params (`attachmentListBuilder`, `fileAttachmentListBuilder`, `mediaAttachmentListBuilder`, `voiceRecordingAttachmentListBuilder`) with the `messageComposerAttachmentList` builder in `StreamComponentFactory` +- [ ] Replace attachment item builder params (`fileAttachmentBuilder`, `mediaAttachmentBuilder`, `voiceRecordingAttachmentBuilder`) with the `messageComposerAttachment` builder in `StreamComponentFactory` +- [ ] Replace `quotedMessageBuilder` / `quotedMessageAttachmentThumbnailBuilders` with `messageComposerInputHeader` or `messageComposerAttachment` overrides in `StreamComponentFactory` +- [ ] If adopting `StreamChatMessageComposer` directly, wire up your own send/attachment logic via `onSendPressed` and `onAttachmentButtonPressed` +- [ ] Move any composer UI customizations to `StreamComponentFactory` diff --git a/migrations/redesign/message_widget.md b/migrations/redesign/message_widget.md new file mode 100644 index 0000000000..f9f1043402 --- /dev/null +++ b/migrations/redesign/message_widget.md @@ -0,0 +1,500 @@ +# Message Widget & Message List Migration Guide + +This guide covers migrating the message widget and message list view from the old design (`feat/design-refresh`) to the new redesigned API. + +--- + +## Table of Contents + +- [Quick Reference](#quick-reference) +- [Architecture Changes](#architecture-changes) +- [StreamMessageWidget](#streammessagewidget) + - [Removed Parameters](#removed-parameters) + - [New Parameters](#new-parameters) + - [Changed Signatures](#changed-signatures) +- [StreamMessageListView](#streammessagelistview) + - [Builder Signature Changes](#builder-signature-changes) + - [New List-Level Callbacks](#new-list-level-callbacks) + - [Removed: MessageDetails](#removed-messagedetails) +- [Custom Actions Migration](#custom-actions-migration) +- [Theme Migration](#theme-migration) +- [Swipeable Message Example](#swipeable-message-example) +- [Deleted Classes & Files](#deleted-classes--files) +- [Typedef Changes](#typedef-changes) +- [Migration Checklist](#migration-checklist) + +--- + +## Quick Reference + +| Old | New | +|-----|-----| +| `StreamMessageWidget` (50+ params) | `StreamMessageWidget` (thin shell) + `StreamMessageWidgetProps` | +| `MessageWidgetContent` | `DefaultStreamMessage` + `StreamMessageContent` | +| `BottomRow` | `StreamMessageMetadata` | +| `StreamMessageText` (message_text.dart) | `StreamMessageText` (components/stream_message_text.dart) | +| `StreamDeletedMessage` | `StreamMessageDeleted` | +| `MessageCard` | `core.StreamMessageBubble` | +| `TextBubble` | `core.StreamMessageBubble` | +| `PinnedMessage` | `StreamMessageAnnotations` widget | +| `QuotedMessage` | Inline in `StreamMessageContent` | +| `Username` | Inline in `StreamMessageMetadata` | +| `SendingIndicatorBuilder` | `StreamMessageSendingStatus` | +| `ThreadReplyPainter` | `core.StreamMessageReplies` | +| `ThreadParticipants` | Inline in `core.StreamMessageReplies` | +| `UserAvatarTransform` | `StreamUserAvatar` (inline in `DefaultStreamMessage`) | +| `DisplayWidget` enum | `StreamVisibility` (from theme) | +| `MessageBuilder` typedef | `StreamMessageWidgetBuilder` typedef | +| `ParentMessageBuilder` typedef | `StreamMessageWidgetBuilder` typedef | +| `OnQuotedMessageTap = void Function(String?)` | `void Function(Message quotedMessage)` | +| `StreamMessageWidget.customActions` | `StreamMessageWidgetProps.actionsBuilder` | +| `StreamMessageWidget.onCustomActionTap` | Use `onTap` per `StreamContextMenuAction` | +| `CustomMessageAction` | Removed — use `StreamContextMenuAction` with `onTap` | +| `StreamMessageWidget.copyWith()` | `StreamMessageWidgetProps.copyWith()` | + +--- + +## Architecture Changes + +The old design used a single monolithic `StreamMessageWidget` with 50+ parameters controlling every aspect of rendering. The new design splits responsibilities: + +- **`StreamMessageWidget`** — thin shell that resolves the `StreamComponentFactory` and delegates to the factory builder or `DefaultStreamMessage`. +- **`StreamMessageWidgetProps`** — plain data class holding all configuration. Supports `copyWith()`. +- **`DefaultStreamMessage`** — the default rendering implementation. Composes the sub-components below. +- **`StreamMessageContent`** — bubble, attachments, text, reactions. Thread replies are passed in as a pre-built widget from `DefaultStreamMessage`. +- **`StreamMessageMetadata`** — username, timestamp, sending status, edited indicator. +- **`StreamMessageAnnotations`** — pinned, saved-for-later, show-in-channel annotations. +- **`StreamUserAvatar`** — author avatar (inline in `DefaultStreamMessage`). +- **`StreamMessageReactions`** — clustered reaction chips around the bubble. +- **`StreamMessageText`** — markdown-rendered message text. +- **`StreamMessageDeleted`** — deleted message placeholder. +- **`StreamMessageSendingStatus`** — delivery status icon. + +### Component Factory Pattern + +The new design adds a **component factory** layer for app-wide customization. The `messageBuilder` / `parentMessageBuilder` callbacks on `StreamMessageListView` are still supported for per-list customization. + +**App-wide customization via component factory:** +```dart +StreamChat( + client: client, + componentBuilders: StreamComponentBuilders( + extensions: streamChatComponentBuilders( + messageWidget: (context, props) { + return DefaultStreamMessage( + props: props.copyWith( + actionsBuilder: (context, defaultActions) { + return [...defaultActions, myCustomAction]; + }, + ), + ); + }, + ), + ), + child: ..., +) +``` + +**Per-list customization via `messageBuilder` (still supported):** +```dart +StreamMessageListView( + messageBuilder: (context, message, defaultProps) { + return StreamMessageWidget.fromProps(props: defaultProps); + }, +) +``` + +Both can be combined — the component factory applies first, then the per-list `messageBuilder` can further customize or wrap the result. + +--- + +## StreamMessageWidget + +### Removed Parameters + +These parameters have been removed entirely. See the **Migration Path** column for how to achieve the same result. + +#### Visibility Booleans + +| Old Parameter | Migration Path | +|---|---| +| `showReactions` | Controlled via `StreamMessageItemThemeData` visibility | +| `showDeleteMessage` | Controlled via channel permissions (`canDeleteOwnMessage`, `canDeleteAnyMessage`) | +| `showEditMessage` | Controlled via channel permissions (`canUpdateOwnMessage`, `canUpdateAnyMessage`) | +| `showReplyMessage` | Controlled via channel permissions (`canSendReply`) | +| `showThreadReplyMessage` | Controlled via channel permissions (`canSendReply`) | +| `showMarkUnreadMessage` | Shown automatically when applicable | +| `showResendMessage` | Shown automatically for failed messages | +| `showCopyMessage` | Shown automatically when message has text | +| `showFlagButton` | Controlled via channel permissions (`canFlagMessage`) | +| `showPinButton` | Controlled via channel permissions (`canPinMessage`) | +| `showPinHighlight` | Controlled via `StreamMessageItemThemeData` background color | +| `showReactionPicker` | Removed | +| `showUsername` | Controlled via `StreamMessageItemThemeData.metadataVisibility` | +| `showTimestamp` | Controlled via `StreamMessageItemThemeData.metadataVisibility` | +| `showEditedLabel` | Controlled via `StreamMessageItemThemeData.metadataVisibility` | +| `showSendingIndicator` | Controlled via `StreamMessageItemThemeData.metadataVisibility` | +| `showThreadReplyIndicator` | Controlled via `StreamMessageItemThemeData.repliesVisibility` | +| `showInChannelIndicator` | Shown automatically via `StreamMessageAnnotations` | +| `showUserAvatar` (`DisplayWidget`) | Controlled via `StreamMessageItemThemeData.avatarVisibility` | + +#### Builder Callbacks + +| Old Parameter | Migration Path | +|---|---| +| `userAvatarBuilder` | Use component factory to replace `DefaultStreamMessage` | +| `textBuilder` | Use component factory to replace `StreamMessageContent` | +| `quotedMessageBuilder` | Use component factory to replace `StreamMessageContent` | +| `deletedMessageBuilder` | Use component factory to replace `StreamMessageContent` | +| `editMessageInputBuilder` | Removed; use `onEditMessageTap` callback instead | +| `bottomRowBuilderWithDefaultWidget` | Use component factory; `StreamMessageMetadata` is the new equivalent | +| `reactionPickerBuilder` | Configured globally via `StreamChatConfigurationData.reactionIconResolver` | +| `reactionIndicatorBuilder` | Replaced by `StreamMessageReactions` component | + +#### Shape & Style + +| Old Parameter | Migration Path | +|---|---| +| `shape` | Controlled via `StreamMessageBubble` theming in `stream_core_flutter` | +| `borderSide` | Controlled via `StreamMessageBubble` theming | +| `borderRadiusGeometry` | Controlled via `StreamMessageBubble` theming | +| `attachmentShape` | Controlled via attachment builder theming | +| `textPadding` | Controlled via `StreamMessageBubble` content padding theming | +| `attachmentPadding` | Configured internally by `ParseAttachments` | +| `messageTheme` | Resolved from context via `StreamMessageItemTheme.of(context)` | + +#### Other Removed Parameters + +| Old Parameter | Migration Path | +|---|---| +| `reverse` | Determined by `StreamMessagePlacement` context (set by list view) | +| `translateUserAvatar` | Removed; avatar positioning is theme-driven | +| `onConfirmDeleteTap` | Handled internally by `StreamMessageActionsBuilder` | +| `onShowMessage` | Removed | +| `onReactionsHover` | Removed | +| `customActions` | Use `actionsBuilder` on `StreamMessageWidgetProps` | +| `onCustomActionTap` | Use `actionsBuilder` on `StreamMessageWidgetProps` | +| `onAttachmentTap` | Handle in custom attachment builders | +| `imageAttachmentThumbnailSize` | Configured in attachment builders | +| `imageAttachmentThumbnailResizeType` | Configured in attachment builders | +| `imageAttachmentThumbnailCropType` | Configured in attachment builders | +| `attachmentActionsModalBuilder` | Configured in attachment builders | +| `attachmentBuilders` | Moved to `StreamChatConfigurationData.attachmentBuilders` (still overridable per-message via `StreamMessageWidgetProps.attachmentBuilders`) | +| `copyWith()` on `StreamMessageWidget` | Use `StreamMessageWidgetProps.copyWith()` instead | + +### New Parameters + +| New Parameter | Description | +|---|---| +| `padding` | Outer padding around the message item (overrides theme) | +| `spacing` | Horizontal spacing between avatar and content (overrides theme) | +| `backgroundColor` | Background color for the message row (overrides theme) | +| `maxWidth` | Max content width in logical pixels (default: `264`) | +| `onMessageLinkTap` | `void Function(Message, String)` — receives message and URL | +| `onUserMentionTap` | `void Function(User)` — receives the mentioned user | +| `onQuotedMessageTap` | `void Function(Message)` — receives the quoted message object | +| `onReactionsTap` | `void Function(Message)` — overrides default reaction detail sheet | +| `reactionSorting` | `Comparator` for reaction display order | +| `actionsBuilder` | `MessageActionsBuilder` for customizing the actions list | +| `onMessageActions` | Override the default long-press modal entirely | +| `onBouncedErrorMessageActions` | Override the bounced-error modal entirely | +| `onEditMessageTap` | Called when edit action is selected | + +### Changed Signatures + +| Callback | Old Signature | New Signature | +|---|---|---| +| Link tap | `void Function(String url)` | `void Function(Message message, String url)` | +| Mention tap | `void Function(User user)` | `void Function(User user)` (renamed: `onMentionTap` → `onUserMentionTap`) | +| Quoted message tap | `void Function(String? quotedMessageId)` | `void Function(Message quotedMessage)` | +| Thread tap | `void Function(Message message)` | `void Function(Message message)` (unchanged signature, renamed: `onThreadTap`) | +| Reply tap | `void Function(Message message)` | `void Function(Message message)` (new: `onReplyTap`) | + +--- + +## StreamMessageListView + +### Builder Signature Changes + +Both `messageBuilder` and `parentMessageBuilder` now use the same typedef: + +**Before:** +```dart +typedef MessageBuilder = Widget Function( + BuildContext context, + MessageDetails details, + List messages, + StreamMessageWidget defaultMessageWidget, +); + +typedef ParentMessageBuilder = Widget Function( + BuildContext context, + Message? parentMessage, + StreamMessageWidget defaultMessageWidget, +); +``` + +**After:** +```dart +typedef StreamMessageWidgetBuilder = Widget Function( + BuildContext context, + Message message, + StreamMessageWidgetProps defaultProps, +); +``` + +The old builders received a pre-built `StreamMessageWidget` that you could `copyWith`. The new builders receive `StreamMessageWidgetProps` — raw configuration data. Use `StreamMessageWidget.fromProps(props:)` to build the default widget through the component factory. + +**Before:** +```dart +StreamMessageListView( + messageBuilder: (context, details, messages, defaultWidget) { + return defaultWidget.copyWith(showReactions: false); + }, +) +``` + +**After:** +```dart +StreamMessageListView( + messageBuilder: (context, message, defaultProps) { + // Build default widget (goes through component factory) + return StreamMessageWidget.fromProps(props: defaultProps); + + // Or customize props before building + return StreamMessageWidget.fromProps( + props: defaultProps.copyWith( + actionsBuilder: (context, actions) => [...actions, myAction], + ), + ); + + // Or replace entirely + return MyCustomMessageWidget(message: message); + }, +) +``` + +> **Important:** The `messageBuilder` callback now receives a `BuildContext` that has `StreamMessagePlacement` in its ancestor chain. You can call `StreamMessagePlacement.alignmentDirectionalOf(context)` to determine message alignment. + +### New List-Level Callbacks + +These callbacks were previously only configurable per-message on `StreamMessageWidget`. They are now available at the list level and forwarded to all messages: + +| New Parameter | Type | +|---|---| +| `onEditMessageTap` | `void Function(Message)?` | +| `onReplyTap` | `void Function(Message)?` | +| `onUserAvatarTap` | `void Function(User)?` | +| `onReactionsTap` | `void Function(Message)?` | +| `onQuotedMessageTap` | `void Function(Message)?` | +| `onMessageLinkTap` | `void Function(Message, String)?` | +| `onUserMentionTap` | `void Function(User)?` | + +### Changed: `showUnreadCountOnScrollToBottom` Default + +```dart +// Old +showUnreadCountOnScrollToBottom: false + +// New +showUnreadCountOnScrollToBottom: true +``` + +### Removed: MessageDetails + +The old `messageBuilder` received `MessageDetails` which contained `userId`, `message`, `messages`, and `index`. The new builder receives just `Message` and `StreamMessageWidgetProps`. The user ID is accessible via `StreamChat.of(context).currentUser?.id`. Message alignment is provided by `StreamMessagePlacement.of(context)`. + +--- + +## Custom Actions Migration + +**Before (using `customActions` + `onCustomActionTap`):** +```dart +StreamMessageWidget( + message: message, + messageTheme: theme, + customActions: [ + StreamMessageAction( + leading: Icon(Icons.info), + title: Text('Info'), + onTap: (message) => showInfo(message), + ), + ], + onCustomActionTap: (action) { + // handle CustomMessageAction + }, +) +``` + +**After (using `actionsBuilder` via component factory):** +```dart +StreamChat( + client: client, + componentBuilders: StreamComponentBuilders( + extensions: streamChatComponentBuilders( + messageWidget: (context, props) { + return DefaultStreamMessage( + props: props.copyWith( + actionsBuilder: (context, defaultActions) { + return StreamContextMenuAction.partitioned( + items: [ + ...defaultActions, + StreamContextMenuAction( + leading: Icon(context.streamIcons.informationCircle), + label: Text('Info'), + onTap: () => showInfo(props.message), + ), + ], + ); + }, + ), + ); + }, + ), + ), + child: ..., +) +``` + +**After (removing a default action):** +```dart +actionsBuilder: (context, defaultActions) { + return StreamContextMenuAction.partitioned( + items: defaultActions.where( + (a) => a.props.value is! DeleteMessage, + ).toList(), + ); +}, +``` + +> **Important:** +> - `customActions` and `onCustomActionTap` are removed +> - `CustomMessageAction` class is removed — use `StreamContextMenuAction` with `onTap` +> - `actionsBuilder` receives defaults already filtered by channel permissions +> - Return `List` — you can mix `StreamContextMenuAction` and `StreamContextMenuSeparator` + +--- + +## Theme Migration + +**Before (explicit `messageTheme` parameter):** +```dart +StreamMessageWidget( + message: message, + messageTheme: isMyMessage + ? streamTheme.ownMessageTheme + : streamTheme.otherMessageTheme, +) +``` + +**After (theme resolved automatically from context):** +```dart +StreamMessageWidget(message: message) +``` + +`StreamMessageItemTheme` is provided by `StreamChatTheme` and resolved based on `StreamMessagePlacement` (alignment, stack position, etc.). + +### StreamMessageItemThemeData + +The old per-property visibility booleans are replaced by a structured visibility system: + +```dart +StreamMessageItemThemeData( + avatarVisibility: StreamMessageStyleVisibility( + incoming: StreamVisibility.visible, + outgoing: StreamVisibility.gone, + ), + annotationVisibility: StreamMessageStyleVisibility(...), + metadataVisibility: StreamMessageStyleVisibility(...), + repliesVisibility: StreamMessageStyleVisibility(...), + + incoming: StreamMessageItemStyle( + padding: EdgeInsets.all(4), + backgroundColor: Colors.white, + ), + outgoing: StreamMessageItemStyle( + padding: EdgeInsets.all(4), + backgroundColor: Colors.blue.shade50, + ), +) +``` + +--- + +## Swipeable Message Example + +```dart +StreamMessageListView( + messageBuilder: (context, message, defaultProps) { + final defaultWidget = StreamMessageWidget.fromProps(props: defaultProps); + + if (message.isDeleted || message.state.isFailed) return defaultWidget; + + final alignment = StreamMessagePlacement.alignmentDirectionalOf(context); + final isEnd = alignment == AlignmentDirectional.centerEnd; + + return Swipeable( + key: ValueKey(message.id), + direction: isEnd ? SwipeDirection.endToStart : SwipeDirection.startToEnd, + swipeThreshold: 0.2, + onSwiped: (_) => onReply(message), + child: defaultWidget, + ); + }, +) +``` + +--- + +## Deleted Classes & Files + +| Old File | Old Class | Replacement | +|---|---|---| +| `message_widget_content.dart` | `MessageWidgetContent` | `DefaultStreamMessage` + `StreamMessageContent` | +| `message_widget_content_components.dart` | Various internal helpers | Merged into `components/` sub-widgets | +| `bottom_row.dart` | `BottomRow` | `StreamMessageMetadata` | +| `message_text.dart` | `StreamMessageText` | `components/stream_message_text.dart` | +| `deleted_message.dart` | `StreamDeletedMessage` | `StreamMessageDeleted` | +| `message_card.dart` | `MessageCard` | `core.StreamMessageBubble` | +| `text_bubble.dart` | `TextBubble` | `core.StreamMessageBubble` | +| `pinned_message.dart` | `PinnedMessage` | `StreamMessageAnnotations` widget | +| `quoted_message.dart` | `QuotedMessage` | Inline in `StreamMessageContent` | +| `thread_painter.dart` | `ThreadReplyPainter` | `core.StreamMessageReplies` | +| `thread_participants.dart` | `ThreadParticipants` | Inline in `core.StreamMessageReplies` | +| `user_avatar_transform.dart` | `UserAvatarTransform` | `StreamUserAvatar` (inline in `DefaultStreamMessage`) | +| `username.dart` | `Username` | Inline in `StreamMessageMetadata` | +| `sending_indicator_builder.dart` | `SendingIndicatorBuilder` | `StreamMessageSendingStatus` | + +--- + +## Typedef Changes + +| Old Typedef | New Typedef | +|---|---| +| `MessageBuilder = Widget Function(BuildContext, MessageDetails, List, StreamMessageWidget)` | `StreamMessageWidgetBuilder = Widget Function(BuildContext, Message, StreamMessageWidgetProps)` | +| `ParentMessageBuilder = Widget Function(BuildContext, Message?, StreamMessageWidget)` | `StreamMessageWidgetBuilder` (same as above) | +| `OnQuotedMessageTap = void Function(String?)` | Removed — use `void Function(Message)` directly | +| — | `MessageActionsBuilder = List Function(BuildContext, List>)` (new) | + +> **Note:** `MessageBuilder` and `ParentMessageBuilder` are removed from `typedefs.dart`. The new `StreamMessageWidgetBuilder` is defined in `message_list_view.dart` and exported via the barrel file. + +--- + +## Migration Checklist + +- [ ] Replace `StreamMessageWidget(message:, messageTheme:, ...)` with `StreamMessageWidget(message:)` — theme is now resolved from context +- [ ] Remove all `show*` boolean parameters — visibility is now controlled via `StreamMessageItemThemeData` and channel permissions +- [ ] Remove `customActions` and `onCustomActionTap` — use `actionsBuilder` via component factory or `StreamMessageWidgetProps.copyWith()` +- [ ] Remove all per-widget builder callbacks (`userAvatarBuilder`, `textBuilder`, `quotedMessageBuilder`, `deletedMessageBuilder`, `bottomRowBuilderWithDefaultWidget`, `reactionPickerBuilder`, `reactionIndicatorBuilder`) — use component factory instead +- [ ] Remove `shape`, `borderSide`, `borderRadiusGeometry`, `attachmentShape`, `textPadding`, `attachmentPadding` — controlled via `StreamMessageBubble` theming +- [ ] Remove `reverse` — determined by `StreamMessagePlacement` context +- [ ] Remove `translateUserAvatar` — avatar positioning is theme-driven +- [ ] Update `messageBuilder` / `parentMessageBuilder` callbacks to new `StreamMessageWidgetBuilder` signature +- [ ] Replace `MessageDetails` usage — use `StreamMessagePlacement.of(context)` for alignment, `StreamChat.of(context).currentUser` for user ID +- [ ] Update `onLinkTap` to `onMessageLinkTap` with new signature `void Function(Message, String)` +- [ ] Update `onMentionTap` to `onUserMentionTap` +- [ ] Update `onQuotedMessageTap` from `void Function(String?)` to `void Function(Message)` +- [ ] Replace `StreamDeletedMessage` with `StreamMessageDeleted` +- [ ] Replace `StreamMessageAction` with `StreamContextMenuAction` (see [message_actions.md](message_actions.md)) +- [ ] Replace `StreamSvgIcon(icon: StreamSvgIcons.*)` with `Icon(context.streamIcons.*)` +- [ ] Remove `StreamMessageWidget.copyWith()` usage — use `StreamMessageWidgetProps.copyWith()` instead diff --git a/migrations/redesign/reaction_list.md b/migrations/redesign/reaction_list.md new file mode 100644 index 0000000000..df9539beb3 --- /dev/null +++ b/migrations/redesign/reaction_list.md @@ -0,0 +1,173 @@ +# Reaction List Migration Guide + +This guide covers the new reaction list controller, view, and detail sheet introduced in the Stream Chat Flutter SDK design refresh. + +--- + +## Table of Contents + +- [StreamReactionListController](#streamreactionlistcontroller) +- [StreamReactionListView](#streamreactionlistview) +- [ReactionDetailSheet](#reactiondetailsheet) +- [Migration Checklist](#migration-checklist) + +--- + +## StreamReactionListController + +`StreamReactionListController` is a new controller in `stream_chat_flutter_core` for fetching and paginating reactions for a message. It extends `PagedValueNotifier`, following the same pattern as `StreamChannelListController` and other list controllers. + +### Constructor + +```dart +StreamReactionListController({ + required StreamChatClient client, + required String messageId, + Filter? filter, + SortOrder? sort, + int limit = 25, // defaultReactionPagedLimit +}) +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `client` | `StreamChatClient` | **required** | The Stream chat client | +| `messageId` | `String` | **required** | ID of the message to load reactions for | +| `filter` | `Filter?` | `null` | Query filter; supports fields `type`, `user_id`, `created_at` | +| `sort` | `SortOrder?` | `null` | Sort order; only `created_at` is backend-supported (`ReactionSortKey.createdAt`) | +| `limit` | `int` | `25` | Page size | + +### Methods + +| Method | Description | +|--------|-------------| +| `doInitialLoad()` | Loads the first page of reactions | +| `loadMore(String? nextPageKey)` | Loads the next page using cursor-based pagination | +| `refresh({bool resetValue = true})` | Reloads from the beginning; resets active filter/sort to constructor values when `resetValue` is `true` | + +### Runtime Filter / Sort Changes + +You can update `filter` and `sort` at runtime (e.g., when the user taps a reaction-type tab) and then call `doInitialLoad()` to reload: + +```dart +controller.filter = Filter.equal('type', 'like'); +controller.doInitialLoad(); +``` + +### Basic Usage + +```dart +final controller = StreamReactionListController( + client: StreamChat.of(context).client, + messageId: message.id, + sort: const [SortOption.desc(ReactionSortKey.createdAt)], +); + +await controller.doInitialLoad(); +``` + +--- + +## StreamReactionListView + +`StreamReactionListView` is a new widget in `stream_chat_flutter` that renders a paginated list of reactions using a `StreamReactionListController`. + +### Constructor + +```dart +StreamReactionListView({ + required StreamReactionListController controller, + required StreamReactionListViewIndexedWidgetBuilder itemBuilder, + PagedValueScrollViewIndexedWidgetBuilder? separatorBuilder, + WidgetBuilder? emptyBuilder, + WidgetBuilder? loadingBuilder, + Widget Function(BuildContext, StreamChatError)? errorBuilder, + int loadMoreTriggerIndex = 3, + // Standard ListView params: scrollDirection, reverse, scrollController, + // primary, physics, shrinkWrap, padding, cacheExtent, etc. +}) +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `controller` | `StreamReactionListController` | yes | Provides and paginates the reaction data | +| `itemBuilder` | `StreamReactionListViewIndexedWidgetBuilder` | yes | Builds each reaction item | +| `separatorBuilder` | `PagedValueScrollViewIndexedWidgetBuilder?` | no | Builds separators between items (defaults to `SizedBox.shrink`) | +| `emptyBuilder` | `WidgetBuilder?` | no | Widget shown when there are no reactions | +| `loadingBuilder` | `WidgetBuilder?` | no | Widget shown during initial load | +| `errorBuilder` | `Widget Function(BuildContext, StreamChatError)?` | no | Widget shown on error | +| `loadMoreTriggerIndex` | `int` | no | How many items from the end to trigger the next page load (default: 3) | + +### Usage + +```dart +StreamReactionListView( + controller: controller, + itemBuilder: (context, reactions, index) { + final reaction = reactions[index]; + return ListTile( + leading: Text(reaction.type), + title: Text(reaction.user?.name ?? ''), + ); + }, +) +``` + +--- + +## ReactionDetailSheet + +`ReactionDetailSheet` replaces the old `MessageReactionsModal`. It shows a bottom sheet with the total reaction count, emoji filter chips per reaction type, and a scrollable list of reactors using `StreamReactionListController` internally. + +### Showing the Sheet + +Use the static `show` method — the constructor is private: + +```dart +final action = await ReactionDetailSheet.show( + context: context, + message: message, + initialReactionType: 'like', // optional: pre-select a reaction type +); +``` + +`show` returns a `MessageAction?`: +- `SelectReaction` — if the user picks or removes a reaction +- `null` — if the sheet is dismissed without selection + +### Parameters of `show` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `context` | `BuildContext` | yes | Build context | +| `message` | `Message` | yes | The message whose reactions to display | +| `initialReactionType` | `String?` | no | Pre-selects this reaction type chip when the sheet opens | + +### Migration from `MessageReactionsModal` + +**Before:** +```dart +showDialog( + context: context, + builder: (_) => MessageReactionsModal(message: message), +); +``` + +**After:** +```dart +await ReactionDetailSheet.show( + context: context, + message: message, +); +``` + +> **Note:** `ReactionDetailSheet` is displayed as a `DraggableScrollableSheet` (snapping between 50% and full height) and supports cursor-based pagination for large reaction lists. + +--- + +## Migration Checklist + +- [ ] Replace `MessageReactionsModal` with `ReactionDetailSheet.show()` +- [ ] Use `StreamReactionListController` to load/paginate reactions programmatically +- [ ] Use `StreamReactionListView` with a `StreamReactionListController` for custom reaction list UIs +- [ ] For runtime reaction-type filtering, set `controller.filter` and call `controller.doInitialLoad()` diff --git a/migrations/redesign/reaction_picker.md b/migrations/redesign/reaction_picker.md new file mode 100644 index 0000000000..ce2acf3433 --- /dev/null +++ b/migrations/redesign/reaction_picker.md @@ -0,0 +1,341 @@ +# Reaction Picker Migration Guide + +This guide covers the migration for the redesigned reaction picker and reaction indicator components in Stream Chat Flutter SDK. + +--- + +## Table of Contents + +- [Quick Reference](#quick-reference) +- [StreamChatConfigurationData](#streamchatconfigurationdata) +- [Removed Icon-List APIs](#removed-icon-list-apis) +- [ReactionIconResolver and DefaultReactionIconResolver](#reactioniconresolver-and-defaultreactioniconresolver) +- [StreamMessageReactionPicker](#streammessagereactionpicker-formerly-streamreactionpicker) +- [StreamReactionIndicator](#streamreactionindicator) +- [New Components](#new-components) +- [Migration Checklist](#migration-checklist) + +--- + +## Quick Reference + +| Symbol | Change | +|--------|--------| +| `StreamChatConfigurationData.reactionIcons` | **Removed** — replaced by `reactionIconResolver` | +| `StreamChatConfigurationData.reactionIconResolver` | **New** — optional (default: `DefaultReactionIconResolver()`). Replaces `reactionIcons` | +| `ReactionIconResolver` | **New** — abstract contract for mapping reaction type → `StreamEmojiContent` | +| `DefaultReactionIconResolver` | **New** — ready-to-use default; extend to customize `defaultReactions`, `emojiCode`, or `resolve` | +| `ReactionPickerIconList` / `ReactionIndicatorIconList` | **Removed** — list rendering now lives inside picker/indicator widgets | +| `ReactionPickerIcon` / `ReactionIndicatorIcon` | **Removed** — use resolver-based reaction mapping instead | +| `StreamReactionPicker` | **Renamed** to `StreamMessageReactionPicker` — reaction set from `config.reactionIconResolver.defaultReactions` only | +| `StreamReactionPickerTheme` / `StreamReactionPickerThemeData` | **New** (from `stream_core_flutter`) — theme-based visual customisation for the picker | +| `StreamReactionIndicator` | **Changed** — uses `config.reactionIconResolver.resolve(type)` only | +| `ReactionDetailSheet` | **New** — `ReactionDetailSheet.show()` for reaction details bottom sheet | + +> **Note:** If you were using default reactions only, behavior stays the same (`like`, `haha`, `love`, `wow`, `sad`). Migration is required only for custom reaction icon/type setups. + +--- + +## StreamChatConfigurationData + +### Breaking Changes: + +- `reactionIcons` **removed** — was a list of reaction type + builder pairs for picker/indicator +- `reactionIconResolver` **new** — optional; defaults to `DefaultReactionIconResolver()`. All reaction UI uses it + +### Migration + +**Before:** +```dart +StreamChat( + client: client, + streamChatConfigData: StreamChatConfigurationData( + reactionIcons: [ /* type + builder per reaction */ ], + ), + child: MyApp(), +) +``` + +**After:** +```dart +StreamChat( + client: client, + streamChatConfigData: StreamChatConfigurationData( + reactionIconResolver: const MyReactionIconResolver(), + ), + child: MyApp(), +) +``` + +Extend `DefaultReactionIconResolver` (see below), pass as `reactionIconResolver`. Omit to keep defaults. + +> **Important:** +> - Resolver replaces the old list: use `defaultReactions` + `resolve(type)` (which uses `emojiCode(type)`) + +--- + +## Removed Icon-List APIs + +### Breaking Changes: + +- `ReactionPickerIconList` and `ReactionIndicatorIconList` were removed +- `ReactionPickerIcon` and `ReactionIndicatorIcon` were removed +- Per-widget icon list injection moved to a single global resolver (`StreamChatConfigurationData.reactionIconResolver`) + +### Migration + +**Before:** +```dart +StreamChat( + client: client, + streamChatConfigData: StreamChatConfigurationData( + reactionIcons: [ + // old reaction icon entries + ], + ), + child: MyApp(), +) +``` + +**After:** +```dart +class MyReactionIconResolver extends DefaultReactionIconResolver { + const MyReactionIconResolver(); + + @override + Set get defaultReactions => const {'like', 'love', 'celebrate'}; + + @override + String? emojiCode(String type) { + if (type == 'celebrate') return '🎉'; + return super.emojiCode(type); + } +} + +StreamChat( + client: client, + streamChatConfigData: StreamChatConfigurationData( + reactionIconResolver: const MyReactionIconResolver(), + ), + child: MyApp(), +) +``` + +--- + +## ReactionIconResolver and DefaultReactionIconResolver + +Picker uses `defaultReactions`; picker and indicator call `resolve(type)` → uses `emojiCode(type)` to return a `StreamEmojiContent` model. Extend `DefaultReactionIconResolver` and override only what you need. + +### Contract + +- **`defaultReactions`** — types in quick-pick bar. Every type here must be resolvable by `emojiCode(type)` (return non-null emoji) or fallback is shown. +- **`emojiCode(type)`** — return Unicode emoji (e.g. `'👍'`) or `null`. Used by `resolve`. +- **`supportedReactions`** — full resolver-supported type set. Keep this in sync with your resolver implementation. +- **`resolve(type)`** — returns a `StreamEmojiContent` for display. Default: `emojiCode(type)` → `StreamUnicodeEmoji` else `StreamUnicodeEmoji('❓')`. Override to return `StreamImageEmoji` for custom emoji. + +Override points on `DefaultReactionIconResolver`: `defaultReactions`, `emojiCode`, `resolve`, `supportedReactions`. + +### Migration (custom quick-pick set) + +Restrict `defaultReactions` to keys in `streamSupportedEmojis` so inherited `emojiCode` returns the emoji. + +**Before:** Custom list of reaction types on config or picker. + +**After:** +```dart +class MyReactionIconResolver extends DefaultReactionIconResolver { + const MyReactionIconResolver(); + static const _defaults = {'like', 'haha', 'love', 'wow', 'sad'}; + + @override + Set get defaultReactions => _defaults; +} +// StreamChatConfigurationData(reactionIconResolver: const MyReactionIconResolver(), ...) +``` + +> **Important:** If you add a type not in `streamSupportedEmojis`, override `emojiCode` to return the Unicode emoji for it (see next section). + +### Migration (custom types not in streamSupportedEmojis) + +Override `defaultReactions` (and/or `supportedReactions`) and `emojiCode` so every type has an emoji. + +**After:** +```dart +class MyReactionIconResolver extends DefaultReactionIconResolver { + const MyReactionIconResolver(); + static const _defaults = {'like', 'love', 'custom_celebration'}; + static const _supported = {'like', 'love', 'custom_celebration'}; + static const _customEmojis = {'custom_celebration': '🎉'}; + + @override + Set get defaultReactions => _defaults; + + @override + Set get supportedReactions => _supported; + + @override + String? emojiCode(String type) => _customEmojis[type] ?? streamSupportedEmojis[type]?.emoji; +} +``` + +### Migration (custom rendering, e.g. Twemoji) + +For type-based custom rendering (e.g. Twemoji assets keyed by reaction type), +override `resolve(type)` and return `StreamImageEmoji` for custom emoji. + +**After:** +```dart +class MyReactionIconResolver extends DefaultReactionIconResolver { + const MyReactionIconResolver(); + + @override + StreamEmojiContent resolve(String type) { + switch (type) { + case 'love': + return StreamImageEmoji(url: Uri.parse('https://cdn.example.com/twemoji/heart.png')); + case 'haha': + return StreamImageEmoji(url: Uri.parse('https://cdn.example.com/twemoji/joy.png')); + default: + return super.resolve(type); + } + } +} +``` + +--- + +## StreamMessageReactionPicker (formerly StreamReactionPicker) + +### Breaking Changes: + +- **Renamed** from `StreamReactionPicker` to `StreamMessageReactionPicker` +- `StreamReactionPicker` now refers to the domain-agnostic core component from `stream_core_flutter` +- Picker icons are no longer configured with per-widget icon models +- Quick-pick entries now come from `config.reactionIconResolver.defaultReactions` +- Visual properties (`backgroundColor`, `padding`, `shape`) removed from the widget — use `StreamReactionPickerTheme` instead +- The core picker now uses a `StreamComponentFactory` pattern with `StreamReactionPickerProps` for full customization + +### Migration + +**Before:** +```dart +StreamReactionPicker( + message: message, +) +``` + +**After:** +```dart +StreamMessageReactionPicker( + message: message, + onReactionPicked: onReactionPicked, +) +``` + +Configure reactions globally via `reactionIconResolver`: + +```dart +StreamChat( + client: client, + streamChatConfigData: StreamChatConfigurationData( + reactionIconResolver: const MyReactionIconResolver(), + ), + child: MyApp(), +) +``` + +Customize visual appearance via theme: + +```dart +StreamReactionPickerTheme( + data: StreamReactionPickerThemeData( + backgroundColor: Colors.white, + elevation: 4, + spacing: 2, + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.all(Radius.circular(24)), + ), + side: BorderSide(color: Colors.grey), + ), + child: // ... +) +``` + +--- + +## StreamReactionIndicator + +### Breaking Changes: + +- Indicator icons are resolved only through `config.reactionIconResolver.resolve(type)` +- Old icon-list based customization paths were removed + +### Migration + +**Before:** +```dart +StreamChat( + client: client, + streamChatConfigData: StreamChatConfigurationData( + reactionIcons: [ /* old icon list */ ], + ), + child: MyApp(), +) +``` + +**After:** +```dart +StreamChat( + client: client, + streamChatConfigData: StreamChatConfigurationData( + reactionIconResolver: const MyReactionIconResolver(), + ), + child: MyApp(), +) +``` + +Then keep indicator usage unchanged: + +```dart +StreamReactionIndicator( + message: message, + onTap: onTap, +) +``` + +Customize via `reactionIconResolver` in config. + +--- + +## New Components + +### ReactionDetailSheet + +Bottom sheet: reaction counts, filter chips per type, list of users. Returns `Future` (e.g. `SelectReaction`). + +```dart +final action = await ReactionDetailSheet.show( + context: context, + message: message, + initialReactionType: selectedType, // optional +); +if (action is SelectReaction) handleSelectReaction(action); +``` + +### ReactionIconResolver / DefaultReactionIconResolver + +Exported for `StreamChatConfigurationData`. See [ReactionIconResolver and DefaultReactionIconResolver](#reactioniconresolver-and-defaultreactioniconresolver). + +--- + +## Migration Checklist + +- [ ] Rename `StreamReactionPicker` → `StreamMessageReactionPicker` in your code +- [ ] Remove `reactionIcons` from `StreamChatConfigurationData` +- [ ] Remove `backgroundColor`, `padding`, `shape` props from picker usage — use `StreamReactionPickerTheme` instead +- [ ] Custom quick-pick: extend `DefaultReactionIconResolver`, override `defaultReactions` with types from `streamSupportedEmojis` (so `emojiCode` returns emoji); set `reactionIconResolver` +- [ ] Custom types not in `streamSupportedEmojis`: also override `emojiCode` to return Unicode emoji for each; optionally `supportedReactions` +- [ ] Custom rendering (e.g. Twemoji): extend `DefaultReactionIconResolver`, override `resolve(type)` to return `StreamImageEmoji`, set `reactionIconResolver` +- [ ] Remove old icon-list based customization and configure reactions via `reactionIconResolver` only +- [ ] Optionally use `ReactionDetailSheet.show()` diff --git a/migrations/redesign/stream_avatar.md b/migrations/redesign/stream_avatar.md new file mode 100644 index 0000000000..4897a4c7f2 --- /dev/null +++ b/migrations/redesign/stream_avatar.md @@ -0,0 +1,227 @@ +# Stream Avatar Components Migration Guide + +This guide covers the migration for the redesigned avatar components in Stream Chat Flutter SDK. + +--- + +## Table of Contents + +- [Quick Reference](#quick-reference) +- [StreamUserAvatar](#streamuseravatar) +- [StreamChannelAvatar](#streamchannelavatar) +- [StreamGroupAvatar](#streamgroupavatar) +- [StreamUserAvatarStack](#streamuseravatarstack) +- [Size Reference](#size-reference) +- [Migration Checklist](#migration-checklist) + +--- + +## Quick Reference + +| Component | Key Changes | +|-----------|-------------| +| [**StreamUserAvatar**](#streamuseravatar) | `constraints` → `size` enum, `showOnlineStatus` → `showOnlineIndicator`, `onTap` removed | +| [**StreamChannelAvatar**](#streamchannelavatar) | `constraints` → `size` enum, `onTap` and builder callbacks removed | +| [**StreamGroupAvatar**](#streamgroupavatar) | Renamed to `StreamUserAvatarGroup`, `members` → `users` | +| [**StreamUserAvatarStack**](#streamuseravatarstack) | New component for overlapping avatars | + +--- + +## StreamUserAvatar + +### Breaking Changes: + +- `constraints` parameter replaced with `size` enum (`StreamAvatarSize`) +- `showOnlineStatus` renamed to `showOnlineIndicator` +- `onTap` callback removed — wrap with `GestureDetector` or `InkWell` instead +- `borderRadius` parameter removed +- `selected`, `selectionColor`, `selectionThickness` parameters removed +- `onlineIndicatorAlignment` and `onlineIndicatorConstraints` removed + +### Migration: + +**Before:** +```dart +StreamUserAvatar( + user: user, + constraints: BoxConstraints.tight(const Size(40, 40)), + borderRadius: BorderRadius.circular(20), + showOnlineStatus: false, + onTap: (user) => print('Tapped ${user.name}'), +) +``` + +**After:** +```dart +GestureDetector( + onTap: () => print('Tapped ${user.name}'), + child: StreamUserAvatar( + size: StreamAvatarSize.lg, + user: user, + showOnlineIndicator: false, + ), +) +``` + +> **Important:** +> - Use `GestureDetector` or `InkWell` to handle tap events +> - Use `StreamAvatarSize` enum values (`.xs`, `.sm`, `.md`, `.lg`, `.xl`, `.xxl`) instead of `BoxConstraints` +> - See [Size Reference](#size-reference) for mapping old constraints to new enum values + +--- + +## StreamChannelAvatar + +### Breaking Changes: + +- `constraints` parameter replaced with `size` enum (`StreamAvatarGroupSize`) +- `onTap` callback removed — wrap with `GestureDetector` or `InkWell` instead +- `borderRadius` parameter removed +- `selected`, `selectionColor`, `selectionThickness` parameters removed +- `ownSpaceAvatarBuilder`, `oneToOneAvatarBuilder`, `groupAvatarBuilder` callbacks removed + +### Migration: + +**Before:** +```dart +StreamChannelAvatar( + channel: channel, + constraints: BoxConstraints.tight(const Size(40, 40)), + onTap: () => print('Tapped channel'), + selected: isSelected, +) +``` + +**After:** +```dart +GestureDetector( + onTap: () => print('Tapped channel'), + child: StreamChannelAvatar( + size: StreamAvatarGroupSize.lg, + channel: channel, + ), +) +``` + +> **Important:** +> - Use `StreamAvatarGroupSize` enum values (`.lg`, `.xl`, `.xxl`) instead of `BoxConstraints` +> - Custom avatar builders are no longer supported + +--- + +## StreamGroupAvatar + +### Breaking Changes: + +- Renamed from `StreamGroupAvatar` to `StreamUserAvatarGroup` +- `members` parameter replaced with `users` (`Iterable` instead of `List`) +- `constraints` parameter replaced with `size` enum (`StreamAvatarGroupSize`) +- `channel` parameter removed +- `onTap` callback removed — wrap with `GestureDetector` or `InkWell` instead +- `borderRadius` parameter removed +- `selected`, `selectionColor`, `selectionThickness` parameters removed + +### Migration: + +**Before:** +```dart +StreamGroupAvatar( + channel: channel, + members: otherMembers, + constraints: BoxConstraints.tight(const Size(40, 40)), + onTap: () => print('Tapped group'), +) +``` + +**After:** +```dart +GestureDetector( + onTap: () => print('Tapped group'), + child: StreamUserAvatarGroup( + size: StreamAvatarGroupSize.lg, + users: otherMembers.map((m) => m.user!), + ), +) +``` + +> **Important:** +> - Extract `User` objects from `Member` when migrating: `members.map((m) => m.user!)` +> - The component no longer requires a `channel` reference + +--- + +## StreamUserAvatarStack + +### Breaking Changes: + +- **New component** for displaying overlapping user avatars (e.g., thread participants) +- Replaces custom `Stack` + `Positioned` implementations + +### Parameters: + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `users` | `Iterable` | required | Users to display | +| `size` | `StreamAvatarStackSize?` | `.sm` | Size of avatars | +| `max` | `int` | `5` | Max avatars before overflow badge | +| `overlap` | `double` | `0.33` | Overlap fraction (0.0 - 1.0) | + +### Usage: + +```dart +StreamUserAvatarStack( + max: 3, + size: StreamAvatarStackSize.xs, + users: threadParticipants, +) +``` + +> **Important:** +> - Use this component instead of manually building overlapping avatar stacks +> - The `overlap` parameter controls how much each avatar overlaps the previous one + +--- + +## Size Reference + +### StreamAvatarSize + +| Old Constraints | New Size | Diameter | +|-----------------|----------|----------| +| `BoxConstraints.tight(Size(20, 20))` | `.xs` | 20px | +| `BoxConstraints.tight(Size(24, 24))` | `.sm` | 24px | +| `BoxConstraints.tight(Size(32, 32))` | `.md` | 32px | +| `BoxConstraints.tight(Size(40, 40))` | `.lg` | 40px | +| `BoxConstraints.tight(Size(48, 48))` | `.xl` | 48px | +| `BoxConstraints.tight(Size(80, 80))` | `.xxl` | 80px | + +### StreamAvatarGroupSize + +| Old Constraints | New Size | Diameter | +|-----------------|----------|----------| +| `BoxConstraints.tight(Size(40, 40))` | `.lg` | 40px | +| `BoxConstraints.tight(Size(48, 48))` | `.xl` | 48px | +| `BoxConstraints.tight(Size(80, 80))` | `.xxl` | 80px | + +### StreamAvatarStackSize + +| Old Constraints | New Size | Diameter | +|-----------------|----------|----------| +| `BoxConstraints.tight(Size(20, 20))` | `.xs` | 20px | +| `BoxConstraints.tight(Size(24, 24))` | `.sm` | 24px | + +> **Note:** +> If your old constraints don't match exactly, choose the closest available size. + +--- + +## Migration Checklist + +- [ ] Replace `StreamUserAvatar` `constraints` with `size` enum (`StreamAvatarSize`) +- [ ] Rename `showOnlineStatus` to `showOnlineIndicator` +- [ ] Move `onTap` callbacks to parent `GestureDetector` or `InkWell` widgets +- [ ] Replace `StreamGroupAvatar` with `StreamUserAvatarGroup` +- [ ] Change `members` parameter to `users` (extract `User` from `Member`) +- [ ] Replace `StreamChannelAvatar` `constraints` with `size` enum (`StreamAvatarGroupSize`) +- [ ] Remove `selected`, `selectionColor`, `selectionThickness` parameters +- [ ] Use `StreamUserAvatarStack` for overlapping avatar displays diff --git a/migrations/redesign/unread_indicator.md b/migrations/redesign/unread_indicator.md new file mode 100644 index 0000000000..0ba95a681e --- /dev/null +++ b/migrations/redesign/unread_indicator.md @@ -0,0 +1,140 @@ +# Unread Indicator Migration Guide + +This guide covers the migration for the unread indicator components in the Stream Chat Flutter SDK design refresh. + +--- + +## Table of Contents + +- [StreamUnreadIndicator](#streamunreadindicator) +- [UnreadIndicatorButton](#unreadindicatorbutton) +- [Migration Checklist](#migration-checklist) + +--- + +## StreamUnreadIndicator + +`StreamUnreadIndicator` shows a small badge with an unread count. It now wraps `StreamBadgeNotification` (from `stream_core_flutter`) and the custom styling parameters have been removed. + +### Breaking Changes + +- `backgroundColor`, `textColor`, and `textStyle` constructor parameters removed — styling is now controlled via `StreamTheme` +- The widget is now wrapped in `IgnorePointer`; it does not respond to taps itself +- Now supports named constructors for different unread count types + +### Named Constructors + +| Constructor | Description | +|-------------|-------------| +| `StreamUnreadIndicator()` | Shows total unread message count | +| `StreamUnreadIndicator.channels({String? cid})` | Shows unread channel count; optionally filtered to a specific channel by `cid` | +| `StreamUnreadIndicator.threads({String? id})` | Shows unread thread count | + +### Migration + +**Before:** +```dart +StreamUnreadIndicator( + backgroundColor: Colors.red, + textColor: Colors.white, + textStyle: TextStyle(fontSize: 12), +) +``` + +**After:** +```dart +// Styling via StreamTheme — see README.md for theming setup +StreamUnreadIndicator() +``` + +> **Note:** The badge automatically displays `99+` for counts above 99. + +--- + +## UnreadIndicatorButton + +`UnreadIndicatorButton` is the floating button shown inside `StreamMessageListView` to indicate unread messages below. It has been completely redesigned. + +### Breaking Changes + +#### New layout (40px height Material container): +- Arrow-up icon → unread count label → vertical divider → dismiss (×) icon +- Replaces the old simple badge button + +#### New callback types + +| Type | Signature | Description | +|------|-----------|-------------| +| `OnUnreadIndicatorTap` | `Future Function(String? lastReadMessageId)` | Called when the main area is tapped; receives the last-read message ID | +| `OnUnreadIndicatorDismissTap` | `Future Function()` | Called when the dismiss (×) button is tapped | + +Both callbacks are `Future` — ensure your implementations are async-compatible. + +#### New `UnreadIndicatorProps` class + +A new `UnreadIndicatorProps` class carries configuration through the component factory: + +```dart +class UnreadIndicatorProps { + final int unreadCount; + final OnUnreadIndicatorTap onTap; + final OnUnreadIndicatorDismissTap onDismissTap; +} +``` + +### Constructor Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `onTap` | `OnUnreadIndicatorTap` | yes | Called when indicator is tapped; receives `lastReadMessageId` | +| `onDismissTap` | `OnUnreadIndicatorDismissTap` | yes | Called when dismiss button is tapped | +| `unreadIndicatorBuilder` | `UnreadIndicatorBuilder?` | no | Optional inline builder; takes priority over component factory | + +### Migration + +**Before:** +```dart +UnreadIndicatorButton( + onTap: () => _scrollToUnread(), + onDismiss: () => _markAllRead(), +) +``` + +**After:** +```dart +UnreadIndicatorButton( + onTap: (lastReadMessageId) async => _scrollToUnread(lastReadMessageId), + onDismissTap: () async => _markAllRead(), +) +``` + +### Customization via Component Factory + +To fully replace the unread indicator button, provide a builder for `UnreadIndicatorProps`: + +```dart +StreamComponentFactory( + builders: StreamComponentBuilders( + extensions: streamChatComponentBuilders( + unreadIndicator: (context, props) => MyCustomUnreadButton( + count: props.unreadCount, + onTap: props.onTap, + onDismiss: props.onDismissTap, + ), + ), + ), + child: ..., +) +``` + +Alternatively, pass `unreadIndicatorBuilder` directly to `UnreadIndicatorButton` for one-off overrides. + +--- + +## Migration Checklist + +- [ ] Remove `backgroundColor`, `textColor`, `textStyle` from `StreamUnreadIndicator` usages +- [ ] Use the appropriate named constructor (`StreamUnreadIndicator()`, `.channels()`, `.threads()`) +- [ ] Update `UnreadIndicatorButton` callbacks: `onDismiss` → `onDismissTap`, `onTap` now receives `String? lastReadMessageId` +- [ ] Make callback implementations `async` (return `Future`) +- [ ] Move custom indicator layouts to `StreamComponentFactory` or `unreadIndicatorBuilder` diff --git a/migrations/v10-migration.md b/migrations/v10-migration.md new file mode 100644 index 0000000000..895dec3e25 --- /dev/null +++ b/migrations/v10-migration.md @@ -0,0 +1,1076 @@ +# Stream Chat Flutter SDK v10.0.0 Migration Guide + +This guide covers all breaking changes in **Stream Chat Flutter SDK v10.0.0**. Whether you're upgrading from v9.x or from a v10 beta, this document provides the complete migration path. + +--- + +## Table of Contents + +- [Who Should Read This](#who-should-read-this) +- [Quick Reference](#quick-reference) +- [Attachment Picker](#attachment-picker) + - [AttachmentPickerType](#attachmentpickertype) + - [StreamAttachmentPickerOption](#streamattachmentpickeroption) + - [showStreamAttachmentPickerModalBottomSheet](#showstreamattachmentpickermodalbottomsheet) + - [AttachmentPickerBottomSheet](#attachmentpickerbottomsheet) + - [customAttachmentPickerOptions](#customattachmentpickeroptions) + - [onCustomAttachmentPickerResult](#oncustomattachmentpickerresult) + - [StreamAttachmentPickerController](#streamattachmentpickercontroller) +- [Reactions](#reactions) + - [SendReaction](#sendreaction) + - [StreamReactionPicker](#streamreactionpicker) + - [ReactionPickerIconList](#reactionpickericonlist) + - [StreamMessageReactionsModal](#streammessagereactionsmodal) +- [Message UI](#message-ui) + - [onAttachmentTap](#onattachmenttap) + - [StreamMessageWidget](#streammessagewidget) + - [StreamMessageAction](#streammessageaction) +- [Message State & Deletion](#message-state--deletion) + - [MessageState](#messagestate) +- [File Upload](#file-upload) + - [AttachmentFileUploader](#attachmentfileuploader) +- [Appendix: Beta Release Timeline](#appendix-beta-release-timeline) +- [Migration Checklist](#migration-checklist) + +--- + +## Who Should Read This + +| Upgrading From | Sections to Review | +|----------------|-------------------| +| **v9.x** | All sections | +| [**v10.0.0-beta.1**](#v1000-beta1) | All sections introduced after beta.1 | +| [**v10.0.0-beta.3**](#v1000-beta3) | Sections introduced in beta.4 and later | +| [**v10.0.0-beta.4**](#v1000-beta4) | Sections introduced in beta.7 and later | +| [**v10.0.0-beta.7**](#v1000-beta7) | Sections introduced in beta.8 and later | +| [**v10.0.0-beta.8**](#v1000-beta8) | Sections introduced in beta.9 and later | +| [**v10.0.0-beta.9**](#v1000-beta9) | Sections introduced in beta.12 | +| [**v10.0.0-beta.12**](#v1000-beta12) | No additional changes | + +Each breaking change section includes an **"Introduced in"** tag so you can quickly identify which changes apply to your upgrade path. + +--- + +## Quick Reference + +| Feature Area | Key Changes | +|-------------|-------------| +| [**Attachment Picker**](#attachment-picker) | Sealed class hierarchy, builder pattern for options, typed result handling | +| [**Reactions**](#reactions) | `Reaction` object API, explicit `onReactionPicked` callbacks required | +| [**Message UI**](#message-ui) | New `onAttachmentTap` signature with fallback support, generic `StreamMessageAction` | +| [**Message State**](#message-state--deletion) | `MessageDeleteScope` replaces `bool hard`, delete-for-me support | +| [**File Upload**](#file-upload) | Four new abstract methods on `AttachmentFileUploader` | + +--- + +## Attachment Picker + +The attachment picker system has been redesigned with a sealed class hierarchy, improved type safety, and a flexible builder pattern for customization. + +--- + +### AttachmentPickerType + +> **Introduced in:** [v10.0.0-beta.3](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.3) + +#### 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 + +> **Introduced in:** [v10.0.0-beta.3](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.3) + +#### 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 + +> **Introduced in:** [v10.0.0-beta.3](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.3) + +#### 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 + +> **Introduced in:** [v10.0.0-beta.3](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.3) + +#### 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. + +--- + +### customAttachmentPickerOptions + +> **Introduced in:** [v10.0.0-beta.8](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.8) + +#### Key Changes: + +- `customAttachmentPickerOptions` has been removed. Use `attachmentPickerOptionsBuilder` instead. +- New builder pattern provides access to default options which can be modified, reordered, or extended. + +#### Migration Steps: + +**Before:** +```dart +StreamMessageInput( + customAttachmentPickerOptions: [ + TabbedAttachmentPickerOption( + key: 'custom-location', + icon: const Icon(Icons.location_on), + supportedTypes: [AttachmentPickerType.images], + optionViewBuilder: (context, controller) { + return CustomLocationPicker(); + }, + ), + ], +) +``` + +**After:** +```dart +StreamMessageInput( + attachmentPickerOptionsBuilder: (context, defaultOptions) { + // You can now modify, filter, reorder, or extend default options + return [ + ...defaultOptions, + TabbedAttachmentPickerOption( + key: 'custom-location', + icon: const Icon(Icons.location_on), + supportedTypes: [AttachmentPickerType.images], + optionViewBuilder: (context, controller) { + return CustomLocationPicker(); + }, + ), + ]; + }, +) +``` + +**Example: Filtering default options** +```dart +StreamMessageInput( + attachmentPickerOptionsBuilder: (context, defaultOptions) { + // Remove poll option + return defaultOptions.where((option) => option.key != 'poll').toList(); + }, +) +``` + +**Example: Reordering options** +```dart +StreamMessageInput( + attachmentPickerOptionsBuilder: (context, defaultOptions) { + // Reverse the order + return defaultOptions.reversed.toList(); + }, +) +``` + +**Using with `showStreamAttachmentPickerModalBottomSheet`:** +```dart +final result = await showStreamAttachmentPickerModalBottomSheet( + context: context, + optionsBuilder: (context, defaultOptions) { + return [ + ...defaultOptions, + TabbedAttachmentPickerOption( + key: 'custom-option', + icon: const Icon(Icons.star), + supportedTypes: [AttachmentPickerType.images], + optionViewBuilder: (context, controller) { + return CustomPickerView(); + }, + ), + ]; + }, +); +``` + +> **Important:** +> - The builder pattern gives you access to default options, allowing more flexible customization +> - The builder works with both mobile (tabbed) and desktop (system) pickers + +--- + +### onCustomAttachmentPickerResult + +> **Introduced in:** [v10.0.0-beta.8](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.8) + +#### Key Changes: + +- `onCustomAttachmentPickerResult` has been removed. Use `onAttachmentPickerResult` which returns `FutureOr`. +- Result handler can now short-circuit default behavior by returning `true`. + +#### Migration Steps: + +**Before:** +```dart +StreamMessageInput( + onCustomAttachmentPickerResult: (result) { + if (result is CustomAttachmentPickerResult) { + final data = result.data; + // Handle custom result + } + }, +) +``` + +**After:** +```dart +StreamMessageInput( + onAttachmentPickerResult: (result) { + if (result is CustomAttachmentPickerResult) { + final data = result.data; + // Handle custom result + return true; // Indicate we handled it - skips default processing + } + return false; // Let default handler process other result types + }, +) +``` + +> **Important:** +> - `onAttachmentPickerResult` replaces `onCustomAttachmentPickerResult` and must return a boolean +> - Return `true` from `onAttachmentPickerResult` to skip default handling +> - Return `false` to allow the default handler to process the result + +--- + +### StreamAttachmentPickerController + +> **Introduced in:** [v10.0.0-beta.12](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.12) + +#### Key Changes: + +- Replaced `ArgumentError('The size of the attachment is...')` with `AttachmentTooLargeError`. +- Replaced `ArgumentError('The maximum number of attachments is...')` with `AttachmentLimitReachedError`. + +#### Migration Steps: + +**Before:** +```dart +try { + await controller.addAttachment(attachment); +} on ArgumentError catch (e) { + // Generic error handling + showError(e.message); +} +``` + +**After:** +```dart +try { + await controller.addAttachment(attachment); +} on AttachmentTooLargeError catch (e) { + // File size exceeded + showError('File is too large. Max size is ${e.maxSize} bytes.'); +} on AttachmentLimitReachedError catch (e) { + // Too many attachments + showError('Cannot add more attachments. Maximum is ${e.maxCount}.'); +} +``` + +> **Important:** +> - Replace `ArgumentError` catches with the specific typed errors +> - `AttachmentTooLargeError` provides `fileSize` and `maxSize` properties +> - `AttachmentLimitReachedError` provides `maxCount` property + +--- + +## Reactions + +The reaction system has been updated to use explicit callbacks and a unified `Reaction` object API. + +--- + +### SendReaction + +> **Introduced in:** [v10.0.0-beta.4](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.4) + +#### 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 + +--- + +### StreamReactionPicker + +> **Introduced in:** [v10.0.0-beta.1](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.1) + +#### 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`. + +--- + +### ReactionPickerIconList + +> **Introduced in:** [v10.0.0-beta.9](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.9) + +#### Key Changes: + +- `message` parameter has been removed +- `reactionIcons` type changed from `List` to `List` +- `onReactionPicked` callback renamed to `onIconPicked` with new signature: `ValueSetter` +- `iconBuilder` parameter changed from default value to nullable with internal fallback +- Message-specific logic (checking for own reactions) moved to parent widget + +#### Migration Steps: + +**Before:** +```dart +ReactionPickerIconList( + message: message, + reactionIcons: icons, + onReactionPicked: (reaction) { + // Handle reaction + channel.sendReaction(message, reaction); + }, +) +``` + +**After:** +```dart +// Map StreamReactionIcon to ReactionPickerIcon with selection state +final ownReactions = [...?message.ownReactions]; +final ownReactionsMap = {for (final it in ownReactions) it.type: it}; + +final pickerIcons = icons.map((icon) { + return ReactionPickerIcon( + type: icon.type, + builder: icon.builder, + isSelected: ownReactionsMap[icon.type] != null, + ); +}).toList(); + +ReactionPickerIconList( + reactionIcons: pickerIcons, + onIconPicked: (pickerIcon) { + final reaction = ownReactionsMap[pickerIcon.type] ?? + Reaction(type: pickerIcon.type); + // Handle reaction + channel.sendReaction(message, reaction); + }, +) +``` + +> **Important:** +> - This is typically an internal widget used by `StreamReactionPicker` +> - If you were using it directly, you now need to handle reaction selection state externally +> - Use `StreamReactionPicker` for most use cases instead of `ReactionPickerIconList` + +--- + +### StreamMessageReactionsModal + +> **Introduced in:** [v10.0.0-beta.1](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.1) + +#### 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`. + +--- + +## Message UI + +Updates to message widgets, attachment handling, and custom action patterns. + +--- + +### onAttachmentTap + +> **Introduced in:** [v10.0.0-beta.9](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.9) + +#### Key Changes: + +- `onAttachmentTap` callback signature has changed to support custom attachment handling with automatic fallback to default behavior. +- Callback now receives `BuildContext` as the first parameter. +- Returns `FutureOr` to indicate whether the attachment was handled. +- Returning `true` skips default behavior, `false` uses default handling (URLs, images, videos, giphys). + +#### Migration Steps: + +**Before:** +```dart +StreamMessageWidget( + message: message, + onAttachmentTap: (message, attachment) { + // Could only override - no way to fallback to default behavior + if (attachment.type == 'location') { + showLocationDialog(context, attachment); + } + // Other attachment types (images, videos, URLs) lost default behavior + }, +) +``` + +**After:** +```dart +StreamMessageWidget( + message: message, + onAttachmentTap: (context, message, attachment) async { + if (attachment.type == 'location') { + await showLocationDialog(context, attachment); + return true; // Handled by custom logic + } + return false; // Use default behavior for images, videos, URLs, etc. + }, +) +``` + +**Example: Handling multiple custom types** +```dart +StreamMessageWidget( + message: message, + onAttachmentTap: (context, message, attachment) async { + switch (attachment.type) { + case 'location': + await Navigator.push( + context, + MaterialPageRoute(builder: (_) => MapView(attachment)), + ); + return true; + + case 'product': + await showProductDialog(context, attachment); + return true; + + default: + return false; // Images, videos, URLs use default viewer + } + }, +) +``` + +> **Important:** +> - The callback now requires `BuildContext` as the first parameter +> - Must return `FutureOr` - `true` if handled, `false` for default behavior +> - Default behavior automatically handles URL previews, images, videos, and giphys +> - Supports both synchronous and asynchronous operations + +--- + +### StreamMessageWidget + +> **Introduced in:** [v10.0.0-beta.1](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.1) + +#### 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. + +--- + +### StreamMessageAction + +> **Introduced in:** [v10.0.0-beta.1](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.1) + +#### 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`. + +--- + +## Message State & Deletion + +Message deletion now supports scoped deletion modes including delete-for-me functionality. + +--- + +### MessageState + +> **Introduced in:** [v10.0.0-beta.7](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.7) + +#### Key Changes: + +- `MessageState` factory constructors now accept `MessageDeleteScope` instead of `bool hard` parameter +- Pattern matching callbacks in state classes now receive `MessageDeleteScope scope` instead of `bool hard` +- New delete-for-me functionality with dedicated states and methods + +#### Migration Steps: + +**Before:** +```dart +// Factory constructors with bool hard +final deletingState = MessageState.deleting(hard: true); +final deletedState = MessageState.deleted(hard: false); +final failedState = MessageState.deletingFailed(hard: true); + +// Pattern matching with bool hard +message.state.whenOrNull( + deleting: (hard) => handleDeleting(hard), + deleted: (hard) => handleDeleted(hard), + deletingFailed: (hard) => handleDeletingFailed(hard), +); +``` + +**After:** +```dart +// Factory constructors with MessageDeleteScope +final deletingState = MessageState.deleting( + scope: MessageDeleteScope.hardDeleteForAll, +); +final deletedState = MessageState.deleted( + scope: MessageDeleteScope.softDeleteForAll, +); +final failedState = MessageState.deletingFailed( + scope: MessageDeleteScope.deleteForMe(), +); + +// Pattern matching with MessageDeleteScope +message.state.whenOrNull( + deleting: (scope) => handleDeleting(scope.hard), + deleted: (scope) => handleDeleted(scope.hard), + deletingFailed: (scope) => handleDeletingFailed(scope.hard), +); + +// New delete-for-me functionality +channel.deleteMessageForMe(message); // Delete only for current user +client.deleteMessageForMe(messageId); // Delete only for current user + +// Check delete-for-me states +if (message.state.isDeletingForMe) { + // Handle deleting for me state +} +if (message.state.isDeletedForMe) { + // Handle deleted for me state +} +if (message.state.isDeletingForMeFailed) { + // Handle delete for me failed state +} +``` + +> **Important:** +> - All `MessageState` factory constructors now require `MessageDeleteScope` parameter +> - Pattern matching callbacks receive `MessageDeleteScope` instead of `bool hard` +> - Use `scope.hard` to access the hard delete boolean value +> - New delete-for-me methods are available on both `Channel` and `StreamChatClient` + +--- + +## File Upload + +The file uploader interface has been expanded with standalone upload and removal methods. + +--- + +### AttachmentFileUploader + +> **Introduced in:** [v10.0.0-beta.7](https://pub.dev/packages/stream_chat_flutter/versions/10.0.0-beta.7) + +#### Key Changes: + +- `AttachmentFileUploader` interface now includes four new abstract methods: `uploadImage`, `uploadFile`, `removeImage`, and `removeFile`. +- Custom implementations must implement these new standalone upload/removal methods. + +#### Migration Steps: + +**Before:** +```dart +class CustomAttachmentFileUploader implements AttachmentFileUploader { + // Only needed to implement sendImage, sendFile, deleteImage, deleteFile + + @override + Future sendImage(/* ... */) async { + // Implementation + } + + @override + Future sendFile(/* ... */) async { + // Implementation + } + + @override + Future deleteImage(/* ... */) async { + // Implementation + } + + @override + Future deleteFile(/* ... */) async { + // Implementation + } +} +``` + +**After:** +```dart +class CustomAttachmentFileUploader implements AttachmentFileUploader { + // Must now implement all 8 methods including the new standalone ones + + @override + Future sendImage(/* ... */) async { + // Implementation + } + + @override + Future sendFile(/* ... */) async { + // Implementation + } + + @override + Future deleteImage(/* ... */) async { + // Implementation + } + + @override + Future deleteFile(/* ... */) async { + // Implementation + } + + // New required methods + @override + Future uploadImage( + AttachmentFile image, { + ProgressCallback? onSendProgress, + CancelToken? cancelToken, + }) async { + // Implementation for standalone image upload + } + + @override + Future uploadFile( + AttachmentFile file, { + ProgressCallback? onSendProgress, + CancelToken? cancelToken, + }) async { + // Implementation for standalone file upload + } + + @override + Future removeImage( + String url, { + CancelToken? cancelToken, + }) async { + // Implementation for standalone image removal + } + + @override + Future removeFile( + String url, { + CancelToken? cancelToken, + }) async { + // Implementation for standalone file removal + } +} +``` + +> **Important:** +> - Custom `AttachmentFileUploader` implementations must now implement four additional methods +> - The new methods support standalone uploads/removals without requiring channel context +> - `UploadImageResponse` and `UploadFileResponse` are aliases for `SendAttachmentResponse` + +--- + +## Appendix: Beta Release Timeline + +This appendix provides a chronological reference of breaking changes by beta version for users upgrading from specific pre-release versions. + +### v10.0.0-beta.1 + +- [StreamReactionPicker](#streamreactionpicker) +- [StreamMessageAction](#streammessageaction) +- [StreamMessageReactionsModal](#streammessagereactionsmodal) +- [StreamMessageWidget](#streammessagewidget) + +### v10.0.0-beta.3 + +- [AttachmentPickerType](#attachmentpickertype) +- [StreamAttachmentPickerOption](#streamattachmentpickeroption) +- [showStreamAttachmentPickerModalBottomSheet](#showstreamattachmentpickermodalbottomsheet) +- [AttachmentPickerBottomSheet](#attachmentpickerbottomsheet) + +### v10.0.0-beta.4 + +- [SendReaction](#sendreaction) + +### v10.0.0-beta.7 + +- [AttachmentFileUploader](#attachmentfileuploader) +- [MessageState](#messagestate) + +### v10.0.0-beta.8 + +- [customAttachmentPickerOptions](#customattachmentpickeroptions) +- [onCustomAttachmentPickerResult](#oncustomattachmentpickerresult) + +### v10.0.0-beta.9 + +- [onAttachmentTap](#onattachmenttap) +- [ReactionPickerIconList](#reactionpickericonlist) + +### v10.0.0-beta.12 + +- [StreamAttachmentPickerController](#streamattachmentpickercontroller) + +--- + +## Migration Checklist + +### For v10.0.0-beta.12: +- [ ] Replace `ArgumentError('The size of the attachment is...')` with `AttachmentTooLargeError` (provides `fileSize` and `maxSize` properties) +- [ ] Replace `ArgumentError('The maximum number of attachments is...')` with `AttachmentLimitReachedError` (provides `maxCount` property) + +### For v10.0.0-beta.9: +- [ ] Update `onAttachmentTap` callback signature to include `BuildContext` as first parameter +- [ ] Return `FutureOr` from `onAttachmentTap` - `true` if handled, `false` for default behavior +- [ ] Leverage automatic fallback to default handling for standard attachment types (images, videos, URLs) +- [ ] Update any direct usage of `ReactionPickerIconList` to handle reaction selection state externally + +### For v10.0.0-beta.8: +- [ ] Replace `customAttachmentPickerOptions` with `attachmentPickerOptionsBuilder` to access and modify default options +- [ ] Replace `onCustomAttachmentPickerResult` with `onAttachmentPickerResult` that returns `FutureOr` + +### For v10.0.0-beta.7: +- [ ] Update custom `AttachmentFileUploader` implementations to include the four new abstract methods: `uploadImage`, `uploadFile`, `removeImage`, and `removeFile` +- [ ] Update `MessageState` factory constructors to use `MessageDeleteScope` parameter +- [ ] Update pattern-matching callbacks to handle `MessageDeleteScope` instead of `bool hard` +- [ ] Leverage new delete-for-me functionality with `deleteMessageForMe` methods +- [ ] Use new state-checking methods for delete-for-me operations + +### For v10.0.0-beta.4: +- [ ] Update `sendReaction` method calls to use `Reaction` object instead of individual parameters + +### 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` + +--- + +**You're ready to migrate!** For additional help, visit the [Stream Chat Flutter documentation](https://getstream.io/chat/docs/sdk/flutter/) or open an issue on [GitHub](https://github.com/GetStream/stream-chat-flutter/issues). \ No newline at end of file diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 1998c95779..f24bbd24e2 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,7 +1,21 @@ +## 10.0.0-beta.13 + +🛑️ Breaking +- SDK Redesign Changes. For more details, please refer to the [migration guide](https://github.com/GetStream/stream-chat-flutter/blob/210ff93f955be3f85c62e860309bd9aa240a5446/migrations). + The SDK redesign introduces a fresher default UI, but also better APIs for customization of the components. + +## 10.0.0-beta.12 + +- Included the changes from version [`9.23.0`](https://pub.dev/packages/stream_chat/changelog). + ## 9.23.0 - Minor bug fixes and improvements +## 10.0.0-beta.11 + +- Included the changes from version [`9.22.0`](https://pub.dev/packages/stream_chat/changelog). + ## 9.22.0 ✅ Added @@ -13,6 +27,10 @@ specify a timestamp before which channel history should be hidden for newly added members. When provided, it takes precedence over the `hideHistory` boolean flag. +## 10.0.0-beta.10 + +- Included the changes from version [`9.21.0`](https://pub.dev/packages/stream_chat/changelog). + ## 9.21.0 🐞 Fixed @@ -20,6 +38,15 @@ - Fixed user's ID from being inadvertently used as their display name during the WebSocket connection process. [[#2447]](https://github.com/GetStream/stream-chat-flutter/issues/2447) +## 10.0.0-beta.9 + +🐞 Fixed + +- Fixed `Location.endAt` field not being properly converted to UTC, causing "expected date" API + errors when sending location messages. + +- Included the changes from version [`9.20.0`](https://pub.dev/packages/stream_chat/changelog). + ## 9.20.0 ✅ Added @@ -44,10 +71,47 @@ - `markRead`, `markUnread`, `markThreadRead`, and `markThreadUnread` methods now throw `StreamChatError` when channel lacks required capabilities. +## 10.0.0-beta.8 + +✅ Added + +- Added support for `user.messages.deleted` event. + +- Included the changes from version [`9.19.0`](https://pub.dev/packages/stream_chat/changelog). + ## 9.19.0 - Minor bug fixes and improvements +## 10.0.0-beta.7 + +🛑️ Breaking + +- **Changed `MessageState` factory constructors**: The `deleting`, `deleted`, and `deletingFailed` + factory constructors now accept a `MessageDeleteScope` parameter instead of `bool hard`. + Pattern matching callbacks also receive `MessageDeleteScope scope` instead of `bool hard`. +- **Added new abstract methods to `AttachmentFileUploader`**: The `AttachmentFileUploader` interface + now includes four new abstract methods (`uploadImage`, `uploadFile`, `removeImage`, `removeFile`). + Custom implementations must implement these methods. + +For more details, please refer to the [migration guide](../../migrations/v10-migration.md). + +✅ Added + +- Added support for deleting messages only for the current user: + - `Channel.deleteMessageForMe()` - Delete a message only for the current user + - `StreamChatClient.deleteMessageForMe()` - Delete a message only for the current user via client + - `MessageDeleteScope` - New sealed class to represent deletion scope + - `MessageState.deletingForMe`, `MessageState.deletedForMe`, `MessageState.deletingForMeFailed` states + - `Message.deletedOnlyForMe`, `Event.deletedForMe`, `Member.deletedMessages` model fields +- Added standalone file and image upload/removal methods for CDN operations: + - `StreamChatClient.uploadImage()` - Upload an image to the Stream CDN + - `StreamChatClient.uploadFile()` - Upload a file to the Stream CDN + - `StreamChatClient.removeImage()` - Remove an image from the Stream CDN + - `StreamChatClient.removeFile()` - Remove a file from the Stream CDN + +- Included the changes from version [`9.18.0`](https://pub.dev/packages/stream_chat/changelog). + ## 9.18.0 🐞 Fixed @@ -68,6 +132,10 @@ - Fixed `ChannelState.memberCount`, `ChannelState.config` and `ChannelState.extraData` getting reset on first load. +## 10.0.0-beta.6 + +- Included the changes from version [`9.17.0`](https://pub.dev/packages/stream_chat/changelog). + ## 9.17.0 🐞 Fixed @@ -77,10 +145,15 @@ during upload. - Fixed `toDraftMessage` to only include successfully uploaded attachments in draft messages. +## 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 @@ -92,6 +165,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 @@ -106,6 +198,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 @@ -124,10 +228,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/lib/main.dart b/packages/stream_chat/example/lib/main.dart index 0744726424..f85f4d99b3 100644 --- a/packages/stream_chat/example/lib/main.dart +++ b/packages/stream_chat/example/lib/main.dart @@ -17,8 +17,7 @@ Future main() async { User( id: 'cool-shadow-7', name: 'Cool Shadow', - image: - 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', + image: 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', ), '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''', ); @@ -60,9 +59,9 @@ class StreamExample extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( - title: 'Stream Chat Dart Example', - home: HomeScreen(channel: channel), - ); + title: 'Stream Chat Dart Example', + home: HomeScreen(channel: channel), + ); } /// Main screen of our application. The layout is comprised of an [AppBar] @@ -87,30 +86,31 @@ class HomeScreen extends StatelessWidget { body: SafeArea( child: StreamBuilder?>( stream: messages, - builder: ( - BuildContext context, - AsyncSnapshot?> snapshot, - ) { - if (snapshot.hasData && snapshot.data != null) { - return MessageView( - messages: snapshot.data!.reversed.toList(), - channel: channel, - ); - } else if (snapshot.hasError) { - return const Center( - child: Text( - 'There was an error loading messages. Please see logs.', - ), - ); - } - return const Center( - child: SizedBox( - width: 100, - height: 100, - child: CircularProgressIndicator(), - ), - ); - }, + builder: + ( + BuildContext context, + AsyncSnapshot?> snapshot, + ) { + if (snapshot.hasData && snapshot.data != null) { + return MessageView( + messages: snapshot.data!.reversed.toList(), + channel: channel, + ); + } else if (snapshot.hasError) { + return const Center( + child: Text( + 'There was an error loading messages. Please see logs.', + ), + ); + } + return const Center( + child: SizedBox( + width: 100, + height: 100, + child: CircularProgressIndicator(), + ), + ); + }, ), ), ); @@ -168,80 +168,80 @@ class _MessageViewState extends State { @override Widget build(BuildContext context) => Column( - children: [ - Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: _messages.length, - reverse: true, - itemBuilder: (BuildContext context, int index) { - final item = _messages[index]; - if (item.user?.id == widget.channel.client.uid) { - return Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(item.text ?? ''), - ), - ); - } else { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(item.text ?? ''), - ), - ); - } - }, - ), - ), - Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _controller, - decoration: const InputDecoration( - hintText: 'Enter your message', - ), - ), + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: _messages.length, + reverse: true, + itemBuilder: (BuildContext context, int index) { + final item = _messages[index]; + if (item.user?.id == widget.channel.client.uid) { + return Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(item.text ?? ''), + ), + ); + } else { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(item.text ?? ''), + ), + ); + } + }, + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + decoration: const InputDecoration( + hintText: 'Enter your message', ), - Material( - type: MaterialType.circle, - color: Colors.blue, - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: () async { - // We can send a new message by calling `sendMessage` on - // the current channel. After sending a message, the - // TextField is cleared and the list view is scrolled - // to show the new item. - if (_controller.value.text.isNotEmpty) { - await widget.channel.sendMessage( - Message(text: _controller.value.text), - ); - _controller.clear(); - _updateList(); - } - }, - child: const Padding( - padding: EdgeInsets.all(8), - child: Center( - child: Icon( - Icons.send, - color: Colors.white, - ), - ), + ), + ), + Material( + type: MaterialType.circle, + color: Colors.blue, + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () async { + // We can send a new message by calling `sendMessage` on + // the current channel. After sending a message, the + // TextField is cleared and the list view is scrolled + // to show the new item. + if (_controller.value.text.isNotEmpty) { + await widget.channel.sendMessage( + Message(text: _controller.value.text), + ); + _controller.clear(); + _updateList(); + } + }, + child: const Padding( + padding: EdgeInsets.all(8), + child: Center( + child: Icon( + Icons.send, + color: Colors.white, ), ), ), - ], + ), ), - ), - ], - ); + ], + ), + ), + ], + ); } /// Helper extension for quickly retrieving diff --git a/packages/stream_chat/example/pubspec.yaml b/packages/stream_chat/example/pubspec.yaml index 6f50b1864b..02420b70c3 100644 --- a/packages/stream_chat/example/pubspec.yaml +++ b/packages/stream_chat/example/pubspec.yaml @@ -17,14 +17,14 @@ version: 1.0.0+1 # 2. Add it to the melos.yaml file for future updates. environment: - sdk: ^3.6.2 - flutter: ">=3.27.4" + sdk: ^3.10.0 + flutter: ">=3.38.1" dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat: ^9.23.0 + stream_chat: ^10.0.0-beta.13 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 9c30799e60..f63116877c 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -75,25 +75,25 @@ class Channel { String? name, String? image, Map? extraData, - }) : _cid = _id != null ? '$_type:$_id' : null, - _extraData = { - ...?extraData, - if (name != null) 'name': name, - if (image != null) 'image': image, - } { + }) : _cid = _id != null ? '$_type:$_id' : null, + _extraData = { + ...?extraData, + if (name != null) 'name': name, + if (image != null) 'image': image, + } { _client.logger.info('New Channel instance created, not yet initialized'); } /// Create a channel client instance from a [ChannelState] object. Channel.fromState(this._client, ChannelState channelState) - : assert( - channelState.channel != null, - 'No channel found inside channel state', - ), - _id = channelState.channel!.id, - _type = channelState.channel!.type, - _cid = channelState.channel!.cid, - _extraData = channelState.channel!.extraData { + : assert( + channelState.channel != null, + 'No channel found inside channel state', + ), + _id = channelState.channel!.id, + _type = channelState.channel!.type, + _cid = channelState.channel!.cid, + _extraData = channelState.channel!.extraData { _initState(channelState); // Initialize the state immediately. } @@ -144,16 +144,11 @@ class Channel { } /// Returns true if the channel is muted. - bool get isMuted => - _client.state.currentUser?.channelMutes - .any((element) => element.channel.cid == cid) == - true; + bool get isMuted => _client.state.currentUser?.channelMutes.any((element) => element.channel.cid == cid) == true; /// Returns true if the channel is muted, as a stream. Stream get isMutedStream => _client.state.currentUserStream - .map((event) => - event?.channelMutes.any((element) => element.channel.cid == cid) == - true) + .map((event) => event?.channelMutes.any((element) => element.channel.cid == cid) == true) .distinct(); /// True if the channel is a group. @@ -304,15 +299,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(); @@ -465,15 +451,12 @@ class Channel { } /// List of user permissions on this channel - List get ownCapabilities => - state?._channelState.channel?.ownCapabilities ?? []; + List get ownCapabilities => state?._channelState.channel?.ownCapabilities ?? []; /// List of user permissions on this channel Stream> get ownCapabilitiesStream { _checkInitialized(); - return state!.channelStateStream - .map((cs) => cs.channel?.ownCapabilities ?? []) - .distinct(); + return state!.channelStateStream.map((cs) => cs.channel?.ownCapabilities ?? []).distinct(); } /// Channel extra data as a stream. @@ -602,80 +585,85 @@ class Channel { } } - return Future.wait(attachments.map((it) { - client.logger.info('Uploading ${it.id} attachment...'); - - final throttledUpdateAttachment = updateAttachment.throttled( - const Duration(milliseconds: 500), - ); - - void onSendProgress(int sent, int total) { - throttledUpdateAttachment([ - it.copyWith( - uploadState: UploadState.inProgress(uploaded: sent, total: total), - ), - ]); - } + return Future.wait( + attachments.map((it) { + client.logger.info('Uploading ${it.id} attachment...'); - final isImage = it.type == AttachmentType.image; - final cancelToken = CancelToken(); - Future future; - if (isImage) { - future = sendImage( - it.file!, - onSendProgress: onSendProgress, - cancelToken: cancelToken, - extraData: it.extraData, - ); - } else { - future = sendFile( - it.file!, - onSendProgress: onSendProgress, - cancelToken: cancelToken, - extraData: it.extraData, + final throttledUpdateAttachment = updateAttachment.throttled( + const Duration(milliseconds: 500), ); - } - _cancelableAttachmentUploadRequest[it.id] = cancelToken; - return future.then((response) { - client.logger.info('Attachment ${it.id} uploaded successfully...'); - - // If the response is SendFileResponse, then we might also be getting - // thumbUrl in case of video. So we need to update the attachment with - // both the assetUrl and thumbUrl. - if (response is SendFileResponse) { - updateAttachment( + + void onSendProgress(int sent, int total) { + throttledUpdateAttachment([ it.copyWith( - assetUrl: response.file, - thumbUrl: response.thumbUrl, - uploadState: const UploadState.success(), + uploadState: UploadState.inProgress(uploaded: sent, total: total), ), + ]); + } + + final isImage = it.type == AttachmentType.image; + final cancelToken = CancelToken(); + Future future; + if (isImage) { + future = sendImage( + it.file!, + onSendProgress: onSendProgress, + cancelToken: cancelToken, + extraData: it.extraData, ); } else { - updateAttachment( - it.copyWith( - imageUrl: response.file, - uploadState: const UploadState.success(), - ), + future = sendFile( + it.file!, + onSendProgress: onSendProgress, + cancelToken: cancelToken, + extraData: it.extraData, ); } - }).catchError((e, stk) { - if (e is StreamChatNetworkError && e.isRequestCancelledError) { - client.logger.info('Attachment ${it.id} upload cancelled'); - - // remove attachment from message if cancelled. - updateAttachment(it, remove: true); - return; - } + _cancelableAttachmentUploadRequest[it.id] = cancelToken; + return future + .then((response) { + client.logger.info('Attachment ${it.id} uploaded successfully...'); + + // If the response is SendFileResponse, then we might also be getting + // thumbUrl in case of video. So we need to update the attachment with + // both the assetUrl and thumbUrl. + if (response is SendFileResponse) { + updateAttachment( + it.copyWith( + assetUrl: response.file, + thumbUrl: response.thumbUrl, + uploadState: const UploadState.success(), + ), + ); + } else { + updateAttachment( + it.copyWith( + imageUrl: response.file, + uploadState: const UploadState.success(), + ), + ); + } + }) + .catchError((e, stk) { + if (e is StreamChatNetworkError && e.isRequestCancelledError) { + client.logger.info('Attachment ${it.id} upload cancelled'); + + // remove attachment from message if cancelled. + updateAttachment(it, remove: true); + return; + } - client.logger.severe('error uploading the attachment', e, stk); - updateAttachment( - it.copyWith(uploadState: UploadState.failed(error: e.toString())), - ); - }).whenComplete(() { - throttledUpdateAttachment.cancel(); - _cancelableAttachmentUploadRequest.remove(it.id); - }); - })).whenComplete(() { + client.logger.severe('error uploading the attachment', e, stk); + updateAttachment( + it.copyWith(uploadState: UploadState.failed(error: e.toString())), + ); + }) + .whenComplete(() { + throttledUpdateAttachment.cancel(); + _cancelableAttachmentUploadRequest.remove(it.id); + }); + }), + ).whenComplete(() { if (message!.attachments.every((it) => it.uploadState.isSuccess)) { _messageAttachmentsUploadCompleter.remove(messageId)?.complete(message); } @@ -698,13 +686,11 @@ 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. - _messageAttachmentsUploadCompleter - .remove(message.id) - ?.completeError(const StreamChatError('Message cancelled')); + _messageAttachmentsUploadCompleter.remove(message.id)?.completeError(const StreamChatError('Message cancelled')); final quotedMessage = state!.messages.firstWhereOrNull( (m) => m.id == message.quotedMessageId, @@ -723,13 +709,12 @@ class Channel { ).toList(), ); - state!.updateMessage(message); + state?.updateMessage(message); try { if (message.attachments.any((it) => !it.uploadState.isSuccess)) { final attachmentsUploadCompleter = Completer(); - _messageAttachmentsUploadCompleter[message.id] = - attachmentsUploadCompleter; + _messageAttachmentsUploadCompleter[message.id] = attachmentsUploadCompleter; _uploadAttachments( message.id, @@ -761,22 +746,29 @@ class Channel { ), ); - final sentMessage = response.message.syncWith(message).copyWith( + final sentMessage = response.message + .syncWith(message) + .copyWith( // Update the message state to sent. 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; @@ -795,13 +787,10 @@ 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. - _messageAttachmentsUploadCompleter - .remove(message.id) - ?.completeError(const StreamChatError('Message cancelled')); + _messageAttachmentsUploadCompleter.remove(message.id)?.completeError(const StreamChatError('Message cancelled')); // ignore: parameter_assignments message = message.copyWith( @@ -820,8 +809,7 @@ class Channel { try { if (message.attachments.any((it) => !it.uploadState.isSuccess)) { final attachmentsUploadCompleter = Completer(); - _messageAttachmentsUploadCompleter[message.id] = - attachmentsUploadCompleter; + _messageAttachmentsUploadCompleter[message.id] = attachmentsUploadCompleter; _uploadAttachments( message.id, @@ -842,7 +830,9 @@ class Channel { ), ); - final updateMessage = response.message.syncWith(message).copyWith( + final updateMessage = response.message + .syncWith(message) + .copyWith( // Update the message state to updated. state: MessageState.updated, ownReactions: message.ownReactions, @@ -852,22 +842,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; } } @@ -884,13 +872,10 @@ 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. - _messageAttachmentsUploadCompleter - .remove(message.id) - ?.completeError(const StreamChatError('Message cancelled')); + _messageAttachmentsUploadCompleter.remove(message.id)?.completeError(const StreamChatError('Message cancelled')); // ignore: parameter_assignments message = message.copyWith( @@ -912,7 +897,9 @@ class Channel { ), ); - final updatedMessage = response.message.syncWith(message).copyWith( + final updatedMessage = response.message + .syncWith(message) + .copyWith( // Update the message state to updated. state: MessageState.updated, ownReactions: message.ownReactions, @@ -922,21 +909,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; @@ -945,31 +930,49 @@ class Channel { final _deleteMessageLock = Lock(); - /// Deletes the [message] from the channel. - Future deleteMessage( + /// Deletes the [message] for everyone. + /// + /// If [hard] is true, the message is permanently deleted from the server + /// and cannot be recovered. In this case, any attachments associated with the + /// message are also deleted from the server. + Future deleteMessage(Message message, {bool hard = false}) { + final deletionScope = MessageDeleteScope.deleteForAll(hard: hard); + + return _deleteMessage(message, scope: deletionScope); + } + + /// Deletes the [message] only for the current user. + /// + /// Note: This does not delete the message for other channel members and + /// they can still see the message. + Future deleteMessageForMe(Message message) { + const deletionScope = MessageDeleteScope.deleteForMe(); + + return _deleteMessage(message, scope: deletionScope); + } + + // Deletes the [message] from the channel. + // + // The [scope] defines whether to delete the message for everyone or just + // for the current user. + // + // If the message is a local message (not yet sent to the server) or a bounced + // error message, it is deleted locally without making an API call. + // + // If the message is deleted for everyone and [scope.hard] is true, the + // message is permanently deleted from the server and cannot be recovered. + // In this case, any attachments associated with the message are also deleted + // from the server. + Future _deleteMessage( Message message, { - bool hard = false, + required MessageDeleteScope scope, }) async { _checkInitialized(); // 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( - message.copyWith( - type: MessageType.deleted, - localDeletedAt: DateTime.now(), - state: MessageState.deleted(hard: hard), - ), - hardDelete: hard, - ); - - // Removing the attachments upload completer to stop the `sendMessage` - // waiting for attachments to complete. - _messageAttachmentsUploadCompleter - .remove(message.id) - ?.completeError(const StreamChatError('Message deleted')); - + _deleteLocalMessage(message); // Returning empty response to mark the api call as success. return EmptyResponse(); } @@ -978,64 +981,139 @@ class Channel { message = message.copyWith( type: MessageType.deleted, deletedAt: DateTime.now(), - state: MessageState.deleting(hard: hard), + deletedForMe: scope is DeleteForMe, + state: MessageState.deleting(scope: scope), ); - state?.deleteMessage(message, hardDelete: hard); + state?.deleteMessage(message, hardDelete: scope.hard); try { // Wait for the previous delete call to finish. Otherwise, the order of // messages will not be maintained. final response = await _deleteMessageLock.synchronized( - () => _client.deleteMessage(message.id, hard: hard), + () => switch (scope) { + DeleteForMe() => _client.deleteMessageForMe(message.id), + DeleteForAll() => _client.deleteMessage(message.id, hard: scope.hard), + }, ); final deletedMessage = message.copyWith( - state: MessageState.deleted(hard: hard), + deletedForMe: scope is DeleteForMe, + state: MessageState.deleted(scope: scope), ); - state?.deleteMessage(deletedMessage, hardDelete: hard); - - if (hard) { - deletedMessage.attachments.forEach((attachment) { - if (attachment.uploadState.isSuccess) { - if (attachment.type == AttachmentType.image) { - deleteImage(attachment.imageUrl!); - } else if (attachment.type == AttachmentType.file) { - deleteFile(attachment.assetUrl!); - } - } - }); - } + state?.deleteMessage(deletedMessage, hardDelete: scope.hard); + // If hard delete, also delete the attachments from the server. + if (scope.hard) _deleteMessageAttachments(deletedMessage); return response; } catch (e) { + final failedMessage = message.copyWith( + // Update the message state to failed. + state: MessageState.deletingFailed(scope: scope), + ); + + state?.deleteMessage(failedMessage, hardDelete: scope.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. + // Deletes a local [message] that is not yet sent to the server. + // + // This is typically called when a user wants to delete a message that they + // have composed but not yet sent, or if a message failed to send and the user + // wants to remove it from their local view. + void _deleteLocalMessage(Message message) { + state?.deleteMessage( + hardDelete: true, // Local messages are always hard deleted. + message.copyWith( + type: MessageType.deleted, + localDeletedAt: DateTime.now(), + state: MessageState.hardDeleted, + ), + ); + + // Removing the attachments upload completer to stop the `sendMessage` + // waiting for attachments to complete. + final completer = _messageAttachmentsUploadCompleter.remove(message.id); + completer?.completeError(const StreamChatError('Message deleted')); + } + + // Deletes all the attachments associated with the given [message] + // from the server. This is typically called when a message is hard deleted. + Future _deleteMessageAttachments(Message message) async { + final attachments = message.attachments; + final deleteFutures = attachments.map((it) async { + if (it.imageUrl case final url?) return deleteImage(url); + if (it.assetUrl case final url?) return deleteFile(url); + }); + + try { + await Future.wait(deleteFutures); + } catch (e, stk) { + _client.logger.warning('Error deleting message attachments', e, stk); + } + } + + /// Retries operations on a message based on its failed state. + /// + /// 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 + /// again, using the same scope (for me or for all) as the original request. + /// - For messages with [isBouncedWithError], it attempts to send the message. /// - /// For example, if the message failed to send, it will retry sending the - /// message and vice-versa. + /// Throws a [StateError] if the message is not in a failed state or + /// bounced with an error. 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), - deletingFailed: (hard) => deleteMessage(message, hard: hard), + 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: (scope) => switch (scope) { + DeleteForMe() => deleteMessageForMe(message), + DeleteForAll(hard: final 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', + ); + }, ); } @@ -1045,9 +1123,7 @@ class Channel { Object? /*num|DateTime*/ timeoutOrExpirationDate, }) { assert(() { - if (timeoutOrExpirationDate is! DateTime && - timeoutOrExpirationDate != null && - timeoutOrExpirationDate is! num) { + if (timeoutOrExpirationDate is! DateTime && timeoutOrExpirationDate != null && timeoutOrExpirationDate is! num) { throw ArgumentError('Invalid timeout or Expiration date'); } return true; @@ -1071,13 +1147,12 @@ class Channel { } /// Unpins provided message. - Future unpinMessage(Message message) => - partialUpdateMessage( - message, - set: { - 'pinned': false, - }, - ); + Future unpinMessage(Message message) => partialUpdateMessage( + message, + set: { + 'pinned': false, + }, + ); /// Creates or updates a new [draft] for this channel. Future createDraft( @@ -1108,6 +1183,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, { @@ -1206,7 +1347,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)); @@ -1370,28 +1511,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( @@ -1404,9 +1534,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; @@ -1422,6 +1551,8 @@ class Channel { Message message, Reaction reaction, ) async { + _checkInitialized(); + final updatedMessage = message.deleteMyReaction( reactionType: reaction.type, ); @@ -1466,8 +1597,7 @@ class Channel { /// ```dart /// channel.updateName('Updated channel name'); /// ``` - Future updateName(String name) => - updatePartial(set: {'name': name}); + Future updateName(String name) => updatePartial(set: {'name': name}); /// Update the channel's [image]. /// @@ -1484,8 +1614,7 @@ class Channel { /// ```dart /// channel.updateImage('https://getstream.io/new-image'); /// ``` - Future updateImage(String image) => - updatePartial(set: {'image': image}); + Future updateImage(String image) => updatePartial(set: {'image': image}); /// Update the channel custom data. This replaces all of the channel data /// with the given [channelData]. @@ -1790,11 +1919,10 @@ class Channel { Future getReactions( String messageId, { PaginationParams? pagination, - }) => - _client.getReactions( - messageId, - pagination: pagination, - ); + }) => _client.getReactions( + messageId, + pagination: pagination, + ); /// Retrieves a list of messages by given [messageIDs]. Future getMessagesById( @@ -1811,11 +1939,10 @@ class Channel { Future translateMessage( String messageId, String language, - ) => - _client.translateMessage( - messageId, - language, - ); + ) => _client.translateMessage( + messageId, + language, + ); /// Creates a new channel. Future create() => query(state: false); @@ -1922,15 +2049,14 @@ class Channel { Filter? filter, SortOrder? sort, PaginationParams? pagination, - }) => - _client.queryMembers( - type, - channelId: id, - filter: filter, - members: state?.members, - sort: sort, - pagination: pagination, - ); + }) => _client.queryMembers( + type, + channelId: id, + filter: filter, + members: state?.members, + sort: sort, + pagination: pagination, + ); /// Query channel banned users. Future queryBannedUsers({ @@ -2098,15 +2224,14 @@ class Channel { String? eventType2, String? eventType3, String? eventType4, - ]) => - _client - .on( - eventType, - eventType2, - eventType3, - eventType4, - ) - .where((e) => e.cid == cid); + ]) => _client + .on( + eventType, + eventType2, + eventType3, + eventType4, + ) + .where((e) => e.cid == cid); late final _keyStrokeHandler = KeyStrokeHandler( onStartTyping: startTyping, @@ -2138,10 +2263,12 @@ class Channel { if (!_canSendTypingEvents) return; client.logger.info('start typing'); - await sendEvent(Event( - type: EventType.typingStart, - parentId: parentId, - )); + await sendEvent( + Event( + type: EventType.typingStart, + parentId: parentId, + ), + ); } /// Sends the [EventType.typingStop] event. @@ -2149,10 +2276,12 @@ class Channel { if (!_canSendTypingEvents) return; client.logger.info('stop typing'); - await sendEvent(Event( - type: EventType.typingStop, - parentId: parentId, - )); + await sendEvent( + Event( + type: EventType.typingStop, + parentId: parentId, + ), + ); } /// Call this method to dispose the channel client. @@ -2195,87 +2324,90 @@ class ChannelClientState { _checkExpiredAttachmentMessages(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(); - _listenChannelMessageCount(); + // endregion + // region MEMBER EVENTS _listenMemberAdded(); - _listenMemberRemoved(); - _listenMemberUpdated(); - _listenMemberBanned(); - _listenMemberUnbanned(); + _listenUserMessagesDeleted(); + // 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(); + _listenChannelPushPreferenceUpdated(); final persistenceClient = _client.chatPersistenceClient; - persistenceClient?.getChannelThreads(_channel.cid!).then((threads) { - // Load all the threads for the channel from the offline storage. - if (threads.isNotEmpty) _threads = threads; - }).then((_) => retryFailedMessages()); + persistenceClient + ?.getChannelThreads(_channel.cid!) + .then((threads) { + // Load all the threads for the channel from the offline storage. + if (threads.isNotEmpty) _threads = threads; + }) + .then((_) => retryFailedMessages()); } final Channel _channel; @@ -2284,35 +2416,34 @@ class ChannelClientState { void _checkExpiredAttachmentMessages(ChannelState channelState) async { final expiredAttachmentMessagesId = channelState.messages - ?.where((m) => - !_updatedMessagesIds.contains(m.id) && - m.attachments.isNotEmpty && - m.attachments.any((e) { - final url = e.imageUrl ?? e.assetUrl; - if (url == null || !url.contains('')) { - return false; - } - try { - final uri = Uri.parse(url); - if (!uri.host.endsWith('stream-io-cdn.com') || - uri.queryParameters['Expires'] == null) { + ?.where( + (m) => + !_updatedMessagesIds.contains(m.id) && + m.attachments.isNotEmpty && + m.attachments.any((e) { + final url = e.imageUrl ?? e.assetUrl; + if (url == null || !url.contains('')) { return false; } - final secondsFromEpoch = - int.parse(uri.queryParameters['Expires']!); - final expiration = DateTime.fromMillisecondsSinceEpoch( - secondsFromEpoch * 1000, - ); - return expiration.isBefore(DateTime.now()); - } catch (_) { - return false; - } - })) + try { + final uri = Uri.parse(url); + if (!uri.host.endsWith('stream-io-cdn.com') || uri.queryParameters['Expires'] == null) { + return false; + } + final secondsFromEpoch = int.parse(uri.queryParameters['Expires']!); + final expiration = DateTime.fromMillisecondsSinceEpoch( + secondsFromEpoch * 1000, + ); + return expiration.isBefore(DateTime.now()); + } catch (_) { + return false; + } + }), + ) .map((e) => e.id) .toList(); - if (expiredAttachmentMessagesId != null && - expiredAttachmentMessagesId.isNotEmpty) { + if (expiredAttachmentMessagesId != null && expiredAttachmentMessagesId.isNotEmpty) { await _channel.initialized; _updatedMessagesIds.addAll(expiredAttachmentMessagesId); _channel.getMessagesById(expiredAttachmentMessagesId); @@ -2320,141 +2451,156 @@ class ChannelClientState { } void _listenMemberAdded() { - _subscriptions.add(_channel.on(EventType.memberAdded).listen((Event e) { - final member = e.member!; - final existingMembers = channelState.members ?? []; + _subscriptions.add( + _channel.on(EventType.memberAdded).listen((Event e) { + final member = e.member!; + final existingMembers = channelState.members ?? []; - updateChannelState( - channelState.copyWith( - members: [...existingMembers, member], - ), - ); - })); + updateChannelState( + channelState.copyWith( + members: [...existingMembers, member], + ), + ); + }), + ); } void _listenMemberRemoved() { - _subscriptions.add(_channel.on(EventType.memberRemoved).listen((Event e) { - final user = e.user!; - final existingRead = channelState.read ?? []; - final existingMembers = channelState.members ?? []; - - updateChannelState( - channelState.copyWith( - read: [...existingRead.where((r) => r.user.id != user.id)], - members: [...existingMembers.where((m) => m.userId != user.id)], - ), - ); - })); + _subscriptions.add( + _channel.on(EventType.memberRemoved).listen((Event e) { + final user = e.user!; + final existingRead = channelState.read ?? []; + final existingMembers = channelState.members ?? []; + + updateChannelState( + channelState.copyWith( + read: [...existingRead.where((r) => r.user.id != user.id)], + members: [...existingMembers.where((m) => m.userId != user.id)], + ), + ); + }), + ); } void _listenMemberUpdated() { _subscriptions // Listen to events containing member users - ..add(_channel.on().listen( - (event) { - final user = event.user; - if (user == null) return; + ..add( + _channel.on().listen( + (event) { + final user = event.user; + if (user == null) return; - final existingMembers = [...?channelState.members]; - final existingMembership = channelState.membership; + final existingMembers = [...?channelState.members]; + final existingMembership = channelState.membership; - // Return if the user is not a existing member of the channel. - if (!existingMembers.any((m) => m.userId == user.id)) return; + // Return if the user is not a existing member of the channel. + if (!existingMembers.any((m) => m.userId == user.id)) return; - Member? maybeUpdateMemberUser(Member? existingMember) { - if (existingMember == null) return null; - if (existingMember.userId == user.id) { - return existingMember.copyWith(user: user); + Member? maybeUpdateMemberUser(Member? existingMember) { + if (existingMember == null) return null; + if (existingMember.userId == user.id) { + return existingMember.copyWith(user: user); + } + return existingMember; } - return existingMember; - } - - updateChannelState( - channelState.copyWith( - membership: maybeUpdateMemberUser(existingMembership), - members: [...existingMembers.map(maybeUpdateMemberUser).nonNulls], - ), - ); - }, - )) + updateChannelState( + channelState.copyWith( + membership: maybeUpdateMemberUser(existingMembership), + members: [...existingMembers.map(maybeUpdateMemberUser).nonNulls], + ), + ); + }, + ), + ) // Listen to member updated events. - ..add(_channel.on(EventType.memberUpdated).listen( - (Event e) { - final member = e.member!; - final existingMembers = channelState.members ?? []; - final existingMembership = channelState.membership; - - Member? maybeUpdateMember(Member? existingMember) { - if (existingMember == null) return null; - if (existingMember.userId == member.userId) return member; - return existingMember; - } + ..add( + _channel.on(EventType.memberUpdated).listen( + (Event e) { + final member = e.member!; + final existingMembers = channelState.members ?? []; + final existingMembership = channelState.membership; + + Member? maybeUpdateMember(Member? existingMember) { + if (existingMember == null) return null; + if (existingMember.userId == member.userId) return member; + return existingMember; + } - updateChannelState( - channelState.copyWith( - membership: maybeUpdateMember(existingMembership), - members: [...existingMembers.map(maybeUpdateMember).nonNulls], - ), - ); - }, - )); + updateChannelState( + channelState.copyWith( + membership: maybeUpdateMember(existingMembership), + members: [...existingMembers.map(maybeUpdateMember).nonNulls], + ), + ); + }, + ), + ); } void _listenChannelUpdated() { - _subscriptions.add(_channel.on(EventType.channelUpdated).listen((Event e) { - final channel = e.channel!; - updateChannelState(channelState.copyWith( - channel: channelState.channel?.merge(channel), - members: channel.members, - )); - })); - } - - void _listenChannelMessageCount() { - _subscriptions.add(_channel.on().listen( - (Event e) { - final messageCount = e.channelMessageCount; - if (messageCount == null) return; - + _subscriptions.add( + _channel.on(EventType.channelUpdated).listen((Event e) { + final channel = e.channel!; updateChannelState( channelState.copyWith( - channel: channelState.channel?.copyWith( - messageCount: messageCount, - ), + channel: channelState.channel?.merge(channel), + members: channel.members, ), ); - }, - )); - } - - void _listenChannelTruncated() { - _subscriptions.add(_channel - .on(EventType.channelTruncated, EventType.notificationChannelTruncated) - .listen((event) async { - final channel = event.channel!; - await _client.chatPersistenceClient?.deleteMessageByCid(channel.cid); - truncate(); - if (event.message != null) { - updateMessage(event.message!); - } - })); + }), + ); + } + + void _listenChannelMessageCount() { + _subscriptions.add( + _channel.on().listen( + (Event e) { + final messageCount = e.channelMessageCount; + if (messageCount == null) return; + + updateChannelState( + channelState.copyWith( + channel: channelState.channel?.copyWith( + messageCount: messageCount, + ), + ), + ); + }, + ), + ); + } + + void _listenChannelTruncated() { + _subscriptions.add( + _channel.on(EventType.channelTruncated, EventType.notificationChannelTruncated).listen((event) async { + final channel = event.channel!; + await _client.chatPersistenceClient?.deleteMessageByCid(channel.cid); + truncate(); + if (event.message != null) { + updateMessage(event.message!); + } + }), + ); } void _listenMemberBanned() { - _subscriptions.add(_channel - .on(EventType.userBanned) - .where((it) => it.cid != null) // filters channel ban from app ban - .listen( - (event) async { - final user = event.user!; - final member = await _channel - .queryMembers(filter: Filter.equal('id', user.id)) - .then((it) => it.members.first); - - _updateMember(member); - }, - )); + _subscriptions.add( + _channel + .on(EventType.userBanned) + .where((it) => it.cid != null) // filters channel ban from app ban + .listen( + (event) async { + final user = event.user!; + final member = await _channel + .queryMembers(filter: Filter.equal('id', user.id)) + .then((it) => it.members.first); + + _updateMember(member); + }, + ), + ); } void _listenUserStartWatching() { @@ -2463,12 +2609,14 @@ class ChannelClientState { final watcher = event.user; if (watcher != null) { final existingWatchers = channelState.watchers; - updateChannelState(channelState.copyWith( - watchers: [ - watcher, - ...?existingWatchers?.where((user) => user.id != watcher.id), - ], - )); + updateChannelState( + channelState.copyWith( + watchers: [ + watcher, + ...?existingWatchers?.where((user) => user.id != watcher.id), + ], + ), + ); } }), ); @@ -2480,30 +2628,32 @@ class ChannelClientState { final watcher = event.user; if (watcher != null) { final existingWatchers = channelState.watchers; - updateChannelState(channelState.copyWith( - watchers: [ - ...?existingWatchers?.where((user) => user.id != watcher.id) - ], - )); + updateChannelState( + channelState.copyWith( + watchers: [...?existingWatchers?.where((user) => user.id != watcher.id)], + ), + ); } }), ); } void _listenMemberUnbanned() { - _subscriptions.add(_channel - .on(EventType.userUnbanned) - .where((it) => it.cid != null) // filters channel ban from app ban - .listen( - (event) async { - final user = event.user!; - final member = await _channel - .queryMembers(filter: Filter.equal('id', user.id)) - .then((it) => it.members.first); - - _updateMember(member); - }, - )); + _subscriptions.add( + _channel + .on(EventType.userUnbanned) + .where((it) => it.cid != null) // filters channel ban from app ban + .listen( + (event) async { + final user = event.user!; + final member = await _channel + .queryMembers(filter: Filter.equal('id', user.id)) + .then((it) => it.members.first); + + _updateMember(member); + }, + ), + ); } void _updateMember(Member member) { @@ -2541,8 +2691,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); } @@ -2557,185 +2709,206 @@ 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; - if (eventPoll == null) return; + _subscriptions.add( + _channel.on(EventType.pollUpdated).listen((event) { + final eventPoll = event.poll; + if (eventPoll == null) return; - final pollMessage = _findPollMessage(eventPoll.id); - if (pollMessage == null) return; + final pollMessage = _findPollMessage(eventPoll.id); + if (pollMessage == null) return; - final oldPoll = pollMessage.poll; + final oldPoll = pollMessage.poll; - final latestAnswers = oldPoll?.latestAnswers ?? eventPoll.latestAnswers; - final ownVotesAndAnswers = - oldPoll?.ownVotesAndAnswers ?? eventPoll.ownVotesAndAnswers; + final latestAnswers = oldPoll?.latestAnswers ?? eventPoll.latestAnswers; + final ownVotesAndAnswers = oldPoll?.ownVotesAndAnswers ?? eventPoll.ownVotesAndAnswers; - final poll = eventPoll.copyWith( - latestAnswers: latestAnswers, - ownVotesAndAnswers: ownVotesAndAnswers, - ); + final poll = eventPoll.copyWith( + latestAnswers: latestAnswers, + ownVotesAndAnswers: ownVotesAndAnswers, + ); - final message = pollMessage.copyWith(poll: poll); - updateMessage(message); - })); + final message = pollMessage.copyWith(poll: poll); + updateMessage(message); + }), + ); } void _listenPollClosed() { - _subscriptions.add(_channel.on(EventType.pollClosed).listen((event) { - final eventPoll = event.poll; - if (eventPoll == null) return; + _subscriptions.add( + _channel.on(EventType.pollClosed).listen((event) { + final eventPoll = event.poll; + if (eventPoll == null) return; - final pollMessage = _findPollMessage(eventPoll.id); - if (pollMessage == null) return; + final pollMessage = _findPollMessage(eventPoll.id); + if (pollMessage == null) return; - final oldPoll = pollMessage.poll; - final poll = oldPoll?.copyWith(isClosed: true) ?? eventPoll; + final oldPoll = pollMessage.poll; + final poll = oldPoll?.copyWith(isClosed: true) ?? eventPoll; - final message = pollMessage.copyWith(poll: poll); - updateMessage(message); - })); + final message = pollMessage.copyWith(poll: poll); + updateMessage(message); + }), + ); } void _listenPollAnswerCasted() { - _subscriptions.add(_channel.on(EventType.pollAnswerCasted).listen((event) { - final (eventPoll, eventPollVote) = (event.poll, event.pollVote); - if (eventPoll == null || eventPollVote == null) return; - - final pollMessage = _findPollMessage(eventPoll.id); - if (pollMessage == null) return; + _subscriptions.add( + _channel.on(EventType.pollAnswerCasted).listen((event) { + final (eventPoll, eventPollVote) = (event.poll, event.pollVote); + if (eventPoll == null || eventPollVote == null) return; - final oldPoll = pollMessage.poll; + final pollMessage = _findPollMessage(eventPoll.id); + if (pollMessage == null) return; - final latestAnswers = { - for (final ans in oldPoll?.latestAnswers ?? []) ans.id: ans, - eventPollVote.id!: eventPollVote, - }; + final oldPoll = pollMessage.poll; - final currentUserId = _client.state.currentUser?.id; - final ownVotesAndAnswers = { - for (final vote in oldPoll?.ownVotesAndAnswers ?? []) vote.id: vote, - if (eventPollVote.userId == currentUserId) + final latestAnswers = { + for (final ans in oldPoll?.latestAnswers ?? []) ans.id: ans, eventPollVote.id!: eventPollVote, - }; + }; - final poll = eventPoll.copyWith( - latestAnswers: [...latestAnswers.values], - ownVotesAndAnswers: [...ownVotesAndAnswers.values], - ); + final currentUserId = _client.state.currentUser?.id; + final ownVotesAndAnswers = { + for (final vote in oldPoll?.ownVotesAndAnswers ?? []) vote.id: vote, + if (eventPollVote.userId == currentUserId) eventPollVote.id!: eventPollVote, + }; + + final poll = eventPoll.copyWith( + latestAnswers: [...latestAnswers.values], + ownVotesAndAnswers: [...ownVotesAndAnswers.values], + ); - final message = pollMessage.copyWith(poll: poll); - updateMessage(message); - })); + final message = pollMessage.copyWith(poll: poll); + updateMessage(message); + }), + ); } void _listenPollVoteCasted() { - _subscriptions.add(_channel.on(EventType.pollVoteCasted).listen((event) { - final (eventPoll, eventPollVote) = (event.poll, event.pollVote); - if (eventPoll == null || eventPollVote == null) return; + _subscriptions.add( + _channel.on(EventType.pollVoteCasted).listen((event) { + final (eventPoll, eventPollVote) = (event.poll, event.pollVote); + if (eventPoll == null || eventPollVote == null) return; - final pollMessage = _findPollMessage(eventPoll.id); - if (pollMessage == null) return; + final pollMessage = _findPollMessage(eventPoll.id); + if (pollMessage == null) return; - final oldPoll = pollMessage.poll; + final oldPoll = pollMessage.poll; - final latestAnswers = oldPoll?.latestAnswers ?? eventPoll.latestAnswers; - final currentUserId = _client.state.currentUser?.id; - final ownVotesAndAnswers = { - for (final vote in oldPoll?.ownVotesAndAnswers ?? []) vote.id: vote, - if (eventPollVote.userId == currentUserId) - eventPollVote.id!: eventPollVote, - }; + final latestAnswers = oldPoll?.latestAnswers ?? eventPoll.latestAnswers; + final currentUserId = _client.state.currentUser?.id; + final ownVotesAndAnswers = { + for (final vote in oldPoll?.ownVotesAndAnswers ?? []) vote.id: vote, + if (eventPollVote.userId == currentUserId) eventPollVote.id!: eventPollVote, + }; - final poll = eventPoll.copyWith( - latestAnswers: latestAnswers, - ownVotesAndAnswers: [...ownVotesAndAnswers.values], - ); + final poll = eventPoll.copyWith( + latestAnswers: latestAnswers, + ownVotesAndAnswers: [...ownVotesAndAnswers.values], + ); - final message = pollMessage.copyWith(poll: poll); - updateMessage(message); - })); + final message = pollMessage.copyWith(poll: poll); + updateMessage(message); + }), + ); } void _listenPollAnswerRemoved() { - _subscriptions.add(_channel.on(EventType.pollAnswerRemoved).listen((event) { - final (eventPoll, eventPollVote) = (event.poll, event.pollVote); - if (eventPoll == null || eventPollVote == null) return; + _subscriptions.add( + _channel.on(EventType.pollAnswerRemoved).listen((event) { + final (eventPoll, eventPollVote) = (event.poll, event.pollVote); + if (eventPoll == null || eventPollVote == null) return; - final pollMessage = _findPollMessage(eventPoll.id); - if (pollMessage == null) return; + final pollMessage = _findPollMessage(eventPoll.id); + if (pollMessage == null) return; - final oldPoll = pollMessage.poll; + final oldPoll = pollMessage.poll; - final latestAnswers = { - for (final ans in oldPoll?.latestAnswers ?? []) ans.id: ans, - }..remove(eventPollVote.id); + final latestAnswers = { + for (final ans in oldPoll?.latestAnswers ?? []) ans.id: ans, + }..remove(eventPollVote.id); - final ownVotesAndAnswers = { - for (final vote in oldPoll?.ownVotesAndAnswers ?? []) vote.id: vote, - }..remove(eventPollVote.id); + final ownVotesAndAnswers = { + for (final vote in oldPoll?.ownVotesAndAnswers ?? []) vote.id: vote, + }..remove(eventPollVote.id); - final poll = eventPoll.copyWith( - latestAnswers: [...latestAnswers.values], - ownVotesAndAnswers: [...ownVotesAndAnswers.values], - ); + final poll = eventPoll.copyWith( + latestAnswers: [...latestAnswers.values], + ownVotesAndAnswers: [...ownVotesAndAnswers.values], + ); - final message = pollMessage.copyWith(poll: poll); - updateMessage(message); - })); + final message = pollMessage.copyWith(poll: poll); + updateMessage(message); + }), + ); } void _listenPollVoteRemoved() { - _subscriptions.add(_channel.on(EventType.pollVoteRemoved).listen((event) { - final (eventPoll, eventPollVote) = (event.poll, event.pollVote); - if (eventPoll == null || eventPollVote == null) return; + _subscriptions.add( + _channel.on(EventType.pollVoteRemoved).listen((event) { + final (eventPoll, eventPollVote) = (event.poll, event.pollVote); + if (eventPoll == null || eventPollVote == null) return; - final pollMessage = _findPollMessage(eventPoll.id); - if (pollMessage == null) return; + final pollMessage = _findPollMessage(eventPoll.id); + if (pollMessage == null) return; - final oldPoll = pollMessage.poll; + final oldPoll = pollMessage.poll; - final latestAnswers = oldPoll?.latestAnswers ?? eventPoll.latestAnswers; - final ownVotesAndAnswers = { - for (final vote in oldPoll?.ownVotesAndAnswers ?? []) vote.id: vote, - }..remove(eventPollVote.id); + final latestAnswers = oldPoll?.latestAnswers ?? eventPoll.latestAnswers; + final ownVotesAndAnswers = { + for (final vote in oldPoll?.ownVotesAndAnswers ?? []) vote.id: vote, + }..remove(eventPollVote.id); - final poll = eventPoll.copyWith( - latestAnswers: latestAnswers, - ownVotesAndAnswers: [...ownVotesAndAnswers.values], - ); + final poll = eventPoll.copyWith( + latestAnswers: latestAnswers, + ownVotesAndAnswers: [...ownVotesAndAnswers.values], + ); - final message = pollMessage.copyWith(poll: poll); - updateMessage(message); - })); + final message = pollMessage.copyWith(poll: poll); + updateMessage(message); + }), + ); } void _listenPollVoteChanged() { - _subscriptions.add(_channel.on(EventType.pollVoteChanged).listen((event) { - final (eventPoll, eventPollVote) = (event.poll, event.pollVote); - if (eventPoll == null || eventPollVote == null) return; + _subscriptions.add( + _channel.on(EventType.pollVoteChanged).listen((event) { + final (eventPoll, eventPollVote) = (event.poll, event.pollVote); + if (eventPoll == null || eventPollVote == null) return; - final pollMessage = _findPollMessage(eventPoll.id); - if (pollMessage == null) return; + final pollMessage = _findPollMessage(eventPoll.id); + if (pollMessage == null) return; - final oldPoll = pollMessage.poll; + final oldPoll = pollMessage.poll; - final latestAnswers = oldPoll?.latestAnswers ?? eventPoll.latestAnswers; - final currentUserId = _client.state.currentUser?.id; - final ownVotesAndAnswers = { - for (final vote in oldPoll?.ownVotesAndAnswers ?? []) vote.id: vote, - if (eventPollVote.userId == currentUserId) - eventPollVote.id!: eventPollVote, - }; + final latestAnswers = oldPoll?.latestAnswers ?? eventPoll.latestAnswers; + final currentUserId = _client.state.currentUser?.id; + final ownVotesAndAnswers = { + for (final vote in oldPoll?.ownVotesAndAnswers ?? []) vote.id: vote, + if (eventPollVote.userId == currentUserId) eventPollVote.id!: eventPollVote, + }; - final poll = eventPoll.copyWith( - latestAnswers: latestAnswers, - ownVotesAndAnswers: [...ownVotesAndAnswers.values], - ); + final poll = eventPoll.copyWith( + latestAnswers: latestAnswers, + ownVotesAndAnswers: [...ownVotesAndAnswers.values], + ); - final message = pollMessage.copyWith(poll: poll); - updateMessage(message); - })); + final message = pollMessage.copyWith(poll: poll); + updateMessage(message); + }), + ); } void _listenDraftUpdated() { @@ -2819,97 +2992,216 @@ 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); - })); - } - - void _listenReactions() { - _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); - })); + _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 _listenReactionNew() { + _subscriptions.add( + _channel.on(EventType.reactionNew).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.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); + }), + ); } void _listenMessageDeleted() { - _subscriptions.add(_channel.on(EventType.messageDeleted).listen((event) { - final message = event.message!; - final hardDelete = event.hardDelete ?? false; + _subscriptions.add( + _channel.on(EventType.messageDeleted).listen((event) { + final hardDelete = event.hardDelete ?? false; + + final message = event.message!.copyWith( + // TODO: Remove once deletedForMe is properly enriched on the backend. + deletedForMe: event.deletedForMe, + ); - deleteMessage(message, hardDelete: hardDelete); - })); + return deleteMessage(message, hardDelete: hardDelete); + }), + ); } void _listenMessageNew() { - _subscriptions.add(_channel - .on( - EventType.messageNew, - EventType.notificationMessageNew, - ) - .listen((event) { - final message = event.message; - if (message == null) return; - - final isThreadMessage = message.parentId != null; - final isNotShownInChannel = message.showInChannel != true; - final isThreadOnlyMessage = isThreadMessage && isNotShownInChannel; - - // Only add the message if the channel is upToDate or if the message is - // a thread-only message. - if (isUpToDate || isThreadOnlyMessage) { - updateMessage(message); - } + _subscriptions.add( + _channel + .on( + EventType.messageNew, + EventType.notificationMessageNew, + ) + .listen((event) { + final message = event.message; + if (message == null) return; - // Otherwise, check if we can count the message as unread. - if (MessageRules.canCountAsUnread(message, _channel)) { - unreadCount += 1; // Increment unread count - } + 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 isNotShownInChannel = message.showInChannel != true; + final isThreadOnlyMessage = isThreadMessage && isNotShownInChannel; + + // Only add the message if the channel is upToDate or if the message is + // a thread-only message. + if (isUpToDate || isThreadOnlyMessage) updateMessage(message); - _client.channelDeliveryReporter.submitForDelivery([_channel]); - })); + // Otherwise, check if we can count the message as unread. + if (MessageRules.canCountAsUnread(message, _channel)) { + unreadCount += 1; // Increment unread count + } + + _client.channelDeliveryReporter.submitForDelivery([_channel]); } /// Updates the [read] in the state if it exists. Adds it otherwise. @@ -2973,79 +3265,7 @@ class ChannelClientState { } /// Updates the [message] in the state if it exists. Adds it otherwise. - void updateMessage(Message message) { - // Determine if the message should be displayed in the channel view. - if (message.parentId == null || message.showInChannel == true) { - // Create a new list of messages to avoid modifying the original - // list directly. - var newMessages = [...messages]; - final oldIndex = newMessages.indexWhere((m) => m.id == message.id); - - if (oldIndex != -1) { - // If the message already exists, prepare it for update. - final oldMessage = newMessages[oldIndex]; - var updatedMessage = message.syncWith(oldMessage); - - // Preserve quotedMessage if the update doesn't include a new - // quotedMessage. - if (message.quotedMessageId != null && - message.quotedMessage == null && - oldMessage.quotedMessage != null) { - updatedMessage = updatedMessage.copyWith( - quotedMessage: oldMessage.quotedMessage, - ); - } - - // Update the message in the list. - newMessages[oldIndex] = updatedMessage; - - // Update quotedMessage references in all messages. - newMessages = newMessages.map((it) { - // Skip if the current message does not quote the updated message. - if (it.quotedMessageId != message.id) return it; - - // Update the quotedMessage only if the updatedMessage indicates - // deletion. - if (message.isDeleted) { - return it.copyWith( - quotedMessage: updatedMessage.copyWith( - type: message.type, - deletedAt: message.deletedAt, - ), - ); - } - return it; - }).toList(); - } else { - // If the message is new, add it to the list. - 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; - if (MessageRules.canUpdateChannelLastMessageAt(message, _channel)) { - lastMessageAt = [lastMessageAt, message.createdAt].max; - } - - // 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 (message.parentId case final parentId?) { - updateThreadInfo(parentId, [message]); - } - } + void updateMessage(Message message) => _updateMessages([message]); /// Cleans up all the stale error messages which requires no action. void cleanUpStaleErrorMessages() { @@ -3054,80 +3274,15 @@ class ChannelClientState { }); if (errorMessages.isEmpty) return; - return errorMessages.forEach(removeMessage); - } - - /// 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); - } - - return newPinnedMessages; + return _removeMessages(errorMessages); } /// Remove a [message] from this [channelState]. - void removeMessage(Message message) async { - await _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)], - ); - - _threads = newThreads; - - // Early return if the thread message is not shown in channel. - if (message.showInChannel == false) return; - } - - // Remove regular message, thread message shown in channel - var updatedMessages = [...messages]..removeWhere((e) => e.id == message.id); - - // 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, - ); - }).toList(); - - _channelState = _channelState.copyWith( - messages: updatedMessages, - ); - } + void removeMessage(Message message) => _removeMessages([message]); /// Removes/Updates the [message] based on the [hardDelete] value. void deleteMessage(Message message, {bool hardDelete = false}) { - if (hardDelete) return removeMessage(message); - return updateMessage(message); + return _deleteMessages([message], hardDelete: hardDelete); } void _listenReadEvents() { @@ -3219,27 +3374,22 @@ class ChannelClientState { List get messages => _channelState.messages ?? []; /// Channel message list as a stream. - Stream> get messagesStream => channelStateStream - .map((cs) => cs.messages ?? []) - .distinct(const ListEquality().equals); + Stream> get messagesStream => + channelStateStream.map((cs) => cs.messages ?? []).distinct(const ListEquality().equals); /// Channel pinned message list. - List get pinnedMessages => - _channelState.pinnedMessages ?? []; + List get pinnedMessages => _channelState.pinnedMessages ?? []; /// Channel pinned message list as a stream. - Stream> get pinnedMessagesStream => channelStateStream - .map((cs) => cs.pinnedMessages ?? []) - .distinct(const ListEquality().equals); + Stream> get pinnedMessagesStream => + channelStateStream.map((cs) => cs.pinnedMessages ?? []).distinct(const ListEquality().equals); /// Channel pending message list. - List get pendingMessages => - _channelState.pendingMessages ?? []; + List get pendingMessages => _channelState.pendingMessages ?? []; /// Channel pending message list as a stream. - Stream> get pendingMessagesStream => channelStateStream - .map((cs) => cs.pendingMessages ?? []) - .distinct(const ListEquality().equals); + Stream> get pendingMessagesStream => + channelStateStream.map((cs) => cs.pendingMessages ?? []).distinct(const ListEquality().equals); /// Get channel last message. Message? get lastMessage => messages.lastOrNull; @@ -3250,38 +3400,41 @@ class ChannelClientState { } /// Channel members list. - List get members => (_channelState.members ?? []) - .map((e) => e.copyWith(user: _client.state.users[e.user!.id])) - .toList(); + List get members => + (_channelState.members ?? []).map((e) => e.copyWith(user: _client.state.users[e.user!.id])).toList(); /// Channel members list as a stream. - Stream> get membersStream => CombineLatestStream.combine2< - List?, Map, List>( + Stream> get membersStream => + CombineLatestStream.combine2?, Map, List>( channelStateStream.map((cs) => cs.members), _client.state.usersStream, - (members, users) => - [...?members?.map((e) => e!.copyWith(user: users[e.user!.id]))], + (members, users) => [...?members?.map((e) => e!.copyWith(user: users[e.user!.id]))], ).distinct(const ListEquality().equals); /// Channel watcher count. int? get watcherCount => _channelState.watcherCount; /// Channel watcher count as a stream. - Stream get watcherCountStream => - channelStateStream.map((cs) => cs.watcherCount); + Stream get watcherCountStream => channelStateStream.map((cs) => cs.watcherCount); /// Channel watchers list. - List get watchers => (_channelState.watchers ?? []) - .map((e) => _client.state.users[e.id] ?? e) - .toList(); + List get watchers => (_channelState.watchers ?? []).map((e) => _client.state.users[e.id] ?? e).toList(); /// Channel watchers list as a stream. - Stream> get watchersStream => CombineLatestStream.combine2< - List?, Map, List>( - channelStateStream.map((cs) => cs.watchers), - _client.state.usersStream, - (watchers, users) => [...?watchers?.map((e) => users[e.id] ?? e)], - ).distinct(const ListEquality().equals); + Stream> get watchersStream => CombineLatestStream.combine2?, Map, List>( + channelStateStream.map((cs) => cs.watchers), + _client.state.usersStream, + (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; @@ -3293,8 +3446,8 @@ class ChannelClientState { /// Channel member for the current user. Member? get currentUserMember => members.firstWhereOrNull( - (m) => m.user?.id == _client.state.currentUser?.id, - ); + (m) => m.user?.id == _client.state.currentUser?.id, + ); /// Channel role for the current user String? get currentUserChannelRole => currentUserMember?.channelRole; @@ -3375,13 +3528,10 @@ class ChannelClientState { /// Update channelState with updated information. void updateChannelState(ChannelState updatedState) { final _existingStateMessages = [...messages]; - final newMessages = [ - ..._existingStateMessages.merge( - updatedState.messages, - key: (message) => message.id, - update: (original, updated) => updated.syncWith(original), - ), - ].sorted(_sortByCreatedAt); + final newMessages = _mergeMessagesIntoExisting( + existing: _existingStateMessages, + toMerge: updatedState.messages ?? [], + ).sorted(_sortByCreatedAt); final _existingStateWatchers = [...?_channelState.watchers]; final newWatchers = [ @@ -3417,11 +3567,11 @@ class ChannelClientState { pinnedMessages: updatedState.pinnedMessages, pendingMessages: updatedState.pendingMessages, pushPreferences: updatedState.pushPreferences, + activeLiveLocations: updatedState.activeLiveLocations, ); } - int _sortByCreatedAt(Message a, Message b) => - a.createdAt.compareTo(b.createdAt); + int _sortByCreatedAt(Message a, Message b) => a.createdAt.compareTo(b.createdAt); /// The channel state related to this client. ChannelState get _channelState => _channelStateController.value; @@ -3480,19 +3630,18 @@ 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 updatedThreads = {...threads}; + + final threadMessages = [...?updatedThreads[parentId]]; + final updatedThreadMessages = _mergeMessagesIntoExisting( + existing: threadMessages, + toMerge: messages, + ).sorted(_sortByCreatedAt); + + // Update the thread with the modified message list. + updatedThreads[parentId] = updatedThreadMessages; - _threads = newThreads; + _threads = updatedThreads; } Draft? _getThreadDraft(String parentId, List? messages) { @@ -3506,13 +3655,11 @@ class ChannelClientState { /// /// This stream emits a new value whenever the draft associated with the /// specified thread is updated or removed. - Stream threadDraftStream(String parentId) => channelStateStream - .map((cs) => _getThreadDraft(parentId, cs.messages)) - .distinct(); + Stream threadDraftStream(String parentId) => + channelStateStream.map((cs) => _getThreadDraft(parentId, cs.messages)).distinct(); /// Channel related typing users stream. - Stream> get typingEventsStream => - _typingEventsController.stream; + Stream> get typingEventsStream => _typingEventsController.stream; /// Channel related typing users last value. Map get typingEvents => _typingEventsController.value; @@ -3561,8 +3708,7 @@ class ChannelClientState { (_) { final now = DateTime.now(); typingEvents.forEach((user, event) { - if (now.difference(event.createdAt).inSeconds > - incomingTypingStartEventTimeout) { + if (now.difference(event.createdAt).inSeconds > incomingTypingStartEventTimeout) { _client.handleEvent( Event( type: EventType.typingStop, @@ -3585,21 +3731,59 @@ class ChannelClientState { const Duration(seconds: 30), (_) { final now = DateTime.now(); - var expiredMessages = channelState.pinnedMessages - ?.where((m) => m.pinExpires?.isBefore(now) == true) - .toList(); + var expiredMessages = channelState.pinnedMessages?.where((m) => m.pinExpires?.isBefore(now) == true).toList(); if (expiredMessages != null && expiredMessages.isNotEmpty) { expiredMessages = expiredMessages - .map((m) => m.copyWith( - pinExpires: null, - pinned: false, - )) + .map( + (m) => m.copyWith( + pinExpires: null, + pinned: false, + ), + ) .toList(); - updateChannelState(_channelState.copyWith( - pinnedMessages: pinnedMessages.where(_pinIsValid).toList(), - messages: expiredMessages, - )); + updateChannelState( + _channelState.copyWith( + pinnedMessages: pinnedMessages.where(_pinIsValid).toList(), + messages: expiredMessages, + ), + ); + } + }, + ); + } + + 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); } }, ); @@ -3623,6 +3807,387 @@ class ChannelClientState { ); } + Future _deleteMessagesFromUser({ + required String userId, + bool hardDelete = false, + DateTime? deletedAt, + }) async { + // Delete messages from persistence. + // + // Note: We perform this operation separately even though [_removeMessages] + // already handles it as we need to delete all messages from the user, not + // only the ones present in the current state. + final persistence = _channel.client.chatPersistenceClient; + await persistence?.deleteMessagesFromUser( + userId: userId, + cid: _channel.cid, + hardDelete: hardDelete, + deletedAt: deletedAt, + ); + + // Gather messages to delete from state. + final userMessages = {}; + for (final message in [...messages, ...threads.values.flattened]) { + if (message.user?.id != userId) continue; + userMessages[message.id] = message.copyWith( + type: MessageType.deleted, + deletedAt: deletedAt ?? DateTime.now(), + state: switch (hardDelete) { + true => MessageState.hardDeleted, + false => MessageState.softDeleted, + }, + ); + } + + final messagesToDelete = userMessages.values; + return _deleteMessages(messagesToDelete, hardDelete: hardDelete); + } + + void _deleteMessages( + Iterable messages, { + bool hardDelete = false, + }) { + if (messages.isEmpty) return; + + if (hardDelete) return _removeMessages(messages); + return _updateMessages(messages); + } + + void _updateMessages(Iterable messages) { + if (messages.isEmpty) return; + + _updateThreadMessages(messages); + _updateChannelMessages(messages); + _updatePinnedMessages(messages); + _updateActiveLiveLocations(messages); + } + + void _updateThreadMessages(Iterable messages) { + if (messages.isEmpty) return; + + final affectedThreads = {...messages.map((it) => it.parentId).nonNulls}; + // If there are no affected threads, return early. + if (affectedThreads.isEmpty) return; + + final updatedThreads = {...threads}; + for (final thread in affectedThreads) { + final threadMessages = [...?updatedThreads[thread]]; + final updatedThreadMessages = _mergeMessagesIntoExisting( + existing: threadMessages, + toMerge: messages, + ); + + // Update the thread with the modified message list. + updatedThreads[thread] = updatedThreadMessages.toList(); + } + + // Update the threads map. + _threads = updatedThreads; + } + + void _updateChannelMessages(Iterable messages) { + if (messages.isEmpty) return; + + final affectedMessages = messages.map((it) { + // If it's not a thread message, consider it affected. + if (it.parentId == null) return it; + // If it's a thread message shown in channel, consider it affected. + if (it.showInChannel == true) return it; + + return null; // Thread message not shown in channel, ignore it. + }).nonNulls; + + // If there are no affected messages, return early. + if (affectedMessages.isEmpty) return; + + final channelMessages = [...this.messages]; + final updatedChannelMessages = _mergeMessagesIntoExisting( + existing: channelMessages, + toMerge: affectedMessages, + ); + + // Calculate the new last message at time. + var lastMessageAt = _channelState.channel?.lastMessageAt; + for (final message in affectedMessages) { + if (MessageRules.canUpdateChannelLastMessageAt(message, _channel)) { + lastMessageAt = [lastMessageAt, message.createdAt].nonNulls.max; + } + } + + _channelState = _channelState.copyWith( + messages: updatedChannelMessages.sorted(_sortByCreatedAt), + channel: _channelState.channel?.copyWith(lastMessageAt: lastMessageAt), + ); + } + + void _updatePinnedMessages(Iterable messages) { + if (messages.isEmpty) return; + + final pinnedMessages = [...this.pinnedMessages]; + final updatedPinnedMessages = _mergePinnedMessagesIntoExisting( + existing: pinnedMessages, + toMerge: messages, + ); + + _channelState = _channelState.copyWith( + pinnedMessages: updatedPinnedMessages.sorted(_sortByCreatedAt), + ); + } + + void _updateActiveLiveLocations(Iterable messages) { + if (messages.isEmpty) return; + + final activeLiveLocations = [...this.activeLiveLocations]; + final updatedActiveLiveLocations = _mergeActiveLocationsIntoExisting( + existing: activeLiveLocations, + toMerge: messages, + ); + + _channelState = _channelState.copyWith( + activeLiveLocations: updatedActiveLiveLocations.toList(), + ); + } + + Iterable _mergeActiveLocationsIntoExisting({ + required Iterable existing, + required Iterable toMerge, + }) { + if (toMerge.isEmpty) return existing; + + final mergedLocations = existing.mergeFrom( + toMerge, + key: (it) => (it.userId, it.channelCid, it.createdByDeviceId), + value: (message) => message.sharedLocation, + update: (original, updated) => updated, + ); + + final toUpdateMap = {for (final m in toMerge) m.id: m}; + final updatedLocations = mergedLocations.where((it) { + // Remove the location if it's expired. + if (it.isExpired) return false; + + final updatedMessage = toUpdateMap[it.messageId]; + // Remove the location if the attached message is deleted. + if (updatedMessage?.isDeleted == true) return false; + + return true; + }); + + return updatedLocations; + } + + Iterable _mergePinnedMessagesIntoExisting({ + required Iterable existing, + required Iterable toMerge, + }) { + return _mergeMessagesIntoExisting( + existing: existing, + toMerge: toMerge, + ).where(_pinIsValid); + } + + Iterable _mergeMessagesIntoExisting({ + required Iterable existing, + required Iterable toMerge, + }) { + if (toMerge.isEmpty) return existing; + + final mergedMessages = existing.merge( + toMerge, + key: (message) => message.id, + update: (original, updated) { + var merged = updated.syncWith(original); + + // Preserve quotedMessage if the updated doesn't include it. + if (updated.quotedMessageId != null && updated.quotedMessage == null) { + merged = merged.copyWith(quotedMessage: original.quotedMessage); + } + + return merged; + }, + ); + + final toUpdateMap = {for (final m in toMerge) m.id: m}; + final updatedMessages = mergedMessages.map((it) { + // Continue if the message doesn't quote any of the updated messages. + if (!toUpdateMap.containsKey(it.quotedMessageId)) return it; + + final updatedQuotedMessage = toUpdateMap[it.quotedMessageId]; + // Update the quotedMessage reference in the message. + return it.copyWith(quotedMessage: updatedQuotedMessage); + }); + + return updatedMessages; + } + + void _removeMessages(Iterable messages) { + if (messages.isEmpty) return; + + final messageIds = messages.map((m) => m.id).toSet().toList(); + final persistenceClient = _channel.client.chatPersistenceClient; + // Remove the messages from the persistence client. + persistenceClient?.deleteMessageByIds(messageIds); + persistenceClient?.deletePinnedMessageByIds(messageIds); + + _removeThreadMessages(messages); + _removeChannelMessages(messages); + _removePinnedMessages(messages); + _removeActiveLiveLocations(messages); + } + + void _removeThreadMessages(Iterable messages) { + if (messages.isEmpty) return; + + final affectedThreads = {...messages.map((it) => it.parentId).nonNulls}; + // If there are no affected threads, return early. + if (affectedThreads.isEmpty) return; + + final updatedThreads = {...threads}; + for (final thread in affectedThreads) { + final threadMessages = updatedThreads[thread]; + // Continue if the thread doesn't exist. + if (threadMessages == null) continue; + + // Remove the deleted message from the thread messages and reference from + // other messages quoting it. + final updatedThreadMessages = _removeMessagesFromExisting( + existing: threadMessages, + toRemove: messages, + ); + + // If there are no more messages in the thread, remove the thread entry. + if (updatedThreadMessages.isEmpty) { + updatedThreads.remove(thread); + continue; + } + + // Otherwise, update the thread with the modified message list. + updatedThreads[thread] = updatedThreadMessages.toList(); + } + + // Update the threads map. + _threads = updatedThreads; + } + + void _removeChannelMessages(Iterable messages) { + if (messages.isEmpty) return; + + final affectedMessages = messages.map((it) { + // If it's not a thread message, consider it affected. + if (it.parentId == null) return it; + // If it's a thread message shown in channel, consider it affected. + if (it.showInChannel == true) return it; + + return null; // Thread message not shown in channel, ignore it. + }).nonNulls; + + // If there are no affected messages, return early. + if (affectedMessages.isEmpty) return; + + final channelMessages = [...this.messages]; + final updatedChannelMessages = _removeMessagesFromExisting( + existing: channelMessages, + toRemove: affectedMessages, + ); + + _channelState = _channelState.copyWith( + messages: updatedChannelMessages.toList(), + ); + } + + void _removePinnedMessages(Iterable messages) { + if (messages.isEmpty) return; + + final pinnedMessages = [...this.pinnedMessages]; + final updatedPinnedMessages = _removePinnedMessagesFromExisting( + existing: pinnedMessages, + toRemove: messages, + ); + + _channelState = _channelState.copyWith( + pinnedMessages: updatedPinnedMessages.toList(), + ); + } + + void _removeActiveLiveLocations(Iterable messages) { + if (messages.isEmpty) return; + + final activeLiveLocations = [...this.activeLiveLocations]; + final updatedActiveLiveLocations = _removeActiveLocationsFromExisting( + existing: activeLiveLocations, + toRemove: messages, + ); + + _channelState = _channelState.copyWith( + activeLiveLocations: updatedActiveLiveLocations.toList(), + ); + } + + Iterable _removeActiveLocationsFromExisting({ + required Iterable existing, + required Iterable toRemove, + }) { + if (toRemove.isEmpty) return existing; + + final toRemoveIds = toRemove.map((m) => m.id).toSet(); + final updatedLocations = existing.where( + // Remove the location if its attached message is in the toRemove list. + (it) => !toRemoveIds.contains(it.messageId), + ); + + return updatedLocations; + } + + Iterable _removePinnedMessagesFromExisting({ + required Iterable existing, + required Iterable toRemove, + }) { + return _removeMessagesFromExisting( + existing: existing, + toRemove: toRemove, + ).where(_pinIsValid); + } + + Iterable _removeMessagesFromExisting({ + required Iterable existing, + required Iterable toRemove, + }) { + if (toRemove.isEmpty) return existing; + + final toRemoveIds = toRemove.map((m) => m.id).toSet(); + final updatedMessages = existing + .where((it) { + // Remove the message if it's in the toRemove list. + return !toRemoveIds.contains(it.id); + }) + .map((it) { + // Continue if the message doesn't quote any of the deleted messages. + if (!toRemoveIds.contains(it.quotedMessageId)) return it; + + // Setting it to null will remove the quoted message from the message. + return it.copyWith(quotedMessageId: null, quotedMessage: null); + }); + + return updatedMessages; + } + + // Listens to user message deleted events and marks messages from that user + // as either soft or hard deleted based on the event data. + void _listenUserMessagesDeleted() { + _subscriptions.add( + _channel.on(EventType.userMessagesDeleted).listen((event) async { + final user = event.user; + if (user == null) return; + + return _deleteMessagesFromUser( + userId: user.id, + hardDelete: event.hardDelete ?? false, + deletedAt: event.createdAt, + ); + }), + ); + } + /// Call this method to dispose this object. void dispose() { _debouncedUpdatePersistenceChannelThreads.cancel(); @@ -3634,13 +4199,24 @@ class ChannelClientState { _threadsController.close(); _staleTypingEventsCleanerTimer?.cancel(); _stalePinnedMessagesCleanerTimer?.cancel(); + _staleLiveLocationsCleanerTimer?.cancel(); _typingEventsController.close(); } } bool _pinIsValid(Message message) { - final now = DateTime.now(); - return message.pinExpires!.isAfter(now); + // If the message is deleted, the pin is not valid. + if (message.isDeleted) return false; + + // If the message is not pinned, it's not valid. + if (message.pinned != true) return false; + + // If there's no expiration, the pin is valid. + final pinExpires = message.pinExpires; + if (pinExpires == null) return true; + + // If there's an expiration, check if it's still valid. + return pinExpires.isAfter(DateTime.now()); } /// Extension methods for reading related operations on a ChannelClientState. @@ -3887,4 +4463,9 @@ extension ChannelCapabilityCheck on Channel { bool get canUseDeliveryReceipts { return ownCapabilities.contains(ChannelCapability.deliveryEvents); } + + /// 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/channel_delivery_reporter.dart b/packages/stream_chat/lib/src/client/channel_delivery_reporter.dart index 001f19625a..1342d397a3 100644 --- a/packages/stream_chat/lib/src/client/channel_delivery_reporter.dart +++ b/packages/stream_chat/lib/src/client/channel_delivery_reporter.dart @@ -10,9 +10,10 @@ import 'package:synchronized/synchronized.dart'; /// /// Each [MessageDeliveryInfo] represents an acknowledgment that the current /// user has received a message. -typedef MarkChannelsDelivered = Future Function( - Iterable deliveries, -); +typedef MarkChannelsDelivered = + Future Function( + Iterable deliveries, + ); /// Manages the delivery reporting for channel messages. /// @@ -31,8 +32,8 @@ class ChannelDeliveryReporter { Logger? logger, required this.onMarkChannelsDelivered, Duration throttleDuration = const Duration(seconds: 1), - }) : _logger = logger, - _markAsDeliveredThrottleDuration = throttleDuration; + }) : _logger = logger, + _markAsDeliveredThrottleDuration = throttleDuration; final Logger? _logger; final Duration _markAsDeliveredThrottleDuration; @@ -43,7 +44,7 @@ class ChannelDeliveryReporter { final MarkChannelsDelivered onMarkChannelsDelivered; final _deliveryCandidatesLock = Lock(); - final _deliveryCandidates = {}; + final _deliveryCandidates = {}; /// Submits [channels] for delivery reporting. /// diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 6682ec28cc..4f24364bb3 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -7,6 +7,7 @@ import 'package:meta/meta.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/client/channel.dart'; import 'package:stream_chat/src/client/channel_delivery_reporter.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'; @@ -26,6 +27,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_delivery.dart'; @@ -35,8 +38,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'; @@ -80,12 +86,11 @@ class StreamChatClient { RetryPolicy? retryPolicy, String? baseURL, String? baseWsUrl, - Duration connectTimeout = const Duration(seconds: 6), - Duration receiveTimeout = const Duration(seconds: 6), + Duration connectTimeout = kDefaultConnectTimeout, + Duration receiveTimeout = kDefaultReceiveTimeout, StreamChatApi? chatApi, WebSocket? ws, - AttachmentFileUploaderProvider attachmentFileUploaderProvider = - StreamAttachmentFileUploader.new, + AttachmentFileUploaderProvider attachmentFileUploaderProvider = StreamAttachmentFileUploader.new, Iterable? chatApiInterceptors, HttpClientAdapter? httpClientAdapter, }) { @@ -97,7 +102,8 @@ class StreamChatClient { receiveTimeout: receiveTimeout, ); - _chatApi = chatApi ?? + _chatApi = + chatApi ?? StreamChatApi( apiKey, options: options, @@ -110,7 +116,8 @@ class StreamChatClient { httpClientAdapter: httpClientAdapter, ); - _ws = ws ?? + _ws = + ws ?? WebSocket( apiKey: apiKey, baseUrl: baseWsUrl ?? options.baseUrl, @@ -120,7 +127,8 @@ class StreamChatClient { logger: detachedLogger('🔌'), ); - _retryPolicy = retryPolicy ?? + _retryPolicy = + retryPolicy ?? RetryPolicy( shouldRetry: (_, __, error) { return error is StreamChatNetworkError && error.isRetriable; @@ -239,21 +247,19 @@ class StreamChatClient { onMarkChannelsDelivered: markChannelsDelivered, ); - 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; @@ -288,12 +294,11 @@ class StreamChatClient { User user, String token, { bool connectWebSocket = true, - }) => - _connectUser( - user, - token: Token.fromRawValue(token), - connectWebSocket: connectWebSocket, - ); + }) => _connectUser( + user, + token: Token.fromRawValue(token), + connectWebSocket: connectWebSocket, + ); /// Connects the current user using the [tokenProvider] to fetch the token. /// It returns a [Future] that resolves when the connection is setup. @@ -301,12 +306,11 @@ class StreamChatClient { User user, TokenProvider tokenProvider, { bool connectWebSocket = true, - }) => - _connectUser( - user, - provider: tokenProvider, - connectWebSocket: connectWebSocket, - ); + }) => _connectUser( + user, + provider: tokenProvider, + connectWebSocket: connectWebSocket, + ); /// Connects the current user with an anonymous id, this triggers a connection /// to the API. It returns a [Future] that resolves when the connection is @@ -519,10 +523,12 @@ class StreamChatClient { final isConnected = currStatus == ConnectionStatus.connected; // Notify the connection status change event - handleEvent(Event( - type: EventType.connectionChanged, - online: isConnected, - )); + handleEvent( + Event( + type: EventType.connectionChanged, + online: isConnected, + ), + ); final connectionRecovered = !wasConnected && isConnected; @@ -539,10 +545,12 @@ class StreamChatClient { if (persistenceEnabled) await sync(cids: cids); } - handleEvent(Event( - type: EventType.connectionRecovered, - online: true, - )); + handleEvent( + Event( + type: EventType.connectionRecovered, + online: true, + ), + ); } } @@ -555,11 +563,10 @@ class StreamChatClient { String? eventType4, ]) { if (eventType == null || eventType == EventType.any) return eventStream; - return eventStream.where((event) => - event.type == eventType || - event.type == eventType2 || - event.type == eventType3 || - event.type == eventType4); + return eventStream.where( + (event) => + event.type == eventType || event.type == eventType2 || event.type == eventType3 || event.type == eventType4, + ); } // Lock to make sure only one sync process is running at a time. @@ -661,6 +668,7 @@ class StreamChatClient { offlineChannels = await queryChannelsOffline( filter: filter, channelStateSort: channelStateSort, + messageLimit: messageLimit, paginationParams: paginationParams, ); @@ -671,26 +679,29 @@ class StreamChatClient { } try { - final newQueryChannelsFuture = queryChannelsOnline( - filter: filter, - sort: channelStateSort, - state: state, - watch: watch, - presence: presence, - memberLimit: memberLimit, - messageLimit: messageLimit, - paginationParams: paginationParams, - waitForConnect: waitForConnect, - ).timeout( - const Duration(seconds: 30), - onTimeout: () { - logger.warning('Online channel query timed out'); - throw TimeoutException('Channel query timed out'); - }, - ).whenComplete(() { - // Always clean up cache reference when done - _queryChannelsStreams.remove(hash); - }); + final newQueryChannelsFuture = + queryChannelsOnline( + filter: filter, + sort: channelStateSort, + state: state, + watch: watch, + presence: presence, + memberLimit: memberLimit, + messageLimit: messageLimit, + paginationParams: paginationParams, + waitForConnect: waitForConnect, + ) + .timeout( + const Duration(seconds: 30), + onTimeout: () { + logger.warning('Online channel query timed out'); + throw TimeoutException('Channel query timed out'); + }, + ) + .whenComplete(() { + // Always clean up cache reference when done + _queryChannelsStreams.remove(hash); + }); // Store the future in cache _queryChannelsStreams[hash] = newQueryChannelsFuture; @@ -703,27 +714,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, @@ -762,7 +752,8 @@ class StreamChatClient { watch: watch, presence: presence, memberLimit: memberLimit, - messageLimit: messageLimit, + // Default limit is set to 25 in backend. + messageLimit: messageLimit ?? 25, paginationParams: paginationParams, ); @@ -777,10 +768,7 @@ class StreamChatClient { final channels = res.channels; - final users = channels - .expand((it) => it.members ?? []) - .map((it) => it.user) - .toList(growable: false); + final users = channels.expand((it) => it.members ?? []).map((it) => it.user).toList(growable: false); this.state.updateUsers(users); @@ -805,14 +793,22 @@ class StreamChatClient { Future> queryChannelsOffline({ Filter? filter, SortOrder? channelStateSort, + int? messageLimit, PaginationParams paginationParams = const PaginationParams(), }) async { - final offlineChannels = (await chatPersistenceClient?.getChannelStates( - filter: filter, - channelStateSort: channelStateSort, - paginationParams: paginationParams, - )) ?? - []; + final offlineChannels = await chatPersistenceClient?.getChannelStates( + filter: filter, + channelStateSort: channelStateSort, + // Default limit is set to 25 in backend. + messageLimit: messageLimit ?? 25, + paginationParams: paginationParams, + ); + + if (offlineChannels == null || offlineChannels.isEmpty) { + logger.info('No channels found in offline storage for the given query'); + return []; + } + final updatedData = _mapChannelStateToChannel(offlineChannels); state.addChannels(updatedData.key); return updatedData.value; @@ -861,12 +857,11 @@ class StreamChatClient { required Filter filter, SortOrder? sort, PaginationParams? pagination, - }) => - _chatApi.moderation.queryBannedUsers( - filter: filter, - sort: sort, - pagination: pagination, - ); + }) => _chatApi.moderation.queryBannedUsers( + filter: filter, + sort: sort, + pagination: pagination, + ); /// A message search. Future search( @@ -875,14 +870,13 @@ class StreamChatClient { SortOrder? sort, PaginationParams? paginationParams, Filter? messageFilters, - }) => - _chatApi.general.searchMessages( - filter, - query: query, - sort: sort, - pagination: paginationParams, - messageFilters: messageFilters, - ); + }) => _chatApi.general.searchMessages( + filter, + query: query, + sort: sort, + pagination: paginationParams, + messageFilters: messageFilters, + ); /// Send a [file] to the [channelId] of type [channelType] Future sendFile( @@ -892,15 +886,14 @@ class StreamChatClient { ProgressCallback? onSendProgress, CancelToken? cancelToken, Map? extraData, - }) => - _chatApi.fileUploader.sendFile( - file, - channelId, - channelType, - onSendProgress: onSendProgress, - cancelToken: cancelToken, - extraData: extraData, - ); + }) => _chatApi.fileUploader.sendFile( + file, + channelId, + channelType, + onSendProgress: onSendProgress, + cancelToken: cancelToken, + extraData: extraData, + ); /// Send a [image] to the [channelId] of type [channelType] Future sendImage( @@ -910,15 +903,14 @@ class StreamChatClient { ProgressCallback? onSendProgress, CancelToken? cancelToken, Map? extraData, - }) => - _chatApi.fileUploader.sendImage( - image, - channelId, - channelType, - onSendProgress: onSendProgress, - cancelToken: cancelToken, - extraData: extraData, - ); + }) => _chatApi.fileUploader.sendImage( + image, + channelId, + channelType, + onSendProgress: onSendProgress, + cancelToken: cancelToken, + extraData: extraData, + ); /// Delete a file from this channel Future deleteFile( @@ -927,14 +919,13 @@ class StreamChatClient { String channelType, { CancelToken? cancelToken, Map? extraData, - }) => - _chatApi.fileUploader.deleteFile( - url, - channelId, - channelType, - cancelToken: cancelToken, - extraData: extraData, - ); + }) => _chatApi.fileUploader.deleteFile( + url, + channelId, + channelType, + cancelToken: cancelToken, + extraData: extraData, + ); /// Delete an image from this channel Future deleteImage( @@ -943,14 +934,71 @@ class StreamChatClient { String channelType, { CancelToken? cancelToken, Map? extraData, - }) => - _chatApi.fileUploader.deleteImage( - url, - channelId, - channelType, - cancelToken: cancelToken, - extraData: extraData, - ); + }) => _chatApi.fileUploader.deleteImage( + url, + channelId, + channelType, + cancelToken: cancelToken, + extraData: extraData, + ); + + /// Upload an image to the Stream CDN + /// + /// Upload progress can be tracked using [onProgress], and the operation can + /// be cancelled using [cancelToken]. + /// + /// Returns a [UploadImageResponse] once uploaded successfully. + Future uploadImage( + AttachmentFile image, { + ProgressCallback? onUploadProgress, + CancelToken? cancelToken, + }) => _chatApi.fileUploader.uploadImage( + image, + onSendProgress: onUploadProgress, + cancelToken: cancelToken, + ); + + /// Upload a file to the Stream CDN + /// + /// Upload progress can be tracked using [onProgress], and the operation can + /// be cancelled using [cancelToken]. + /// + /// Returns a [UploadFileResponse] once uploaded successfully. + Future uploadFile( + AttachmentFile file, { + ProgressCallback? onUploadProgress, + CancelToken? cancelToken, + }) => _chatApi.fileUploader.uploadFile( + file, + onSendProgress: onUploadProgress, + cancelToken: cancelToken, + ); + + /// Remove an image from the Stream CDN using its [url]. + /// + /// The operation can be cancelled using [cancelToken] if needed. + /// + /// Returns an [EmptyResponse] once removed successfully. + Future removeImage( + String url, { + CancelToken? cancelToken, + }) => _chatApi.fileUploader.removeImage( + url, + cancelToken: cancelToken, + ); + + /// Remove a file from the Stream CDN using its [url]. + /// + /// The operation can be cancelled using [cancelToken] if needed. + /// + /// Returns an [EmptyResponse] once removed successfully. + Future removeFile( + String url, { + CancelToken? cancelToken, + }) => _chatApi.fileUploader.removeFile( + url, + cancelToken: cancelToken, + ); /// Replaces the [channelId] of type [ChannelType] data with [data]. /// @@ -960,13 +1008,12 @@ class StreamChatClient { String channelType, Map data, { Message? message, - }) => - _chatApi.channel.updateChannel( - channelId, - channelType, - data, - message: message, - ); + }) => _chatApi.channel.updateChannel( + channelId, + channelType, + data, + message: message, + ); /// Partial update for the [channelId] of type [ChannelType]. Sets the /// data provided in [set], and removes the attributes given in [unset]. @@ -977,32 +1024,29 @@ class StreamChatClient { String channelType, { Map? set, List? unset, - }) => - _chatApi.channel.updateChannelPartial( - channelId, - channelType, - set: set, - unset: unset, - ); + }) => _chatApi.channel.updateChannelPartial( + channelId, + channelType, + set: set, + unset: unset, + ); /// Add a device for Push Notifications. Future addDevice( String id, PushProvider pushProvider, { String? pushProviderName, - }) => - _chatApi.device.addDevice( - id, - pushProvider, - pushProviderName: pushProviderName, - ); + }) => _chatApi.device.addDevice( + id, + pushProvider, + pushProviderName: pushProviderName, + ); /// Gets a list of user devices. Future getDevices() => _chatApi.device.getDevices(); /// Remove a user's device. - Future removeDevice(String id) => - _chatApi.device.removeDevice(id); + Future removeDevice(String id) => _chatApi.device.removeDevice(id); /// Set push preferences for the current user. /// @@ -1103,13 +1147,12 @@ class StreamChatClient { String channelType, { String? channelId, Map? channelData, - }) => - queryChannel( - channelType, - channelId: channelId, - state: false, - channelData: channelData, - ); + }) => queryChannel( + channelType, + channelId: channelId, + state: false, + channelData: channelData, + ); /// watches the provided channel /// Creates first if not yet created @@ -1117,13 +1160,12 @@ class StreamChatClient { String channelType, { String? channelId, Map? channelData, - }) => - queryChannel( - channelType, - channelId: channelId, - watch: true, - channelData: channelData, - ); + }) => queryChannel( + channelType, + channelId: channelId, + watch: true, + channelData: channelData, + ); /// Query the API, get messages, members or other channel fields /// Creates the channel first if not yet created @@ -1137,18 +1179,17 @@ class StreamChatClient { PaginationParams? messagesPagination, PaginationParams? membersPagination, PaginationParams? watchersPagination, - }) => - _chatApi.channel.queryChannel( - channelType, - channelId: channelId, - channelData: channelData, - state: state, - watch: watch, - presence: presence, - messagesPagination: messagesPagination, - membersPagination: membersPagination, - watchersPagination: watchersPagination, - ); + }) => _chatApi.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: state, + watch: watch, + presence: presence, + messagesPagination: messagesPagination, + membersPagination: membersPagination, + watchersPagination: watchersPagination, + ); /// Query channel members Future queryMembers( @@ -1158,15 +1199,14 @@ class StreamChatClient { List? members, SortOrder? sort, PaginationParams? pagination, - }) => - _chatApi.general.queryMembers( - channelType, - channelId: channelId, - filter: filter, - members: members, - sort: sort, - pagination: pagination, - ); + }) => _chatApi.general.queryMembers( + channelType, + channelId: channelId, + filter: filter, + members: members, + sort: sort, + pagination: pagination, + ); /// Hides the channel from [queryChannels] for the user /// until a message is added If [clearHistory] is set to true - all messages @@ -1175,32 +1215,29 @@ class StreamChatClient { String channelId, String channelType, { bool clearHistory = false, - }) => - _chatApi.channel.hideChannel( - channelId, - channelType, - clearHistory: clearHistory, - ); + }) => _chatApi.channel.hideChannel( + channelId, + channelType, + clearHistory: clearHistory, + ); /// Removes the hidden status for the channel Future showChannel( String channelId, String channelType, - ) => - _chatApi.channel.showChannel( - channelId, - channelType, - ); + ) => _chatApi.channel.showChannel( + channelId, + channelType, + ); /// Delete this channel. Messages are permanently removed. Future deleteChannel( String channelId, String channelType, - ) => - _chatApi.channel.deleteChannel( - channelId, - channelType, - ); + ) => _chatApi.channel.deleteChannel( + channelId, + channelType, + ); /// Removes all messages from the channel up to [truncatedAt] or now if /// [truncatedAt] is not provided. @@ -1212,52 +1249,47 @@ class StreamChatClient { Message? message, bool? skipPush, DateTime? truncatedAt, - }) => - _chatApi.channel.truncateChannel( - channelId, - channelType, - message: message, - skipPush: skipPush, - truncatedAt: truncatedAt, - ); + }) => _chatApi.channel.truncateChannel( + channelId, + channelType, + message: message, + skipPush: skipPush, + truncatedAt: truncatedAt, + ); /// Mutes the channel Future muteChannel( String channelCid, { Duration? expiration, - }) => - _chatApi.moderation.muteChannel( - channelCid, - expiration: expiration, - ); + }) => _chatApi.moderation.muteChannel( + channelCid, + expiration: expiration, + ); /// Unmutes the channel - Future unmuteChannel(String channelCid) => - _chatApi.moderation.unmuteChannel(channelCid); + Future unmuteChannel(String channelCid) => _chatApi.moderation.unmuteChannel(channelCid); /// Accept invitation to the channel Future acceptChannelInvite( String channelId, String channelType, { Message? message, - }) => - _chatApi.channel.acceptChannelInvite( - channelId, - channelType, - message: message, - ); + }) => _chatApi.channel.acceptChannelInvite( + channelId, + channelType, + message: message, + ); /// Reject invitation to the channel Future rejectChannelInvite( String channelId, String channelType, { Message? message, - }) => - _chatApi.channel.rejectChannelInvite( - channelId, - channelType, - message: message, - ); + }) => _chatApi.channel.rejectChannelInvite( + channelId, + channelType, + message: message, + ); /// Add members to the channel Future addChannelMembers( @@ -1267,15 +1299,14 @@ class StreamChatClient { Message? message, bool hideHistory = false, DateTime? hideHistoryBefore, - }) => - _chatApi.channel.addMembers( - channelId, - channelType, - memberIds, - message: message, - hideHistory: hideHistory, - hideHistoryBefore: hideHistoryBefore, - ); + }) => _chatApi.channel.addMembers( + channelId, + channelType, + memberIds, + message: message, + hideHistory: hideHistory, + hideHistoryBefore: hideHistoryBefore, + ); /// Remove members from the channel Future removeChannelMembers( @@ -1283,13 +1314,12 @@ class StreamChatClient { String channelType, List memberIds, { Message? message, - }) => - _chatApi.channel.removeMembers( - channelId, - channelType, - memberIds, - message: message, - ); + }) => _chatApi.channel.removeMembers( + channelId, + channelType, + memberIds, + message: message, + ); /// Invite members to the channel Future inviteChannelMembers( @@ -1297,23 +1327,21 @@ class StreamChatClient { String channelType, List memberIds, { Message? message, - }) => - _chatApi.channel.inviteChannelMembers( - channelId, - channelType, - memberIds, - message: message, - ); + }) => _chatApi.channel.inviteChannelMembers( + channelId, + channelType, + memberIds, + message: message, + ); /// Stop watching the channel Future stopChannelWatching( String channelId, String channelType, - ) => - _chatApi.channel.stopWatching( - channelId, - channelType, - ); + ) => _chatApi.channel.stopWatching( + channelId, + channelType, + ); /// Send action for a specific message of this channel Future sendAction( @@ -1321,13 +1349,12 @@ class StreamChatClient { String channelType, String messageId, Map formData, - ) => - _chatApi.message.sendAction( - channelId, - channelType, - messageId, - formData, - ); + ) => _chatApi.message.sendAction( + channelId, + channelType, + messageId, + formData, + ); /// Mark [channelId] of type [channelType] all messages as read /// Optionally provide a [messageId] if you want to mark a @@ -1336,12 +1363,11 @@ class StreamChatClient { String channelId, String channelType, { String? messageId, - }) => - _chatApi.channel.markRead( - channelId, - channelType, - messageId: messageId, - ); + }) => _chatApi.channel.markRead( + channelId, + channelType, + messageId: messageId, + ); /// Marks the [channelId] of type [channelType] as unread /// by a given [messageId]. @@ -1351,12 +1377,11 @@ class StreamChatClient { String channelId, String channelType, String messageId, - ) => - _chatApi.channel.markUnread( - channelId, - channelType, - messageId, - ); + ) => _chatApi.channel.markUnread( + channelId, + channelType, + messageId, + ); /// Marks the [channelId] of type [channelType] as unread /// by a given [timestamp]. @@ -1366,12 +1391,11 @@ class StreamChatClient { String channelId, String channelType, DateTime timestamp, - ) => - _chatApi.channel.markUnreadByTimestamp( - channelId, - channelType, - timestamp, - ); + ) => _chatApi.channel.markUnreadByTimestamp( + channelId, + channelType, + timestamp, + ); /// Mark the thread with [threadId] in the channel with [channelId] of type /// [channelType] as read. @@ -1379,12 +1403,11 @@ class StreamChatClient { String channelId, String channelType, String threadId, - ) => - _chatApi.channel.markThreadRead( - channelId, - channelType, - threadId, - ); + ) => _chatApi.channel.markThreadRead( + channelId, + channelType, + threadId, + ); /// Mark the thread with [threadId] in the channel with [channelId] of type /// [channelType] as unread. @@ -1392,24 +1415,20 @@ class StreamChatClient { String channelId, String channelType, String threadId, - ) => - _chatApi.channel.markThreadUnread( - channelId, - channelType, - threadId, - ); + ) => _chatApi.channel.markThreadUnread( + channelId, + channelType, + threadId, + ); /// Creates a new Poll - Future createPoll(Poll poll) => - _chatApi.polls.createPoll(poll); + Future createPoll(Poll poll) => _chatApi.polls.createPoll(poll); /// Retrieves a Poll by [pollId] - Future getPoll(String pollId) => - _chatApi.polls.getPoll(pollId); + Future getPoll(String pollId) => _chatApi.polls.getPoll(pollId); /// Updates a Poll - Future updatePoll(Poll poll) => - _chatApi.polls.updatePoll(poll); + Future updatePoll(Poll poll) => _chatApi.polls.updatePoll(poll); /// Partially updates a Poll by [pollId]. /// @@ -1419,50 +1438,46 @@ class StreamChatClient { String pollId, { Map? set, List? unset, - }) => - _chatApi.polls.partialUpdatePoll( - pollId, - set: set, - unset: unset, - ); + }) => _chatApi.polls.partialUpdatePoll( + pollId, + set: set, + unset: unset, + ); /// Deletes the Poll by [pollId]. - Future deletePoll(String pollId) => - _chatApi.polls.deletePoll(pollId); + Future deletePoll(String pollId) => _chatApi.polls.deletePoll(pollId); /// Marks the Poll [pollId] as closed. - Future closePoll(String pollId) => - partialUpdatePoll(pollId, set: { - 'is_closed': true, - }); + Future closePoll(String pollId) => partialUpdatePoll( + pollId, + set: { + 'is_closed': true, + }, + ); /// Creates a new Poll Option for the Poll [pollId]. Future createPollOption( String pollId, PollOption option, - ) => - _chatApi.polls.createPollOption(pollId, option); + ) => _chatApi.polls.createPollOption(pollId, option); /// Retrieves a Poll Option by [optionId] for the Poll [pollId]. Future getPollOption( String pollId, String optionId, - ) => - _chatApi.polls.getPollOption(pollId, optionId); + ) => _chatApi.polls.getPollOption(pollId, optionId); /// Updates a Poll Option for the Poll [pollId]. Future updatePollOption( String pollId, PollOption option, - ) => - _chatApi.polls.updatePollOption(pollId, option); + ) => _chatApi.polls.updatePollOption(pollId, option); /// Deletes a Poll Option by [optionId] for the Poll [pollId]. Future deletePollOption( String pollId, String optionId, - ) => - _chatApi.polls.deletePollOption(pollId, optionId); + ) => _chatApi.polls.deletePollOption(pollId, optionId); /// Cast a [vote] for the Poll [pollId]. Future castPollVote( @@ -1489,20 +1504,18 @@ class StreamChatClient { String messageId, String pollId, String voteId, - ) => - _chatApi.polls.removePollVote(messageId, pollId, voteId); + ) => _chatApi.polls.removePollVote(messageId, pollId, voteId); /// Queries Polls with the given [filter] and [sort] options. Future queryPolls({ Filter? filter, SortOrder? sort, PaginationParams pagination = const PaginationParams(), - }) => - _chatApi.polls.queryPolls( - filter: filter, - sort: sort, - pagination: pagination, - ); + }) => _chatApi.polls.queryPolls( + filter: filter, + sort: sort, + pagination: pagination, + ); /// Queries Poll Votes for the Poll [pollId] with the given [filter] /// and [sort] options. @@ -1511,20 +1524,18 @@ class StreamChatClient { Filter? filter, SortOrder? sort, PaginationParams pagination = const PaginationParams(), - }) => - _chatApi.polls.queryPollVotes( - pollId, - filter: filter, - sort: sort, - pagination: pagination, - ); + }) => _chatApi.polls.queryPollVotes( + pollId, + filter: filter, + sort: sort, + pagination: pagination, + ); /// Update or Create the given user object. Future updateUser(User user) => updateUsers([user]); /// Batch update a list of users - Future updateUsers(List users) => - _chatApi.user.updateUsers(users); + Future updateUsers(List users) => _chatApi.user.updateUsers(users); /// Partially update the given user with [id]. /// Use [set] to define values to be set. @@ -1545,48 +1556,43 @@ class StreamChatClient { /// Batch partial updates the [users]. Future partialUpdateUsers( List users, - ) => - _chatApi.user.partialUpdateUsers(users); + ) => _chatApi.user.partialUpdateUsers(users); /// Bans a user from all channels Future banUser( String targetUserId, [ Map options = const {}, - ]) => - _chatApi.moderation.banUser( - targetUserId, - options: options, - ); + ]) => _chatApi.moderation.banUser( + targetUserId, + options: options, + ); /// Remove global ban for a user Future unbanUser( String targetUserId, [ Map options = const {}, - ]) => - _chatApi.moderation.unbanUser( - targetUserId, - options: options, - ); + ]) => _chatApi.moderation.unbanUser( + targetUserId, + options: options, + ); /// Shadow bans a user Future shadowBan( String targetID, [ Map options = const {}, - ]) => - banUser(targetID, { - 'shadow': true, - ...options, - }); + ]) => banUser(targetID, { + 'shadow': true, + ...options, + }); /// Removes shadow ban from a user Future removeShadowBan( String targetID, [ Map options = const {}, - ]) => - unbanUser(targetID, { - 'shadow': true, - ...options, - }); + ]) => unbanUser(targetID, { + 'shadow': true, + ...options, + }); final _userBlockLock = Lock(); @@ -1656,38 +1662,34 @@ class StreamChatClient { // Emit an local event with the unread count information as a side effect // in order to update the current user state. - handleEvent(Event( - totalUnreadCount: response.totalUnreadCount, - unreadChannels: response.channels.length, - unreadThreads: response.threads.length, - )); + handleEvent( + Event( + totalUnreadCount: response.totalUnreadCount, + unreadChannels: response.channels.length, + unreadThreads: response.threads.length, + ), + ); return response; } /// Mutes a user - Future muteUser(String userId) => - _chatApi.moderation.muteUser(userId); + Future muteUser(String userId) => _chatApi.moderation.muteUser(userId); /// Unmutes a user - Future unmuteUser(String userId) => - _chatApi.moderation.unmuteUser(userId); + Future unmuteUser(String userId) => _chatApi.moderation.unmuteUser(userId); /// Flag a message - Future flagMessage(String messageId) => - _chatApi.moderation.flagMessage(messageId); + Future flagMessage(String messageId) => _chatApi.moderation.flagMessage(messageId); /// Unflag a message - Future unflagMessage(String messageId) => - _chatApi.moderation.unflagMessage(messageId); + Future unflagMessage(String messageId) => _chatApi.moderation.unflagMessage(messageId); /// Flag a user - Future flagUser(String userId) => - _chatApi.moderation.flagUser(userId); + Future flagUser(String userId) => _chatApi.moderation.flagUser(userId); /// Unflag a message - Future unflagUser(String userId) => - _chatApi.moderation.unflagUser(userId); + Future unflagUser(String userId) => _chatApi.moderation.unflagUser(userId); /// Mark all channels for this user as read Future markAllRead() => _chatApi.channel.markAllRead(); @@ -1720,44 +1722,34 @@ class StreamChatClient { String channelId, String channelType, Event event, - ) => - _chatApi.channel.sendEvent( - channelId, - channelType, - event, - ); + ) => _chatApi.channel.sendEvent( + channelId, + channelType, + event, + ); /// Send a [reactionType] for this [messageId] /// 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( String messageId, String reactionType, - ) => - _chatApi.message.deleteReaction( - messageId, - reactionType, - ); + ) => _chatApi.message.deleteReaction( + messageId, + reactionType, + ); /// Sends the message to the given channel Future sendMessage( @@ -1766,46 +1758,59 @@ class StreamChatClient { String channelType, { bool skipPush = false, bool skipEnrichUrl = false, - }) => - _chatApi.message.sendMessage( - channelId, - channelType, - message, - skipPush: skipPush, - skipEnrichUrl: skipEnrichUrl, - ); + }) => _chatApi.message.sendMessage( + channelId, + channelType, + message, + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ); /// Lists all the message replies for the [parentId] Future getReplies( String parentId, { PaginationParams? options, - }) => - _chatApi.message.getReplies( - parentId, - options: options, - ); + }) => _chatApi.message.getReplies( + parentId, + options: options, + ); /// Get all the reactions for a [messageId] Future getReactions( String messageId, { PaginationParams? pagination, - }) => - _chatApi.message.getReactions( - messageId, - pagination: pagination, - ); + }) => _chatApi.message.getReactions( + messageId, + pagination: pagination, + ); + + /// Queries reactions for a [messageId] with optional [filter], [sort], + /// and [pagination]. + /// + /// Unlike [getReactions], this method supports filtering by reaction type, + /// user ID, or creation date, sorting, and cursor-based pagination. + Future queryReactions( + String messageId, { + Filter? filter, + SortOrder? sort, + PaginationParams? pagination, + }) => _chatApi.message.queryReactions( + messageId, + filter: filter, + sort: sort, + pagination: pagination, + ); /// Update the given message Future updateMessage( Message message, { bool skipPush = false, bool skipEnrichUrl = false, - }) => - _chatApi.message.updateMessage( - message, - skipPush: skipPush, - skipEnrichUrl: skipEnrichUrl, - ); + }) => _chatApi.message.updateMessage( + message, + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ); /// Partially update the given [messageId] /// Use [set] to define values to be set @@ -1815,34 +1820,40 @@ class StreamChatClient { Map? set, List? unset, bool skipEnrichUrl = false, - }) => - _chatApi.message.partialUpdateMessage( - messageId, - set: set, - unset: unset, - skipEnrichUrl: skipEnrichUrl, - ); + }) => _chatApi.message.partialUpdateMessage( + messageId, + set: set, + unset: unset, + skipEnrichUrl: skipEnrichUrl, + ); - /// Deletes the given message + /// Deletes the given message. + /// + /// If [hard] is true, the message is permanently deleted. Future deleteMessage( String messageId, { bool hard = false, - }) async { - final response = await _chatApi.message.deleteMessage( + }) { + return _chatApi.message.deleteMessage( messageId, hard: hard, ); + } - if (hard) { - await chatPersistenceClient?.deleteMessageById(messageId); - } - - return response; + /// Deletes the given message for the current user only. + /// + /// Note: This does not delete the message for other users in the channel. + Future deleteMessageForMe( + String messageId, + ) { + return _chatApi.message.deleteMessage( + messageId, + deleteForMe: true, + ); } /// Get a message by [messageId] - Future getMessage(String messageId) => - _chatApi.message.getMessage(messageId); + Future getMessage(String messageId) => _chatApi.message.getMessage(messageId); /// Retrieves a list of messages by [messageIDs] /// from the given [channelId] of type [channelType] @@ -1850,34 +1861,31 @@ class StreamChatClient { String channelId, String channelType, List messageIDs, - ) => - _chatApi.message.getMessagesById( - channelId, - channelType, - messageIDs, - ); + ) => _chatApi.message.getMessagesById( + channelId, + channelType, + messageIDs, + ); /// Translates the [messageId] in provided [language] Future translateMessage( String messageId, String language, - ) => - _chatApi.message.translateMessage( - messageId, - language, - ); + ) => _chatApi.message.translateMessage( + messageId, + language, + ); /// Creates a draft for the given [channelId] of type [channelType]. Future createDraft( DraftMessage draft, String channelId, String channelType, - ) => - _chatApi.message.createDraft( - channelId, - channelType, - draft, - ); + ) => _chatApi.message.createDraft( + channelId, + channelType, + draft, + ); /// Retrieves a draft for the given [channelId] of type [channelType]. /// @@ -1886,12 +1894,11 @@ class StreamChatClient { String channelId, String channelType, { String? parentId, - }) => - _chatApi.message.getDraft( - channelId, - channelType, - parentId: parentId, - ); + }) => _chatApi.message.getDraft( + channelId, + channelType, + parentId: parentId, + ); /// Deletes a draft for the given [channelId] of type [channelType]. /// @@ -1900,45 +1907,86 @@ class StreamChatClient { String channelId, String channelType, { String? parentId, - }) => - _chatApi.message.deleteDraft( - channelId, - channelType, - parentId: parentId, - ); + }) => _chatApi.message.deleteDraft( + channelId, + channelType, + parentId: parentId, + ); /// Queries drafts for the current user. Future queryDrafts({ Filter? filter, SortOrder? sort, PaginationParams? pagination, - }) => - _chatApi.message.queryDrafts( - sort: sort, - pagination: pagination, - ); + }) => _chatApi.message.queryDrafts( + sort: sort, + 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, String channelType, int cooldown, - ) async => - _chatApi.channel.enableSlowdown( - channelId, - channelType, - cooldown, - ); + ) async => _chatApi.channel.enableSlowdown( + channelId, + channelType, + cooldown, + ); /// Disables slow mode Future disableSlowdown( String channelId, String channelType, - ) async => - _chatApi.channel.disableSlowdown( - channelId, - channelType, - ); + ) async => _chatApi.channel.disableSlowdown( + channelId, + channelType, + ); /// Pins provided message /// [timeoutOrExpirationDate] can either be a [DateTime] or a value in seconds @@ -1948,9 +1996,7 @@ class StreamChatClient { Object? /*num|DateTime*/ timeoutOrExpirationDate, }) { assert(() { - if (timeoutOrExpirationDate is! DateTime && - timeoutOrExpirationDate != null && - timeoutOrExpirationDate is! num) { + if (timeoutOrExpirationDate is! DateTime && timeoutOrExpirationDate != null && timeoutOrExpirationDate is! num) { throw ArgumentError('Invalid timeout or Expiration date'); } return true; @@ -1974,17 +2020,15 @@ class StreamChatClient { } /// Unpins provided message - Future unpinMessage(String messageId) => - partialUpdateMessage( - messageId, - set: { - 'pinned': false, - }, - ); + Future unpinMessage(String messageId) => partialUpdateMessage( + messageId, + set: { + 'pinned': false, + }, + ); /// Get OpenGraph data of the given [url]. - Future enrichUrl(String url) => - _chatApi.general.enrichUrl(url); + Future enrichUrl(String url) => _chatApi.general.enrichUrl(url); /// Queries threads with the given [options] and [pagination] params. /// @@ -1994,13 +2038,12 @@ class StreamChatClient { SortOrder? sort, ThreadOptions options = const ThreadOptions(), PaginationParams pagination = const PaginationParams(), - }) => - _chatApi.threads.queryThreads( - filter: filter, - sort: sort, - options: options, - pagination: pagination, - ); + }) => _chatApi.threads.queryThreads( + filter: filter, + sort: sort, + options: options, + pagination: pagination, + ); /// Retrieves a thread with the given [messageId]. /// @@ -2008,11 +2051,10 @@ class StreamChatClient { Future getThread( String messageId, { ThreadOptions options = const ThreadOptions(), - }) => - _chatApi.threads.getThread( - messageId, - options: options, - ); + }) => _chatApi.threads.getThread( + messageId, + options: options, + ); /// Partially updates the thread with the given [messageId]. /// @@ -2022,12 +2064,11 @@ class StreamChatClient { String messageId, { Map? set, List? unset, - }) => - _chatApi.threads.partialUpdateThread( - messageId, - set: set, - unset: unset, - ); + }) => _chatApi.threads.partialUpdateThread( + messageId, + set: set, + unset: unset, + ); /// Pins the channel for the current user. Future pinChannel({ @@ -2225,15 +2266,28 @@ class ClientState { }), ); + // region CHANNEL EVENTS _listenChannelLeft(); - _listenChannelDeleted(); - _listenChannelHidden(); + // endregion + // region USER EVENTS _listenUserUpdated(); + _listenUserMessagesDeleted(); + // endregion + // region READ EVENTS _listenAllChannelsRead(); + // endregion + + // region LOCATION EVENTS + _listenLocationShared(); + _listenLocationUpdated(); + _listenLocationExpired(); + // endregion + + _startCleaningExpiredLocations(); } /// Stops listening to the client events. @@ -2307,18 +2361,17 @@ class ClientState { _eventsSubscription?.add( _client .on( - EventType.memberRemoved, - EventType.notificationRemovedFromChannel, - ) + EventType.memberRemoved, + EventType.notificationRemovedFromChannel, + ) .listen((event) async { - final isCurrentUser = event.user!.id == currentUser!.id; - if (isCurrentUser && event.channel != null) { - final eventChannel = event.channel!; - await _client.chatPersistenceClient - ?.deleteChannels([eventChannel.cid]); - channels.remove(eventChannel.cid)?.dispose(); - } - }), + final isCurrentUser = event.user!.id == currentUser!.id; + if (isCurrentUser && event.channel != null) { + final eventChannel = event.channel!; + await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); + channels.remove(eventChannel.cid)?.dispose(); + } + }), ); } @@ -2326,17 +2379,132 @@ class ClientState { _eventsSubscription?.add( _client .on( - EventType.channelDeleted, - EventType.notificationChannelDeleted, - ) + EventType.channelDeleted, + EventType.notificationChannelDeleted, + ) .listen((Event event) async { - final eventChannel = event.channel!; - await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); - channels.remove(eventChannel.cid)?.dispose(); + final eventChannel = event.channel!; + await _client.chatPersistenceClient?.deleteChannels([eventChannel.cid]); + channels.remove(eventChannel.cid)?.dispose(); + }), + ); + } + + void _listenUserMessagesDeleted() { + _eventsSubscription?.add( + _client.on(EventType.userMessagesDeleted).listen((event) async { + final cid = event.cid; + // Only handle message deletions that are not channel specific + // (i.e. user banned globally from the app) + if (cid != null) return; + + // Iterate through all the available channels and send the event + // to be handled by the respective channel instances. + for (final cid in [...channels.keys]) { + final channelEvent = event.copyWith(cid: cid); + _client.handleEvent(channelEvent); + } + }), + ); + } + + 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 @@ -2371,6 +2539,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; @@ -2440,14 +2625,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..0bcd097499 --- /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 != true) 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 != true) 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/key_stroke_handler.dart b/packages/stream_chat/lib/src/client/key_stroke_handler.dart index 52878182fd..501c1f944d 100644 --- a/packages/stream_chat/lib/src/client/key_stroke_handler.dart +++ b/packages/stream_chat/lib/src/client/key_stroke_handler.dart @@ -87,13 +87,15 @@ class KeyStrokeHandler { _cancelKeyStrokeTimer(); _keyStrokeTimer = Timer(Duration(seconds: startTypingEventTimeout), () { - _stopTyping(parentId).then((_) { - if (completer.isCompleted) return; - completer.complete(); - }).onError((error, stackTrace) { - if (completer.isCompleted) return; - completer.completeError(error!, stackTrace); - }); + _stopTyping(parentId) + .then((_) { + if (completer.isCompleted) return; + completer.complete(); + }) + .onError((error, stackTrace) { + if (completer.isCompleted) return; + completer.completeError(error!, stackTrace); + }); }); // If the user is typing too long, it should call [onStartTyping] again. diff --git a/packages/stream_chat/lib/src/client/retry_policy.dart b/packages/stream_chat/lib/src/client/retry_policy.dart index b5d0804368..4c7d249e84 100644 --- a/packages/stream_chat/lib/src/client/retry_policy.dart +++ b/packages/stream_chat/lib/src/client/retry_policy.dart @@ -51,5 +51,6 @@ class RetryPolicy { StreamChatClient client, int attempt, StreamChatError? error, - ) shouldRetry; + ) + shouldRetry; } diff --git a/packages/stream_chat/lib/src/client/retry_queue.dart b/packages/stream_chat/lib/src/client/retry_queue.dart index 8675ee68b7..feb6b623f6 100644 --- a/packages/stream_chat/lib/src/client/retry_queue.dart +++ b/packages/stream_chat/lib/src/client/retry_queue.dart @@ -31,12 +31,16 @@ class RetryQueue { final _messageQueue = HeapPriorityQueue(_byDate); void _listenConnectionRecovered() { - client.on(EventType.connectionRecovered).distinct().listen((event) { - if (event.online == true) { - logger?.info('Connection recovered, retrying failed messages'); - channel.state?.retryFailedMessages(); - } - }).addTo(_compositeSubscription); + client + .on(EventType.connectionRecovered) + .distinct() + .listen((event) { + if (event.online == true) { + logger?.info('Connection recovered, retrying failed messages'); + channel.state?.retryFailedMessages(); + } + }) + .addTo(_compositeSubscription); } /// Add a list of messages. @@ -119,10 +123,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/attachment_file_uploader.dart b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart index c851d11472..9575dfa55d 100644 --- a/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart +++ b/packages/stream_chat/lib/src/core/api/attachment_file_uploader.dart @@ -4,9 +4,10 @@ import 'package:stream_chat/src/core/http/stream_http_client.dart'; import 'package:stream_chat/src/core/models/attachment_file.dart'; /// Signature for a function which provides instance of [AttachmentFileUploader] -typedef AttachmentFileUploaderProvider = AttachmentFileUploader Function( - StreamHttpClient httpClient, -); +typedef AttachmentFileUploaderProvider = + AttachmentFileUploader Function( + StreamHttpClient httpClient, + ); /// Class responsible for uploading images and files to a given channel abstract class AttachmentFileUploader { @@ -61,6 +62,54 @@ abstract class AttachmentFileUploader { CancelToken? cancelToken, Map? extraData, }); + + // region Standalone upload methods + + /// Uploads an image file to the CDN. + /// + /// Upload progress can be tracked using [onSendProgress], and the operation + /// can be cancelled using [cancelToken]. + /// + /// Returns a [UploadImageResponse] once uploaded successfully. + Future uploadImage( + AttachmentFile image, { + ProgressCallback? onSendProgress, + CancelToken? cancelToken, + }); + + /// Uploads a file to the CDN. + /// + /// Upload progress can be tracked using [onSendProgress], and the operation + /// can be cancelled using [cancelToken]. + /// + /// Returns a [UploadFileResponse] once uploaded successfully. + Future uploadFile( + AttachmentFile file, { + ProgressCallback? onSendProgress, + CancelToken? cancelToken, + }); + + /// Removes an image from the CDN using its [url]. + /// + /// The operation can be cancelled using [cancelToken] if needed. + /// + /// Returns a [EmptyResponse] once removed successfully. + Future removeImage( + String url, { + CancelToken? cancelToken, + }); + + /// Removes a file from the CDN using its [url]. + /// + /// The operation can be cancelled using [cancelToken] if needed. + /// + /// Returns a [EmptyResponse] once removed successfully. + Future removeFile( + String url, { + CancelToken? cancelToken, + }); + + // endregion } /// Stream's default implementation of [AttachmentFileUploader] @@ -139,4 +188,62 @@ class StreamAttachmentFileUploader implements AttachmentFileUploader { ); return EmptyResponse.fromJson(response.data); } + + @override + Future uploadImage( + AttachmentFile image, { + ProgressCallback? onSendProgress, + CancelToken? cancelToken, + }) async { + final multiPartFile = await image.toMultipartFile(); + final response = await _client.postFile( + '/uploads/image', + multiPartFile, + onSendProgress: onSendProgress, + cancelToken: cancelToken, + ); + return UploadImageResponse.fromJson(response.data); + } + + @override + Future uploadFile( + AttachmentFile file, { + ProgressCallback? onSendProgress, + CancelToken? cancelToken, + }) async { + final multiPartFile = await file.toMultipartFile(); + final response = await _client.postFile( + '/uploads/file', + multiPartFile, + onSendProgress: onSendProgress, + cancelToken: cancelToken, + ); + return UploadFileResponse.fromJson(response.data); + } + + @override + Future removeImage( + String url, { + CancelToken? cancelToken, + }) async { + final response = await _client.delete( + '/uploads/image', + queryParameters: {'url': url}, + cancelToken: cancelToken, + ); + return EmptyResponse.fromJson(response.data); + } + + @override + Future removeFile( + String url, { + CancelToken? cancelToken, + }) async { + final response = await _client.delete( + '/uploads/file', + queryParameters: {'url': url}, + cancelToken: cancelToken, + ); + return EmptyResponse.fromJson(response.data); + } } 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/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 77c2425a55..b2addba7d6 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -17,8 +17,7 @@ class ChannelApi { final StreamHttpClient _client; - String _getChannelUrl(String channelId, String channelType) => - '/channels/$channelType/$channelId'; + String _getChannelUrl(String channelId, String channelType) => '/channels/$channelType/$channelId'; /// Query the API, get messages, members or other channel fields Future queryChannel( @@ -100,8 +99,7 @@ class ChannelApi { _getChannelUrl(channelId, channelType), data: { 'data': data, - if (message != null) - 'message': message.copyWith(updatedAt: DateTime.now()), + if (message != null) 'message': message.copyWith(updatedAt: DateTime.now()), }, ); return UpdateChannelResponse.fromJson(response.data); diff --git a/packages/stream_chat/lib/src/core/api/device_api.dart b/packages/stream_chat/lib/src/core/api/device_api.dart index b450ddc18e..1d75994036 100644 --- a/packages/stream_chat/lib/src/core/api/device_api.dart +++ b/packages/stream_chat/lib/src/core/api/device_api.dart @@ -37,8 +37,7 @@ class DeviceApi { data: { 'id': deviceId, 'push_provider': pushProvider.name, - if (pushProviderName != null && pushProviderName.isNotEmpty) - 'push_provider_name': pushProviderName, + if (pushProviderName != null && pushProviderName.isNotEmpty) 'push_provider_name': pushProviderName, }, ); return EmptyResponse.fromJson(response.data); diff --git a/packages/stream_chat/lib/src/core/api/general_api.dart b/packages/stream_chat/lib/src/core/api/general_api.dart index c364674463..16b4bdb9f8 100644 --- a/packages/stream_chat/lib/src/core/api/general_api.dart +++ b/packages/stream_chat/lib/src/core/api/general_api.dart @@ -60,8 +60,7 @@ class GeneralApi { 'filter_conditions': filter, if (sort != null) 'sort': sort, if (query != null) 'query': query, - if (messageFilters != null) - 'message_filter_conditions': messageFilters, + if (messageFilters != null) 'message_filter_conditions': messageFilters, if (pagination != null) ...pagination.toJson(), }), }, @@ -85,10 +84,7 @@ class GeneralApi { 'payload': jsonEncode({ 'type': channelType, 'filter_conditions': filter ?? {}, - if (channelId != null) - 'id': channelId - else if (members != null) - 'members': members, + if (channelId != null) 'id': channelId else if (members != null) 'members': members, if (sort != null) 'sort': sort, if (pagination != null) ...pagination.toJson(), }), 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..5f3fe356cf 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 { @@ -174,14 +175,20 @@ class MessageApi { Future deleteMessage( String messageId, { bool? hard, + bool? deleteForMe, }) async { + if (hard == true && deleteForMe == true) { + throw ArgumentError( + 'Both hard and deleteForMe cannot be set at the same time.', + ); + } + final response = await _client.delete( '/messages/$messageId', - queryParameters: hard != null - ? { - 'hard': hard, - } - : null, + queryParameters: { + if (hard != null) 'hard': hard, + if (deleteForMe != null) 'delete_for_me': deleteForMe, + }, ); return EmptyResponse.fromJson(response.data); } @@ -210,19 +217,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); } @@ -252,6 +257,28 @@ class MessageApi { return QueryReactionsResponse.fromJson(response.data); } + /// Queries reactions for a [messageId] with optional [filter], [sort], + /// and [pagination]. + /// + /// Unlike [getReactions], this method supports filtering by reaction type, + /// user ID, or creation date, sorting, and cursor-based pagination. + Future queryReactions( + String messageId, { + Filter? filter, + SortOrder? sort, + PaginationParams? pagination, + }) async { + final response = await _client.post( + '/messages/$messageId/reactions', + data: jsonEncode({ + if (filter != null) 'filter': filter.toJson(), + if (sort != null) 'sort': sort.map((e) => e.toJson()).toList(), + if (pagination != null) ...pagination.toJson(), + }), + ); + return QueryReactionsResponse.fromJson(response.data); + } + /// Translates the [messageId] in provided [language] Future translateMessage( String messageId, diff --git a/packages/stream_chat/lib/src/core/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart index 0b5a704608..c3953e3233 100644 --- a/packages/stream_chat/lib/src/core/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -31,13 +31,12 @@ class PaginationParams extends Equatable { this.createdAtBefore, this.createdAtAround, }) : assert( - offset == null || offset == 0 || next == null, - 'Cannot specify non-zero `offset` with `next` parameter', - ); + offset == null || offset == 0 || next == null, + 'Cannot specify non-zero `offset` with `next` parameter', + ); /// Create a new instance from a json - factory PaginationParams.fromJson(Map json) => - _$PaginationParamsFromJson(json); + factory PaginationParams.fromJson(Map json) => _$PaginationParamsFromJson(json); /// The amount of items requested from the APIs. final int limit; @@ -108,41 +107,38 @@ class PaginationParams extends Equatable { DateTime? createdAtBeforeOrEqual, DateTime? createdAtBefore, DateTime? createdAtAround, - }) => - PaginationParams( - limit: limit ?? this.limit, - offset: offset ?? this.offset, - idAround: idAround ?? this.idAround, - next: next ?? this.next, - greaterThan: greaterThan ?? this.greaterThan, - greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual, - lessThan: lessThan ?? this.lessThan, - lessThanOrEqual: lessThanOrEqual ?? this.lessThanOrEqual, - createdAtAfterOrEqual: - createdAtAfterOrEqual ?? this.createdAtAfterOrEqual, - createdAtAfter: createdAtAfter ?? this.createdAtAfter, - createdAtBeforeOrEqual: - createdAtBeforeOrEqual ?? this.createdAtBeforeOrEqual, - createdAtBefore: createdAtBefore ?? this.createdAtBefore, - createdAtAround: createdAtAround ?? this.createdAtAround, - ); + }) => PaginationParams( + limit: limit ?? this.limit, + offset: offset ?? this.offset, + idAround: idAround ?? this.idAround, + next: next ?? this.next, + greaterThan: greaterThan ?? this.greaterThan, + greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual, + lessThan: lessThan ?? this.lessThan, + lessThanOrEqual: lessThanOrEqual ?? this.lessThanOrEqual, + createdAtAfterOrEqual: createdAtAfterOrEqual ?? this.createdAtAfterOrEqual, + createdAtAfter: createdAtAfter ?? this.createdAtAfter, + createdAtBeforeOrEqual: createdAtBeforeOrEqual ?? this.createdAtBeforeOrEqual, + createdAtBefore: createdAtBefore ?? this.createdAtBefore, + createdAtAround: createdAtAround ?? this.createdAtAround, + ); @override List get props => [ - limit, - offset, - next, - idAround, - greaterThan, - greaterThanOrEqual, - lessThan, - lessThanOrEqual, - createdAtAfterOrEqual, - createdAtAfter, - createdAtBeforeOrEqual, - createdAtBefore, - createdAtAround, - ]; + limit, + offset, + next, + idAround, + greaterThan, + greaterThanOrEqual, + lessThan, + lessThanOrEqual, + createdAtAfterOrEqual, + createdAtAfter, + createdAtBeforeOrEqual, + createdAtBefore, + createdAtAround, + ]; } /// Request model for the [client.partialUpdateUser] api call. diff --git a/packages/stream_chat/lib/src/core/api/requests.g.dart b/packages/stream_chat/lib/src/core/api/requests.g.dart index 7a77503534..b04bb02430 100644 --- a/packages/stream_chat/lib/src/core/api/requests.g.dart +++ b/packages/stream_chat/lib/src/core/api/requests.g.dart @@ -6,80 +6,62 @@ part of 'requests.dart'; // JsonSerializableGenerator // ************************************************************************** -PaginationParams _$PaginationParamsFromJson(Map json) => - PaginationParams( - limit: (json['limit'] as num?)?.toInt() ?? 10, - offset: (json['offset'] as num?)?.toInt(), - next: json['next'] as String?, - idAround: json['id_around'] as String?, - greaterThan: json['id_gt'] as String?, - greaterThanOrEqual: json['id_gte'] as String?, - lessThan: json['id_lt'] as String?, - lessThanOrEqual: json['id_lte'] as String?, - createdAtAfterOrEqual: json['created_at_after_or_equal'] == null - ? null - : DateTime.parse(json['created_at_after_or_equal'] as String), - createdAtAfter: json['created_at_after'] == null - ? null - : DateTime.parse(json['created_at_after'] as String), - createdAtBeforeOrEqual: json['created_at_before_or_equal'] == null - ? null - : DateTime.parse(json['created_at_before_or_equal'] as String), - createdAtBefore: json['created_at_before'] == null - ? null - : DateTime.parse(json['created_at_before'] as String), - createdAtAround: json['created_at_around'] == null - ? null - : DateTime.parse(json['created_at_around'] as String), - ); +PaginationParams _$PaginationParamsFromJson(Map json) => PaginationParams( + limit: (json['limit'] as num?)?.toInt() ?? 10, + offset: (json['offset'] as num?)?.toInt(), + next: json['next'] as String?, + idAround: json['id_around'] as String?, + greaterThan: json['id_gt'] as String?, + greaterThanOrEqual: json['id_gte'] as String?, + lessThan: json['id_lt'] as String?, + lessThanOrEqual: json['id_lte'] as String?, + createdAtAfterOrEqual: json['created_at_after_or_equal'] == null + ? null + : DateTime.parse(json['created_at_after_or_equal'] as String), + createdAtAfter: json['created_at_after'] == null ? null : DateTime.parse(json['created_at_after'] as String), + createdAtBeforeOrEqual: json['created_at_before_or_equal'] == null + ? null + : DateTime.parse(json['created_at_before_or_equal'] as String), + createdAtBefore: json['created_at_before'] == null ? null : DateTime.parse(json['created_at_before'] as String), + createdAtAround: json['created_at_around'] == null ? null : DateTime.parse(json['created_at_around'] as String), +); -Map _$PaginationParamsToJson(PaginationParams instance) => - { - 'limit': instance.limit, - if (instance.offset case final value?) 'offset': value, - if (instance.next case final value?) 'next': value, - if (instance.idAround case final value?) 'id_around': value, - if (instance.greaterThan case final value?) 'id_gt': value, - if (instance.greaterThanOrEqual case final value?) 'id_gte': value, - if (instance.lessThan case final value?) 'id_lt': value, - if (instance.lessThanOrEqual case final value?) 'id_lte': value, - if (instance.createdAtAfterOrEqual?.toIso8601String() case final value?) - 'created_at_after_or_equal': value, - if (instance.createdAtAfter?.toIso8601String() case final value?) - 'created_at_after': value, - if (instance.createdAtBeforeOrEqual?.toIso8601String() case final value?) - 'created_at_before_or_equal': value, - if (instance.createdAtBefore?.toIso8601String() case final value?) - 'created_at_before': value, - if (instance.createdAtAround?.toIso8601String() case final value?) - 'created_at_around': value, - }; +Map _$PaginationParamsToJson(PaginationParams instance) => { + 'limit': instance.limit, + if (instance.offset case final value?) 'offset': value, + if (instance.next case final value?) 'next': value, + if (instance.idAround case final value?) 'id_around': value, + if (instance.greaterThan case final value?) 'id_gt': value, + if (instance.greaterThanOrEqual case final value?) 'id_gte': value, + if (instance.lessThan case final value?) 'id_lt': value, + if (instance.lessThanOrEqual case final value?) 'id_lte': value, + if (instance.createdAtAfterOrEqual?.toIso8601String() case final value?) 'created_at_after_or_equal': value, + if (instance.createdAtAfter?.toIso8601String() case final value?) 'created_at_after': value, + if (instance.createdAtBeforeOrEqual?.toIso8601String() case final value?) 'created_at_before_or_equal': value, + if (instance.createdAtBefore?.toIso8601String() case final value?) 'created_at_before': value, + if (instance.createdAtAround?.toIso8601String() case final value?) 'created_at_around': value, +}; -Map _$PartialUpdateUserRequestToJson( - PartialUpdateUserRequest instance) => - { - 'stringify': instance.stringify, - 'hash_code': instance.hashCode, - 'id': instance.id, - 'set': instance.set, - 'unset': instance.unset, - 'props': instance.props, - }; +Map _$PartialUpdateUserRequestToJson(PartialUpdateUserRequest instance) => { + 'stringify': instance.stringify, + 'hash_code': instance.hashCode, + 'id': instance.id, + 'set': instance.set, + 'unset': instance.unset, + 'props': instance.props, +}; -Map _$ThreadOptionsToJson(ThreadOptions instance) => - { - 'stringify': instance.stringify, - 'hash_code': instance.hashCode, - 'watch': instance.watch, - 'reply_limit': instance.replyLimit, - 'participant_limit': instance.participantLimit, - 'member_limit': instance.memberLimit, - 'props': instance.props, - }; +Map _$ThreadOptionsToJson(ThreadOptions instance) => { + 'stringify': instance.stringify, + 'hash_code': instance.hashCode, + 'watch': instance.watch, + 'reply_limit': instance.replyLimit, + 'participant_limit': instance.participantLimit, + 'member_limit': instance.memberLimit, + 'props': instance.props, +}; -Map _$MemberUpdatePayloadToJson( - MemberUpdatePayload instance) => - { - if (instance.archived case final value?) 'archived': value, - if (instance.pinned case final value?) 'pinned': value, - }; +Map _$MemberUpdatePayloadToJson(MemberUpdatePayload instance) => { + if (instance.archived case final value?) 'archived': value, + if (instance.pinned case final value?) 'pinned': value, +}; diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index cbd4505678..c5bd2e92ab 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'; @@ -46,14 +45,14 @@ class ErrorResponse extends _BaseResponse { String? moreInfo; /// Create a new instance from a json - static ErrorResponse fromJson(Map json) => - _$ErrorResponseFromJson(json); + static ErrorResponse fromJson(Map json) => _$ErrorResponseFromJson(json); /// Serialize to json Map toJson() => _$ErrorResponseToJson(this); @override - String toString() => 'ErrorResponse(code: $code, ' + String toString() => + 'ErrorResponse(code: $code, ' 'message: $message, ' 'statusCode: $statusCode, ' 'moreInfo: $moreInfo)'; @@ -67,8 +66,7 @@ class SyncResponse extends _BaseResponse { late List events; /// Create a new instance from a json - static SyncResponse fromJson(Map json) => - _$SyncResponseFromJson(json); + static SyncResponse fromJson(Map json) => _$SyncResponseFromJson(json); } /// Model response for [StreamChatClient.queryChannels] api call @@ -79,16 +77,14 @@ class QueryChannelsResponse extends _BaseResponse { late List channels; /// Create a new instance from a json - static QueryChannelsResponse fromJson(Map json) => - _$QueryChannelsResponseFromJson(json); + static QueryChannelsResponse fromJson(Map json) => _$QueryChannelsResponseFromJson(json); } /// Model response for [StreamChatClient.queryChannels] api call @JsonSerializable(createToJson: false) class TranslateMessageResponse extends MessageResponse { /// Create a new instance from a json - static TranslateMessageResponse fromJson(Map json) => - _$TranslateMessageResponseFromJson(json); + static TranslateMessageResponse fromJson(Map json) => _$TranslateMessageResponseFromJson(json); } /// Model response for [StreamChatClient.queryChannels] api call @@ -99,8 +95,7 @@ class QueryMembersResponse extends _BaseResponse { late List members; /// Create a new instance from a json - static QueryMembersResponse fromJson(Map json) => - _$QueryMembersResponseFromJson(json); + static QueryMembersResponse fromJson(Map json) => _$QueryMembersResponseFromJson(json); } /// Model response for update member API calls, such as @@ -111,8 +106,7 @@ class PartialUpdateMemberResponse extends _BaseResponse { late Member channelMember; /// Create a new instance from a json - static PartialUpdateMemberResponse fromJson(Map json) => - _$PartialUpdateMemberResponseFromJson(json); + static PartialUpdateMemberResponse fromJson(Map json) => _$PartialUpdateMemberResponseFromJson(json); } /// Model response for [StreamChatClient.queryUsers] api call @@ -123,8 +117,7 @@ class QueryUsersResponse extends _BaseResponse { late List users; /// Create a new instance from a json - static QueryUsersResponse fromJson(Map json) => - _$QueryUsersResponseFromJson(json); + static QueryUsersResponse fromJson(Map json) => _$QueryUsersResponseFromJson(json); } /// Model response for [StreamChatClient.queryBannedUsers] api call @@ -135,20 +128,23 @@ class QueryBannedUsersResponse extends _BaseResponse { late List bans; /// Create a new instance from a json - static QueryBannedUsersResponse fromJson(Map json) => - _$QueryBannedUsersResponseFromJson(json); + static QueryBannedUsersResponse fromJson(Map json) => _$QueryBannedUsersResponseFromJson(json); } -/// Model response for [channel.getReactions] api call +/// Model response for [channel.getReactions] or [channel.queryReactions] api call @JsonSerializable(createToJson: false) class QueryReactionsResponse extends _BaseResponse { /// List of reactions returned by the query @JsonKey(defaultValue: []) late List reactions; + /// The cursor for the next page of results. + /// + /// Will be `null` if there are no more results. + late String? next; + /// Create a new instance from a json - static QueryReactionsResponse fromJson(Map json) => - _$QueryReactionsResponseFromJson(json); + static QueryReactionsResponse fromJson(Map json) => _$QueryReactionsResponseFromJson(json); } /// Model response for [Channel.getReplies] api call @@ -159,8 +155,7 @@ class QueryRepliesResponse extends _BaseResponse { late List messages; /// Create a new instance from a json - static QueryRepliesResponse fromJson(Map json) => - _$QueryRepliesResponseFromJson(json); + static QueryRepliesResponse fromJson(Map json) => _$QueryRepliesResponseFromJson(json); } /// Model response for [StreamChatClient.getDevices] api call @@ -171,8 +166,7 @@ class ListDevicesResponse extends _BaseResponse { late List devices; /// Create a new instance from a json - static ListDevicesResponse fromJson(Map json) => - _$ListDevicesResponseFromJson(json); + static ListDevicesResponse fromJson(Map json) => _$ListDevicesResponseFromJson(json); } /// Base Model response for [Channel.sendImage] and [Channel.sendFile] api call. @@ -182,8 +176,7 @@ class SendAttachmentResponse extends _BaseResponse { late String? file; /// Create a new instance from a json - static SendAttachmentResponse fromJson(Map json) => - _$SendAttachmentResponseFromJson(json); + static SendAttachmentResponse fromJson(Map json) => _$SendAttachmentResponseFromJson(json); } /// Model response for [Channel.sendFile] api call @@ -195,13 +188,18 @@ class SendFileResponse extends SendAttachmentResponse { String? thumbUrl; /// Create a new instance from a json - static SendFileResponse fromJson(Map json) => - _$SendFileResponseFromJson(json); + static SendFileResponse fromJson(Map json) => _$SendFileResponseFromJson(json); } /// Model response for [Channel.sendImage] api call typedef SendImageResponse = SendAttachmentResponse; +/// Model response for [StreamChatClient.uploadImage] api call +typedef UploadImageResponse = SendAttachmentResponse; + +/// Model response for [StreamChatClient.uploadFile] api call +typedef UploadFileResponse = SendAttachmentResponse; + /// Model response for [Channel.sendReaction] api call @JsonSerializable(createToJson: false) class SendReactionResponse extends MessageResponse { @@ -209,8 +207,7 @@ class SendReactionResponse extends MessageResponse { late Reaction reaction; /// Create a new instance from a json - static SendReactionResponse fromJson(Map json) => - _$SendReactionResponseFromJson(json); + static SendReactionResponse fromJson(Map json) => _$SendReactionResponseFromJson(json); } /// Model response for [StreamChatClient.connectGuestUser] api call @@ -223,8 +220,7 @@ class ConnectGuestUserResponse extends _BaseResponse { late User user; /// Create a new instance from a json - static ConnectGuestUserResponse fromJson(Map json) => - _$ConnectGuestUserResponseFromJson(json); + static ConnectGuestUserResponse fromJson(Map json) => _$ConnectGuestUserResponseFromJson(json); } /// Model response for [StreamChatClient.updateUser] api call @@ -235,8 +231,7 @@ class UpdateUsersResponse extends _BaseResponse { late Map users; /// Create a new instance from a json - static UpdateUsersResponse fromJson(Map json) => - _$UpdateUsersResponseFromJson(json); + static UpdateUsersResponse fromJson(Map json) => _$UpdateUsersResponseFromJson(json); } /// Base Model response for message based api calls. @@ -249,16 +244,14 @@ class MessageResponse extends _BaseResponse { @JsonSerializable(createToJson: false) class UpdateMessageResponse extends MessageResponse { /// Create a new instance from a json - static UpdateMessageResponse fromJson(Map json) => - _$UpdateMessageResponseFromJson(json); + static UpdateMessageResponse fromJson(Map json) => _$UpdateMessageResponseFromJson(json); } /// Model response for [Channel.sendMessage] api call @JsonSerializable(createToJson: false) class SendMessageResponse extends MessageResponse { /// Create a new instance from a json - static SendMessageResponse fromJson(Map json) => - _$SendMessageResponseFromJson(json); + static SendMessageResponse fromJson(Map json) => _$SendMessageResponseFromJson(json); } /// Model response for [StreamChatClient.getMessage] api call @@ -292,8 +285,7 @@ class SearchMessagesResponse extends _BaseResponse { late String? previous; /// Create a new instance from a json - static SearchMessagesResponse fromJson(Map json) => - _$SearchMessagesResponseFromJson(json); + static SearchMessagesResponse fromJson(Map json) => _$SearchMessagesResponseFromJson(json); } /// Model response for [Channel.getMessagesById] api call @@ -304,8 +296,7 @@ class GetMessagesByIdResponse extends _BaseResponse { late List messages; /// Create a new instance from a json - static GetMessagesByIdResponse fromJson(Map json) => - _$GetMessagesByIdResponseFromJson(json); + static GetMessagesByIdResponse fromJson(Map json) => _$GetMessagesByIdResponseFromJson(json); } /// Model response for [Channel.update] api call @@ -321,8 +312,7 @@ class UpdateChannelResponse extends _BaseResponse { Message? message; /// Create a new instance from a json - static UpdateChannelResponse fromJson(Map json) => - _$UpdateChannelResponseFromJson(json); + static UpdateChannelResponse fromJson(Map json) => _$UpdateChannelResponseFromJson(json); } /// Model response for [Channel.updatePartial] api call @@ -353,8 +343,7 @@ class InviteMembersResponse extends _BaseResponse { Message? message; /// Create a new instance from a json - static InviteMembersResponse fromJson(Map json) => - _$InviteMembersResponseFromJson(json); + static InviteMembersResponse fromJson(Map json) => _$InviteMembersResponseFromJson(json); } /// Model response for [Channel.removeMembers] api call @@ -371,8 +360,7 @@ class RemoveMembersResponse extends _BaseResponse { Message? message; /// Create a new instance from a json - static RemoveMembersResponse fromJson(Map json) => - _$RemoveMembersResponseFromJson(json); + static RemoveMembersResponse fromJson(Map json) => _$RemoveMembersResponseFromJson(json); } /// Model response for [Channel.sendAction] api call @@ -382,8 +370,7 @@ class SendActionResponse extends _BaseResponse { Message? message; /// Create a new instance from a json - static SendActionResponse fromJson(Map json) => - _$SendActionResponseFromJson(json); + static SendActionResponse fromJson(Map json) => _$SendActionResponseFromJson(json); } /// Model response for [Channel.addMembers] api call @@ -400,8 +387,7 @@ class AddMembersResponse extends _BaseResponse { Message? message; /// Create a new instance from a json - static AddMembersResponse fromJson(Map json) => - _$AddMembersResponseFromJson(json); + static AddMembersResponse fromJson(Map json) => _$AddMembersResponseFromJson(json); } /// Model response for [Channel.acceptInvite] api call @@ -418,8 +404,7 @@ class AcceptInviteResponse extends _BaseResponse { Message? message; /// Create a new instance from a json - static AcceptInviteResponse fromJson(Map json) => - _$AcceptInviteResponseFromJson(json); + static AcceptInviteResponse fromJson(Map json) => _$AcceptInviteResponseFromJson(json); } /// Model response for [Channel.rejectInvite] api call @@ -436,16 +421,14 @@ class RejectInviteResponse extends _BaseResponse { Message? message; /// Create a new instance from a json - static RejectInviteResponse fromJson(Map json) => - _$RejectInviteResponseFromJson(json); + static RejectInviteResponse fromJson(Map json) => _$RejectInviteResponseFromJson(json); } /// Model response for empty responses @JsonSerializable(createToJson: false) class EmptyResponse extends _BaseResponse { /// Create a new instance from a json - static EmptyResponse fromJson(Map json) => - _$EmptyResponseFromJson(json); + static EmptyResponse fromJson(Map json) => _$EmptyResponseFromJson(json); } /// Model response for [Channel.query] api call @@ -471,8 +454,7 @@ class ChannelStateResponse extends _BaseResponse { late List read; /// Create a new instance from a json - static ChannelStateResponse fromJson(Map json) => - _$ChannelStateResponseFromJson(json); + static ChannelStateResponse fromJson(Map json) => _$ChannelStateResponseFromJson(json); } /// Model response for [Client.enrichUrl] api call. @@ -511,38 +493,7 @@ class OGAttachmentResponse extends _BaseResponse { String? type; /// Create a new instance from a [json]. - static OGAttachmentResponse fromJson(Map json) => - _$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; + static OGAttachmentResponse fromJson(Map json) => _$OGAttachmentResponseFromJson(json); } /// Contains information about a [User] that was banned from a [Channel] or App. @@ -560,8 +511,7 @@ class UserBlockResponse extends _BaseResponse { late DateTime createdAt; /// Create a new instance from a json - static UserBlockResponse fromJson(Map json) => - _$UserBlockResponseFromJson(json); + static UserBlockResponse fromJson(Map json) => _$UserBlockResponseFromJson(json); } /// Model response for [StreamChatClient.queryBlockedUsers] api call @@ -572,8 +522,7 @@ class BlockedUsersResponse extends _BaseResponse { late List blocks; /// Create a new instance from a json - static BlockedUsersResponse fromJson(Map json) => - _$BlockedUsersResponseFromJson(json); + static BlockedUsersResponse fromJson(Map json) => _$BlockedUsersResponseFromJson(json); } /// Model response for [StreamChatClient.createPoll] api call @@ -583,8 +532,7 @@ class CreatePollResponse extends _BaseResponse { late Poll poll; /// Create a new instance from a json - static CreatePollResponse fromJson(Map json) => - _$CreatePollResponseFromJson(json); + static CreatePollResponse fromJson(Map json) => _$CreatePollResponseFromJson(json); } /// Model response for [StreamChatClient.getPoll] api call @@ -594,8 +542,7 @@ class GetPollResponse extends _BaseResponse { late Poll poll; /// Create a new instance from a json - static GetPollResponse fromJson(Map json) => - _$GetPollResponseFromJson(json); + static GetPollResponse fromJson(Map json) => _$GetPollResponseFromJson(json); } /// Model response for [StreamChatClient.updatePoll] api call @@ -605,8 +552,7 @@ class UpdatePollResponse extends _BaseResponse { late Poll poll; /// Create a new instance from a json - static UpdatePollResponse fromJson(Map json) => - _$UpdatePollResponseFromJson(json); + static UpdatePollResponse fromJson(Map json) => _$UpdatePollResponseFromJson(json); } /// Model response for [StreamChatClient.createPollOption] api call @@ -616,8 +562,7 @@ class CreatePollOptionResponse extends _BaseResponse { late PollOption pollOption; /// Create a new instance from a json - static CreatePollOptionResponse fromJson(Map json) => - _$CreatePollOptionResponseFromJson(json); + static CreatePollOptionResponse fromJson(Map json) => _$CreatePollOptionResponseFromJson(json); } /// Model response for [StreamChatClient.getPollOption] api call @@ -627,8 +572,7 @@ class GetPollOptionResponse extends _BaseResponse { late PollOption pollOption; /// Create a new instance from a json - static GetPollOptionResponse fromJson(Map json) => - _$GetPollOptionResponseFromJson(json); + static GetPollOptionResponse fromJson(Map json) => _$GetPollOptionResponseFromJson(json); } /// Model response for [StreamChatClient.updatePollOption] api call @@ -638,8 +582,7 @@ class UpdatePollOptionResponse extends _BaseResponse { late PollOption pollOption; /// Create a new instance from a json - static UpdatePollOptionResponse fromJson(Map json) => - _$UpdatePollOptionResponseFromJson(json); + static UpdatePollOptionResponse fromJson(Map json) => _$UpdatePollOptionResponseFromJson(json); } /// Model response for [StreamChatClient.castPollVote] api call @@ -649,8 +592,7 @@ class CastPollVoteResponse extends _BaseResponse { late PollVote vote; /// Create a new instance from a json - static CastPollVoteResponse fromJson(Map json) => - _$CastPollVoteResponseFromJson(json); + static CastPollVoteResponse fromJson(Map json) => _$CastPollVoteResponseFromJson(json); } /// Model response for [StreamChatClient.removePollVote] api call @@ -660,8 +602,7 @@ class RemovePollVoteResponse extends EmptyResponse { late PollVote vote; /// Create a new instance from a json - static RemovePollVoteResponse fromJson(Map json) => - _$RemovePollVoteResponseFromJson(json); + static RemovePollVoteResponse fromJson(Map json) => _$RemovePollVoteResponseFromJson(json); } /// Model response for [StreamChatClient.queryPolls] api call @@ -675,8 +616,7 @@ class QueryPollsResponse extends _BaseResponse { late String? next; /// Create a new instance from a json - static QueryPollsResponse fromJson(Map json) => - _$QueryPollsResponseFromJson(json); + static QueryPollsResponse fromJson(Map json) => _$QueryPollsResponseFromJson(json); } /// Model response for [StreamChatClient.queryPollVotes] api call @@ -690,8 +630,7 @@ class QueryPollVotesResponse extends _BaseResponse { late String? next; /// Create a new instance from a json - static QueryPollVotesResponse fromJson(Map json) => - _$QueryPollVotesResponseFromJson(json); + static QueryPollVotesResponse fromJson(Map json) => _$QueryPollVotesResponseFromJson(json); } /// Model response for [StreamChatClient.getThread] api call @@ -701,8 +640,7 @@ class GetThreadResponse extends _BaseResponse { late Thread thread; /// Create a new instance from a json - static GetThreadResponse fromJson(Map json) => - _$GetThreadResponseFromJson(json); + static GetThreadResponse fromJson(Map json) => _$GetThreadResponseFromJson(json); } /// Model response for [StreamChatClient.updateThread] api call @@ -712,8 +650,7 @@ class UpdateThreadResponse extends _BaseResponse { late Thread thread; /// Create a new instance from a json - static UpdateThreadResponse fromJson(Map json) => - _$UpdateThreadResponseFromJson(json); + static UpdateThreadResponse fromJson(Map json) => _$UpdateThreadResponseFromJson(json); } /// Model response for [StreamChatClient.queryThreads] api call @@ -727,8 +664,7 @@ class QueryThreadsResponse extends _BaseResponse { late String? next; /// Create a new instance from a json - static QueryThreadsResponse fromJson(Map json) => - _$QueryThreadsResponseFromJson(json); + static QueryThreadsResponse fromJson(Map json) => _$QueryThreadsResponseFromJson(json); } /// Base Model response for draft based api calls. @@ -741,16 +677,14 @@ class DraftResponse extends _BaseResponse { @JsonSerializable(createToJson: false) class CreateDraftResponse extends DraftResponse { /// Create a new instance from a json - static CreateDraftResponse fromJson(Map json) => - _$CreateDraftResponseFromJson(json); + static CreateDraftResponse fromJson(Map json) => _$CreateDraftResponseFromJson(json); } /// Model response for [StreamChatClient.getDraft] api call @JsonSerializable(createToJson: false) class GetDraftResponse extends DraftResponse { /// Create a new instance from a json - static GetDraftResponse fromJson(Map json) => - _$GetDraftResponseFromJson(json); + static GetDraftResponse fromJson(Map json) => _$GetDraftResponseFromJson(json); } /// Model response for [StreamChatClient.queryDrafts] api call @@ -764,8 +698,7 @@ class QueryDraftsResponse extends _BaseResponse { late String? next; /// Create a new instance from a json - static QueryDraftsResponse fromJson(Map json) => - _$QueryDraftsResponseFromJson(json); + static QueryDraftsResponse fromJson(Map json) => _$QueryDraftsResponseFromJson(json); } /// Base Model response for draft based api calls. @@ -778,16 +711,14 @@ class MessageReminderResponse extends _BaseResponse { @JsonSerializable(createToJson: false) class CreateReminderResponse extends MessageReminderResponse { /// Create a new instance from a json - static CreateReminderResponse fromJson(Map json) => - _$CreateReminderResponseFromJson(json); + static CreateReminderResponse fromJson(Map json) => _$CreateReminderResponseFromJson(json); } /// Model response for [StreamChatClient.updateReminder] api call @JsonSerializable(createToJson: false) class UpdateReminderResponse extends MessageReminderResponse { /// Create a new instance from a json - static UpdateReminderResponse fromJson(Map json) => - _$UpdateReminderResponseFromJson(json); + static UpdateReminderResponse fromJson(Map json) => _$UpdateReminderResponseFromJson(json); } /// Model response for [StreamChatClient.queryReminders] api call @@ -801,8 +732,7 @@ class QueryRemindersResponse extends _BaseResponse { late String? next; /// Create a new instance from a json - static QueryRemindersResponse fromJson(Map json) => - _$QueryRemindersResponseFromJson(json); + static QueryRemindersResponse fromJson(Map json) => _$QueryRemindersResponseFromJson(json); } /// Model response for [StreamChatClient.getUnreadCount] api call @@ -827,8 +757,7 @@ class GetUnreadCountResponse extends _BaseResponse { late List threads; /// Create a new instance from a json - static GetUnreadCountResponse fromJson(Map json) => - _$GetUnreadCountResponseFromJson(json); + static GetUnreadCountResponse fromJson(Map json) => _$GetUnreadCountResponseFromJson(json); } /// Model response for [StreamChatClient.setPushPreferences] api call @@ -846,3 +775,14 @@ class UpsertPushPreferencesResponse extends _BaseResponse { static UpsertPushPreferencesResponse fromJson(Map json) => _$UpsertPushPreferencesResponseFromJson(json); } + +/// Model response for [StreamChatClient.getActiveLiveLocations] 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..4c54864009 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -6,509 +6,418 @@ part of 'responses.dart'; // JsonSerializableGenerator // ************************************************************************** -ErrorResponse _$ErrorResponseFromJson(Map json) => - ErrorResponse() - ..duration = json['duration'] as String? - ..code = (json['code'] as num?)?.toInt() - ..message = json['message'] as String? - ..statusCode = (json['StatusCode'] as num?)?.toInt() - ..moreInfo = json['more_info'] as String?; - -Map _$ErrorResponseToJson(ErrorResponse instance) => - { - 'duration': instance.duration, - 'code': instance.code, - 'message': instance.message, - 'StatusCode': instance.statusCode, - 'more_info': instance.moreInfo, - }; +ErrorResponse _$ErrorResponseFromJson(Map json) => ErrorResponse() + ..duration = json['duration'] as String? + ..code = (json['code'] as num?)?.toInt() + ..message = json['message'] as String? + ..statusCode = (json['StatusCode'] as num?)?.toInt() + ..moreInfo = json['more_info'] as String?; + +Map _$ErrorResponseToJson(ErrorResponse instance) => { + 'duration': instance.duration, + 'code': instance.code, + 'message': instance.message, + 'StatusCode': instance.statusCode, + 'more_info': instance.moreInfo, +}; SyncResponse _$SyncResponseFromJson(Map json) => SyncResponse() ..duration = json['duration'] as String? - ..events = (json['events'] as List?) - ?.map((e) => Event.fromJson(e as Map)) - .toList() ?? - []; + ..events = (json['events'] as List?)?.map((e) => Event.fromJson(e as Map)).toList() ?? []; QueryChannelsResponse _$QueryChannelsResponseFromJson( - Map json) => - QueryChannelsResponse() - ..duration = json['duration'] as String? - ..channels = (json['channels'] as List?) - ?.map((e) => ChannelState.fromJson(e as Map)) - .toList() ?? - []; + Map json, +) => QueryChannelsResponse() + ..duration = json['duration'] as String? + ..channels = + (json['channels'] as List?)?.map((e) => ChannelState.fromJson(e as Map)).toList() ?? []; TranslateMessageResponse _$TranslateMessageResponseFromJson( - Map json) => - TranslateMessageResponse() - ..duration = json['duration'] as String? - ..message = Message.fromJson(json['message'] as Map); + Map json, +) => TranslateMessageResponse() + ..duration = json['duration'] as String? + ..message = Message.fromJson(json['message'] as Map); QueryMembersResponse _$QueryMembersResponseFromJson( - Map json) => - QueryMembersResponse() - ..duration = json['duration'] as String? - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - []; + Map json, +) => QueryMembersResponse() + ..duration = json['duration'] as String? + ..members = + (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? []; PartialUpdateMemberResponse _$PartialUpdateMemberResponseFromJson( - Map json) => - PartialUpdateMemberResponse() - ..duration = json['duration'] as String? - ..channelMember = - Member.fromJson(json['channel_member'] as Map); - -QueryUsersResponse _$QueryUsersResponseFromJson(Map json) => - QueryUsersResponse() - ..duration = json['duration'] as String? - ..users = (json['users'] as List?) - ?.map((e) => User.fromJson(e as Map)) - .toList() ?? - []; + Map json, +) => PartialUpdateMemberResponse() + ..duration = json['duration'] as String? + ..channelMember = Member.fromJson( + json['channel_member'] as Map, + ); + +QueryUsersResponse _$QueryUsersResponseFromJson(Map json) => QueryUsersResponse() + ..duration = json['duration'] as String? + ..users = (json['users'] as List?)?.map((e) => User.fromJson(e as Map)).toList() ?? []; QueryBannedUsersResponse _$QueryBannedUsersResponseFromJson( - Map json) => - QueryBannedUsersResponse() - ..duration = json['duration'] as String? - ..bans = (json['bans'] as List?) - ?.map((e) => BannedUser.fromJson(e as Map)) - .toList() ?? - []; + Map json, +) => QueryBannedUsersResponse() + ..duration = json['duration'] as String? + ..bans = (json['bans'] as List?)?.map((e) => BannedUser.fromJson(e as Map)).toList() ?? []; QueryReactionsResponse _$QueryReactionsResponseFromJson( - Map json) => - QueryReactionsResponse() - ..duration = json['duration'] as String? - ..reactions = (json['reactions'] as List?) - ?.map((e) => Reaction.fromJson(e as Map)) - .toList() ?? - []; + Map json, +) => QueryReactionsResponse() + ..duration = json['duration'] as String? + ..reactions = + (json['reactions'] as List?)?.map((e) => Reaction.fromJson(e as Map)).toList() ?? [] + ..next = json['next'] as String?; QueryRepliesResponse _$QueryRepliesResponseFromJson( - Map json) => - QueryRepliesResponse() - ..duration = json['duration'] as String? - ..messages = (json['messages'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList() ?? - []; - -ListDevicesResponse _$ListDevicesResponseFromJson(Map json) => - ListDevicesResponse() - ..duration = json['duration'] as String? - ..devices = (json['devices'] as List?) - ?.map((e) => Device.fromJson(e as Map)) - .toList() ?? - []; + Map json, +) => QueryRepliesResponse() + ..duration = json['duration'] as String? + ..messages = + (json['messages'] as List?)?.map((e) => Message.fromJson(e as Map)).toList() ?? []; + +ListDevicesResponse _$ListDevicesResponseFromJson(Map json) => ListDevicesResponse() + ..duration = json['duration'] as String? + ..devices = + (json['devices'] as List?)?.map((e) => Device.fromJson(e as Map)).toList() ?? []; SendAttachmentResponse _$SendAttachmentResponseFromJson( - Map json) => - SendAttachmentResponse() - ..duration = json['duration'] as String? - ..file = json['file'] as String?; + Map json, +) => SendAttachmentResponse() + ..duration = json['duration'] as String? + ..file = json['file'] as String?; -SendFileResponse _$SendFileResponseFromJson(Map json) => - SendFileResponse() - ..duration = json['duration'] as String? - ..file = json['file'] as String? - ..thumbUrl = json['thumb_url'] as String?; +SendFileResponse _$SendFileResponseFromJson(Map json) => SendFileResponse() + ..duration = json['duration'] as String? + ..file = json['file'] as String? + ..thumbUrl = json['thumb_url'] as String?; SendReactionResponse _$SendReactionResponseFromJson( - Map json) => - SendReactionResponse() - ..duration = json['duration'] as String? - ..message = Message.fromJson(json['message'] as Map) - ..reaction = Reaction.fromJson(json['reaction'] as Map); + Map json, +) => SendReactionResponse() + ..duration = json['duration'] as String? + ..message = Message.fromJson(json['message'] as Map) + ..reaction = Reaction.fromJson(json['reaction'] as Map); ConnectGuestUserResponse _$ConnectGuestUserResponseFromJson( - Map json) => - ConnectGuestUserResponse() - ..duration = json['duration'] as String? - ..accessToken = json['access_token'] as String - ..user = User.fromJson(json['user'] as Map); - -UpdateUsersResponse _$UpdateUsersResponseFromJson(Map json) => - UpdateUsersResponse() - ..duration = json['duration'] as String? - ..users = (json['users'] as Map?)?.map( - (k, e) => MapEntry(k, User.fromJson(e as Map)), - ) ?? - {}; + Map json, +) => ConnectGuestUserResponse() + ..duration = json['duration'] as String? + ..accessToken = json['access_token'] as String + ..user = User.fromJson(json['user'] as Map); + +UpdateUsersResponse _$UpdateUsersResponseFromJson(Map json) => UpdateUsersResponse() + ..duration = json['duration'] as String? + ..users = + (json['users'] as Map?)?.map( + (k, e) => MapEntry(k, User.fromJson(e as Map)), + ) ?? + {}; UpdateMessageResponse _$UpdateMessageResponseFromJson( - Map json) => - UpdateMessageResponse() - ..duration = json['duration'] as String? - ..message = Message.fromJson(json['message'] as Map); - -SendMessageResponse _$SendMessageResponseFromJson(Map json) => - SendMessageResponse() - ..duration = json['duration'] as String? - ..message = Message.fromJson(json['message'] as Map); - -GetMessageResponse _$GetMessageResponseFromJson(Map json) => - GetMessageResponse() - ..duration = json['duration'] as String? - ..message = Message.fromJson(json['message'] as Map) - ..channel = json['channel'] == null - ? null - : ChannelModel.fromJson(json['channel'] as Map); + Map json, +) => UpdateMessageResponse() + ..duration = json['duration'] as String? + ..message = Message.fromJson(json['message'] as Map); + +SendMessageResponse _$SendMessageResponseFromJson(Map json) => SendMessageResponse() + ..duration = json['duration'] as String? + ..message = Message.fromJson(json['message'] as Map); + +GetMessageResponse _$GetMessageResponseFromJson(Map json) => GetMessageResponse() + ..duration = json['duration'] as String? + ..message = Message.fromJson(json['message'] as Map) + ..channel = json['channel'] == null ? null : ChannelModel.fromJson(json['channel'] as Map); SearchMessagesResponse _$SearchMessagesResponseFromJson( - Map json) => - SearchMessagesResponse() - ..duration = json['duration'] as String? - ..results = (json['results'] as List?) - ?.map( - (e) => GetMessageResponse.fromJson(e as Map)) - .toList() ?? - [] - ..next = json['next'] as String? - ..previous = json['previous'] as String?; + Map json, +) => SearchMessagesResponse() + ..duration = json['duration'] as String? + ..results = + (json['results'] as List?) + ?.map( + (e) => GetMessageResponse.fromJson(e as Map), + ) + .toList() ?? + [] + ..next = json['next'] as String? + ..previous = json['previous'] as String?; GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson( - Map json) => - GetMessagesByIdResponse() - ..duration = json['duration'] as String? - ..messages = (json['messages'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList() ?? - []; + Map json, +) => GetMessagesByIdResponse() + ..duration = json['duration'] as String? + ..messages = + (json['messages'] as List?)?.map((e) => Message.fromJson(e as Map)).toList() ?? []; UpdateChannelResponse _$UpdateChannelResponseFromJson( - Map json) => - UpdateChannelResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); + Map json, +) => UpdateChannelResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() + ..message = json['message'] == null ? null : Message.fromJson(json['message'] as Map); PartialUpdateChannelResponse _$PartialUpdateChannelResponseFromJson( - Map json) => - PartialUpdateChannelResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList(); + Map json, +) => PartialUpdateChannelResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList(); InviteMembersResponse _$InviteMembersResponseFromJson( - Map json) => - InviteMembersResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [] - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); + Map json, +) => InviteMembersResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? [] + ..message = json['message'] == null ? null : Message.fromJson(json['message'] as Map); RemoveMembersResponse _$RemoveMembersResponseFromJson( - Map json) => - RemoveMembersResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [] - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); - -SendActionResponse _$SendActionResponseFromJson(Map json) => - SendActionResponse() - ..duration = json['duration'] as String? - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); - -AddMembersResponse _$AddMembersResponseFromJson(Map json) => - AddMembersResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [] - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); + Map json, +) => RemoveMembersResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? [] + ..message = json['message'] == null ? null : Message.fromJson(json['message'] as Map); + +SendActionResponse _$SendActionResponseFromJson(Map json) => SendActionResponse() + ..duration = json['duration'] as String? + ..message = json['message'] == null ? null : Message.fromJson(json['message'] as Map); + +AddMembersResponse _$AddMembersResponseFromJson(Map json) => AddMembersResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? [] + ..message = json['message'] == null ? null : Message.fromJson(json['message'] as Map); AcceptInviteResponse _$AcceptInviteResponseFromJson( - Map json) => - AcceptInviteResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [] - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); + Map json, +) => AcceptInviteResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? [] + ..message = json['message'] == null ? null : Message.fromJson(json['message'] as Map); RejectInviteResponse _$RejectInviteResponseFromJson( - Map json) => - RejectInviteResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [] - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); + Map json, +) => RejectInviteResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? [] + ..message = json['message'] == null ? null : Message.fromJson(json['message'] as Map); EmptyResponse _$EmptyResponseFromJson(Map json) => EmptyResponse()..duration = json['duration'] as String?; ChannelStateResponse _$ChannelStateResponseFromJson( - Map json) => - ChannelStateResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..messages = (json['messages'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList() ?? - [] - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [] - ..watcherCount = (json['watcher_count'] as num?)?.toInt() ?? 0 - ..read = (json['read'] as List?) - ?.map((e) => Read.fromJson(e as Map)) - .toList() ?? - []; + Map json, +) => ChannelStateResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..messages = + (json['messages'] as List?)?.map((e) => Message.fromJson(e as Map)).toList() ?? [] + ..members = (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList() ?? [] + ..watcherCount = (json['watcher_count'] as num?)?.toInt() ?? 0 + ..read = (json['read'] as List?)?.map((e) => Read.fromJson(e as Map)).toList() ?? []; OGAttachmentResponse _$OGAttachmentResponseFromJson( - Map json) => - OGAttachmentResponse() - ..duration = json['duration'] as String? - ..ogScrapeUrl = json['og_scrape_url'] as String - ..assetUrl = json['asset_url'] as String? - ..authorLink = json['author_link'] as String? - ..authorName = json['author_name'] as String? - ..imageUrl = json['image_url'] as String? - ..text = json['text'] as String? - ..thumbUrl = json['thumb_url'] as String? - ..title = json['title'] as String? - ..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? - ..blockedByUserId = json['blocked_by_user_id'] as String? ?? '' - ..blockedUserId = json['blocked_user_id'] as String? ?? '' - ..createdAt = DateTime.parse(json['created_at'] as String); + Map json, +) => OGAttachmentResponse() + ..duration = json['duration'] as String? + ..ogScrapeUrl = json['og_scrape_url'] as String + ..assetUrl = json['asset_url'] as String? + ..authorLink = json['author_link'] as String? + ..authorName = json['author_name'] as String? + ..imageUrl = json['image_url'] as String? + ..text = json['text'] as String? + ..thumbUrl = json['thumb_url'] as String? + ..title = json['title'] as String? + ..titleLink = json['title_link'] as String? + ..type = json['type'] as String?; + +UserBlockResponse _$UserBlockResponseFromJson(Map json) => UserBlockResponse() + ..duration = json['duration'] as String? + ..blockedByUserId = json['blocked_by_user_id'] as String? ?? '' + ..blockedUserId = json['blocked_user_id'] as String? ?? '' + ..createdAt = DateTime.parse(json['created_at'] as String); BlockedUsersResponse _$BlockedUsersResponseFromJson( - Map json) => - BlockedUsersResponse() - ..duration = json['duration'] as String? - ..blocks = (json['blocks'] as List?) - ?.map((e) => UserBlock.fromJson(e as Map)) - .toList() ?? - []; - -CreatePollResponse _$CreatePollResponseFromJson(Map json) => - CreatePollResponse() - ..duration = json['duration'] as String? - ..poll = Poll.fromJson(json['poll'] as Map); - -GetPollResponse _$GetPollResponseFromJson(Map json) => - GetPollResponse() - ..duration = json['duration'] as String? - ..poll = Poll.fromJson(json['poll'] as Map); - -UpdatePollResponse _$UpdatePollResponseFromJson(Map json) => - UpdatePollResponse() - ..duration = json['duration'] as String? - ..poll = Poll.fromJson(json['poll'] as Map); + Map json, +) => BlockedUsersResponse() + ..duration = json['duration'] as String? + ..blocks = + (json['blocks'] as List?)?.map((e) => UserBlock.fromJson(e as Map)).toList() ?? []; + +CreatePollResponse _$CreatePollResponseFromJson(Map json) => CreatePollResponse() + ..duration = json['duration'] as String? + ..poll = Poll.fromJson(json['poll'] as Map); + +GetPollResponse _$GetPollResponseFromJson(Map json) => GetPollResponse() + ..duration = json['duration'] as String? + ..poll = Poll.fromJson(json['poll'] as Map); + +UpdatePollResponse _$UpdatePollResponseFromJson(Map json) => UpdatePollResponse() + ..duration = json['duration'] as String? + ..poll = Poll.fromJson(json['poll'] as Map); CreatePollOptionResponse _$CreatePollOptionResponseFromJson( - Map json) => - CreatePollOptionResponse() - ..duration = json['duration'] as String? - ..pollOption = - PollOption.fromJson(json['poll_option'] as Map); + Map json, +) => CreatePollOptionResponse() + ..duration = json['duration'] as String? + ..pollOption = PollOption.fromJson( + json['poll_option'] as Map, + ); GetPollOptionResponse _$GetPollOptionResponseFromJson( - Map json) => - GetPollOptionResponse() - ..duration = json['duration'] as String? - ..pollOption = - PollOption.fromJson(json['poll_option'] as Map); + Map json, +) => GetPollOptionResponse() + ..duration = json['duration'] as String? + ..pollOption = PollOption.fromJson( + json['poll_option'] as Map, + ); UpdatePollOptionResponse _$UpdatePollOptionResponseFromJson( - Map json) => - UpdatePollOptionResponse() - ..duration = json['duration'] as String? - ..pollOption = - PollOption.fromJson(json['poll_option'] as Map); + Map json, +) => UpdatePollOptionResponse() + ..duration = json['duration'] as String? + ..pollOption = PollOption.fromJson( + json['poll_option'] as Map, + ); CastPollVoteResponse _$CastPollVoteResponseFromJson( - Map json) => - CastPollVoteResponse() - ..duration = json['duration'] as String? - ..vote = PollVote.fromJson(json['vote'] as Map); + Map json, +) => CastPollVoteResponse() + ..duration = json['duration'] as String? + ..vote = PollVote.fromJson(json['vote'] as Map); RemovePollVoteResponse _$RemovePollVoteResponseFromJson( - Map json) => - RemovePollVoteResponse() - ..duration = json['duration'] as String? - ..vote = PollVote.fromJson(json['vote'] as Map); - -QueryPollsResponse _$QueryPollsResponseFromJson(Map json) => - QueryPollsResponse() - ..duration = json['duration'] as String? - ..polls = (json['polls'] as List?) - ?.map((e) => Poll.fromJson(e as Map)) - .toList() ?? - [] - ..next = json['next'] as String?; + Map json, +) => RemovePollVoteResponse() + ..duration = json['duration'] as String? + ..vote = PollVote.fromJson(json['vote'] as Map); + +QueryPollsResponse _$QueryPollsResponseFromJson(Map json) => QueryPollsResponse() + ..duration = json['duration'] as String? + ..polls = (json['polls'] as List?)?.map((e) => Poll.fromJson(e as Map)).toList() ?? [] + ..next = json['next'] as String?; QueryPollVotesResponse _$QueryPollVotesResponseFromJson( - Map json) => - QueryPollVotesResponse() - ..duration = json['duration'] as String? - ..votes = (json['votes'] as List?) - ?.map((e) => PollVote.fromJson(e as Map)) - .toList() ?? - [] - ..next = json['next'] as String?; - -GetThreadResponse _$GetThreadResponseFromJson(Map json) => - GetThreadResponse() - ..duration = json['duration'] as String? - ..thread = Thread.fromJson(json['thread'] as Map); + Map json, +) => QueryPollVotesResponse() + ..duration = json['duration'] as String? + ..votes = (json['votes'] as List?)?.map((e) => PollVote.fromJson(e as Map)).toList() ?? [] + ..next = json['next'] as String?; + +GetThreadResponse _$GetThreadResponseFromJson(Map json) => GetThreadResponse() + ..duration = json['duration'] as String? + ..thread = Thread.fromJson(json['thread'] as Map); UpdateThreadResponse _$UpdateThreadResponseFromJson( - Map json) => - UpdateThreadResponse() - ..duration = json['duration'] as String? - ..thread = Thread.fromJson(json['thread'] as Map); + Map json, +) => UpdateThreadResponse() + ..duration = json['duration'] as String? + ..thread = Thread.fromJson(json['thread'] as Map); QueryThreadsResponse _$QueryThreadsResponseFromJson( - Map json) => - QueryThreadsResponse() - ..duration = json['duration'] as String? - ..threads = (json['threads'] as List?) - ?.map((e) => Thread.fromJson(e as Map)) - .toList() ?? - [] - ..next = json['next'] as String?; - -CreateDraftResponse _$CreateDraftResponseFromJson(Map json) => - CreateDraftResponse() - ..duration = json['duration'] as String? - ..draft = Draft.fromJson(json['draft'] as Map); - -GetDraftResponse _$GetDraftResponseFromJson(Map json) => - GetDraftResponse() - ..duration = json['duration'] as String? - ..draft = Draft.fromJson(json['draft'] as Map); - -QueryDraftsResponse _$QueryDraftsResponseFromJson(Map json) => - QueryDraftsResponse() - ..duration = json['duration'] as String? - ..drafts = (json['drafts'] as List?) - ?.map((e) => Draft.fromJson(e as Map)) - .toList() ?? - [] - ..next = json['next'] as String?; + Map json, +) => QueryThreadsResponse() + ..duration = json['duration'] as String? + ..threads = (json['threads'] as List?)?.map((e) => Thread.fromJson(e as Map)).toList() ?? [] + ..next = json['next'] as String?; + +CreateDraftResponse _$CreateDraftResponseFromJson(Map json) => CreateDraftResponse() + ..duration = json['duration'] as String? + ..draft = Draft.fromJson(json['draft'] as Map); + +GetDraftResponse _$GetDraftResponseFromJson(Map json) => GetDraftResponse() + ..duration = json['duration'] as String? + ..draft = Draft.fromJson(json['draft'] as Map); + +QueryDraftsResponse _$QueryDraftsResponseFromJson(Map json) => QueryDraftsResponse() + ..duration = json['duration'] as String? + ..drafts = (json['drafts'] as List?)?.map((e) => Draft.fromJson(e as Map)).toList() ?? [] + ..next = json['next'] as String?; CreateReminderResponse _$CreateReminderResponseFromJson( - Map json) => - CreateReminderResponse() - ..duration = json['duration'] as String? - ..reminder = - MessageReminder.fromJson(json['reminder'] as Map); + Map json, +) => CreateReminderResponse() + ..duration = json['duration'] as String? + ..reminder = MessageReminder.fromJson( + json['reminder'] as Map, + ); UpdateReminderResponse _$UpdateReminderResponseFromJson( - Map json) => - UpdateReminderResponse() - ..duration = json['duration'] as String? - ..reminder = - MessageReminder.fromJson(json['reminder'] as Map); + Map json, +) => UpdateReminderResponse() + ..duration = json['duration'] as String? + ..reminder = MessageReminder.fromJson( + json['reminder'] as Map, + ); QueryRemindersResponse _$QueryRemindersResponseFromJson( - Map json) => - QueryRemindersResponse() - ..duration = json['duration'] as String? - ..reminders = (json['reminders'] as List?) - ?.map((e) => MessageReminder.fromJson(e as Map)) - .toList() ?? - [] - ..next = json['next'] as String?; + Map json, +) => QueryRemindersResponse() + ..duration = json['duration'] as String? + ..reminders = + (json['reminders'] as List?)?.map((e) => MessageReminder.fromJson(e as Map)).toList() ?? + [] + ..next = json['next'] as String?; GetUnreadCountResponse _$GetUnreadCountResponseFromJson( - Map json) => - GetUnreadCountResponse() - ..duration = json['duration'] as String? - ..totalUnreadCount = (json['total_unread_count'] as num).toInt() - ..totalUnreadThreadsCount = - (json['total_unread_threads_count'] as num).toInt() - ..totalUnreadCountByTeam = - (json['total_unread_count_by_team'] as Map?)?.map( - (k, e) => MapEntry(k, (e as num).toInt()), + Map json, +) => GetUnreadCountResponse() + ..duration = json['duration'] as String? + ..totalUnreadCount = (json['total_unread_count'] as num).toInt() + ..totalUnreadThreadsCount = (json['total_unread_threads_count'] as num).toInt() + ..totalUnreadCountByTeam = (json['total_unread_count_by_team'] as Map?)?.map( + (k, e) => MapEntry(k, (e as num).toInt()), + ) + ..channels = (json['channels'] as List) + .map( + (e) => UnreadCountsChannel.fromJson(e as Map), ) - ..channels = (json['channels'] as List) - .map((e) => UnreadCountsChannel.fromJson(e as Map)) - .toList() - ..channelType = (json['channel_type'] as List) - .map((e) => - UnreadCountsChannelType.fromJson(e as Map)) - .toList() - ..threads = (json['threads'] as List) - .map((e) => UnreadCountsThread.fromJson(e as Map)) - .toList(); + .toList() + ..channelType = (json['channel_type'] as List) + .map( + (e) => UnreadCountsChannelType.fromJson(e as Map), + ) + .toList() + ..threads = (json['threads'] as List) + .map( + (e) => UnreadCountsThread.fromJson(e as Map), + ) + .toList(); UpsertPushPreferencesResponse _$UpsertPushPreferencesResponseFromJson( - Map json) => - UpsertPushPreferencesResponse() - ..duration = json['duration'] as String? - ..userPreferences = (json['user_preferences'] as Map?) - ?.map( - (k, e) => - MapEntry(k, PushPreference.fromJson(e as Map)), - ) ?? - {} - ..userChannelPreferences = - (json['user_channel_preferences'] as Map?)?.map( - (k, e) => MapEntry( - k, - (e as Map).map( - (k, e) => MapEntry( - k, - ChannelPushPreference.fromJson( - e as Map)), - )), - ) ?? - {}; + Map json, +) => UpsertPushPreferencesResponse() + ..duration = json['duration'] as String? + ..userPreferences = + (json['user_preferences'] as Map?)?.map( + (k, e) => MapEntry(k, PushPreference.fromJson(e as Map)), + ) ?? + {} + ..userChannelPreferences = + (json['user_channel_preferences'] as Map?)?.map( + (k, e) => MapEntry( + k, + (e as Map).map( + (k, e) => MapEntry( + k, + ChannelPushPreference.fromJson(e as Map), + ), + ), + ), + ) ?? + {}; + +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..2a1c7cad58 100644 --- a/packages/stream_chat/lib/src/core/api/sort_order.dart +++ b/packages/stream_chat/lib/src/core/api/sort_order.dart @@ -21,7 +21,7 @@ enum NullOrdering { /// Null values appear at the end of the sorted list, /// regardless of sort direction (ASC or DESC). - nullsLast; + nullsLast, } /// A sort specification for objects that implement [ComparableFieldProvider]. @@ -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: @@ -60,8 +47,8 @@ class SortOption { this.field, { this.nullOrdering = NullOrdering.nullsFirst, Comparator? comparator, - }) : direction = SortOption.DESC, - _comparator = comparator; + }) : direction = SortOption.DESC, + _comparator = comparator; /// Creates a SortOption for ascending order sorting by the specified field. /// @@ -74,12 +61,8 @@ class SortOption { this.field, { this.nullOrdering = NullOrdering.nullsLast, Comparator? comparator, - }) : direction = SortOption.ASC, - _comparator = comparator; - - /// Create a new instance from JSON. - factory SortOption.fromJson(Map json) => - _$SortOptionFromJson(json); + }) : direction = SortOption.ASC, + _comparator = comparator; /// Ascending order (1) static const ASC = 1; @@ -149,8 +132,7 @@ class SortOption { } /// Extension that allows a [SortOrder] to be used as a comparator function. -extension CompositeComparator - on SortOrder { +extension CompositeComparator on SortOrder { /// Compares two objects using all sort options in sequence. /// /// Returns the first non-zero comparison result, or 0 if all comparisons 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..c54ab585ab 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,16 +6,7 @@ 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) => - { - 'field': instance.field, - 'direction': instance.direction, - }; +Map _$SortOptionToJson(SortOption instance) => { + 'field': instance.field, + 'direction': instance.direction, +}; 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..5ece808a93 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'; @@ -29,23 +28,23 @@ class StreamChatApi { TokenManager? tokenManager, ConnectionIdManager? connectionIdManager, SystemEnvironmentManager? systemEnvironmentManager, - AttachmentFileUploaderProvider attachmentFileUploaderProvider = - StreamAttachmentFileUploader.new, + AttachmentFileUploaderProvider attachmentFileUploaderProvider = StreamAttachmentFileUploader.new, Logger? logger, Iterable? interceptors, HttpClientAdapter? httpClientAdapter, - }) : _fileUploaderProvider = attachmentFileUploaderProvider, - _client = client ?? - StreamHttpClient( - apiKey, - options: options, - tokenManager: tokenManager, - connectionIdManager: connectionIdManager, - systemEnvironmentManager: systemEnvironmentManager, - logger: logger, - interceptors: interceptors, - httpClientAdapter: httpClientAdapter, - ); + }) : _fileUploaderProvider = attachmentFileUploaderProvider, + _client = + client ?? + StreamHttpClient( + apiKey, + options: options, + tokenManager: tokenManager, + connectionIdManager: connectionIdManager, + systemEnvironmentManager: systemEnvironmentManager, + logger: logger, + interceptors: interceptors, + httpClientAdapter: httpClientAdapter, + ); final StreamHttpClient _client; final AttachmentFileUploaderProvider _fileUploaderProvider; @@ -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; @@ -97,7 +90,6 @@ class StreamChatApi { GeneralApi? _general; /// Class responsible for uploading images and files to a given channel - AttachmentFileUploader get fileUploader => - _fileUploader ??= _fileUploaderProvider.call(_client); + AttachmentFileUploader get fileUploader => _fileUploader ??= _fileUploaderProvider.call(_client); AttachmentFileUploader? _fileUploader; } 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..00a38f0ad4 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,34 @@ 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/error/chat_error_code.dart b/packages/stream_chat/lib/src/core/error/chat_error_code.dart index d597000ad6..b35c010da2 100644 --- a/packages/stream_chat/lib/src/core/error/chat_error_code.dart +++ b/packages/stream_chat/lib/src/core/error/chat_error_code.dart @@ -94,10 +94,8 @@ enum ChatErrorCode { } const _errorCodeWithDescription = { - ChatErrorCode.undefinedToken: - MapEntry(1000, 'Unauthorised, token not defined'), - ChatErrorCode.inputError: - MapEntry(4, 'Wrong data/parameter is sent to the API'), + ChatErrorCode.undefinedToken: MapEntry(1000, 'Unauthorised, token not defined'), + ChatErrorCode.inputError: MapEntry(4, 'Wrong data/parameter is sent to the API'), ChatErrorCode.duplicateUsername: MapEntry( 6, 'Duplicate username is sent while enforce_unique_usernames is enabled', @@ -112,36 +110,24 @@ const _errorCodeWithDescription = { 21, 'Multiple Levels Reply is not supported - the API only supports 1 level deep reply threads', ), - ChatErrorCode.customCommandEndpointCall: - MapEntry(45, 'Custom Command handler returned an error'), - ChatErrorCode.customCommandEndpointMissing: - MapEntry(44, 'App config does not have custom_action_handler_url'), - ChatErrorCode.authenticationError: - MapEntry(5, 'Unauthenticated, problem with authentication'), + ChatErrorCode.customCommandEndpointCall: MapEntry(45, 'Custom Command handler returned an error'), + ChatErrorCode.customCommandEndpointMissing: MapEntry(44, 'App config does not have custom_action_handler_url'), + ChatErrorCode.authenticationError: MapEntry(5, 'Unauthenticated, problem with authentication'), ChatErrorCode.tokenExpired: MapEntry(40, 'Unauthenticated, token expired'), - ChatErrorCode.tokenBeforeIssuedAt: - MapEntry(42, 'Unauthenticated, token date incorrect'), - ChatErrorCode.tokenNotValid: - MapEntry(41, 'Unauthenticated, token not valid yet'), - ChatErrorCode.tokenSignatureInvalid: - MapEntry(43, 'Unauthenticated, token signature invalid'), + ChatErrorCode.tokenBeforeIssuedAt: MapEntry(42, 'Unauthenticated, token date incorrect'), + ChatErrorCode.tokenNotValid: MapEntry(41, 'Unauthenticated, token not valid yet'), + ChatErrorCode.tokenSignatureInvalid: MapEntry(43, 'Unauthenticated, token signature invalid'), ChatErrorCode.accessKeyError: MapEntry(2, 'Access Key invalid'), - ChatErrorCode.notAllowed: - MapEntry(17, 'Unauthorised / forbidden to make request'), + ChatErrorCode.notAllowed: MapEntry(17, 'Unauthorised / forbidden to make request'), ChatErrorCode.appSuspended: MapEntry(99, 'App suspended'), - ChatErrorCode.cooldownError: - MapEntry(60, 'User tried to post a message during the cooldown period'), + ChatErrorCode.cooldownError: MapEntry(60, 'User tried to post a message during the cooldown period'), ChatErrorCode.doesNotExist: MapEntry(16, 'Resource not found'), ChatErrorCode.requestTimeout: MapEntry(23, 'Request timed out'), ChatErrorCode.payloadTooBig: MapEntry(22, 'Payload too big'), - ChatErrorCode.rateLimitError: - MapEntry(9, 'Too many requests in a certain time frame'), - ChatErrorCode.maximumHeaderSizeExceeded: - MapEntry(24, 'Request headers are too large'), - ChatErrorCode.internalSystemError: - MapEntry(-1, 'Something goes wrong in the system'), - ChatErrorCode.noAccessToChannels: - MapEntry(70, 'No access to requested channels'), + ChatErrorCode.rateLimitError: MapEntry(9, 'Too many requests in a certain time frame'), + ChatErrorCode.maximumHeaderSizeExceeded: MapEntry(24, 'Request headers are too large'), + ChatErrorCode.internalSystemError: MapEntry(-1, 'Something goes wrong in the system'), + ChatErrorCode.noAccessToChannels: MapEntry(70, 'No access to requested channels'), }; const _authenticationErrors = [ @@ -156,8 +142,8 @@ const _authenticationErrors = [ ]; /// -ChatErrorCode? chatErrorCodeFromCode(int code) => _errorCodeWithDescription.keys - .firstWhereOrNull((key) => _errorCodeWithDescription[key]!.key == code); +ChatErrorCode? chatErrorCodeFromCode(int code) => + _errorCodeWithDescription.keys.firstWhereOrNull((key) => _errorCodeWithDescription[key]!.key == code); /// extension ChatErrorCodeX on ChatErrorCode { diff --git a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart index 5e88fb3bc9..b2925940e5 100644 --- a/packages/stream_chat/lib/src/core/error/stream_chat_error.dart +++ b/packages/stream_chat/lib/src/core/error/stream_chat_error.dart @@ -78,10 +78,10 @@ class StreamChatNetworkError extends StreamChatError { this.data, StackTrace? stacktrace, this.isRequestCancelledError = false, - }) : code = errorCode.code, - statusCode = statusCode ?? data?.statusCode, - stackTrace = stacktrace ?? StackTrace.current, - super(errorCode.message); + }) : code = errorCode.code, + statusCode = statusCode ?? data?.statusCode, + stackTrace = stacktrace ?? StackTrace.current, + super(errorCode.message); /// StreamChatNetworkError.raw({ @@ -91,8 +91,8 @@ class StreamChatNetworkError extends StreamChatError { this.data, StackTrace? stacktrace, this.isRequestCancelledError = false, - }) : stackTrace = stacktrace ?? StackTrace.current, - super(message); + }) : stackTrace = stacktrace ?? StackTrace.current, + super(message); /// factory StreamChatNetworkError.fromDioException(DioException exception) { @@ -106,10 +106,7 @@ class StreamChatNetworkError extends StreamChatError { } return StreamChatNetworkError.raw( code: errorResponse?.code ?? -1, - message: errorResponse?.message ?? - response?.statusMessage ?? - exception.message ?? - '', + message: errorResponse?.message ?? response?.statusMessage ?? exception.message ?? '', statusCode: errorResponse?.statusCode ?? response?.statusCode, data: errorResponse, stacktrace: exception.stackTrace, diff --git a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart index 6d6f74c301..258489471e 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/logging_interceptor.dart @@ -125,8 +125,7 @@ class LoggingInterceptor extends Interceptor { final uri = exception.response?.requestOptions.uri; _printBoxed( _logPrintError, - header: - 'DioException ║ Status: ${exception.response?.statusCode} ${exception.response?.statusMessage}', + header: 'DioException ║ Status: ${exception.response?.statusCode} ${exception.response?.statusMessage}', text: uri.toString(), ); if (exception.response != null && exception.response?.data != null) { @@ -152,8 +151,7 @@ class LoggingInterceptor extends Interceptor { _printResponseHeader(_logPrintResponse, response); if (responseHeader) { final responseHeaders = {}; - response.headers - .forEach((k, list) => responseHeaders[k] = list.toString()); + response.headers.forEach((k, list) => responseHeaders[k] = list.toString()); _printMapAsTable(_logPrintResponse, responseHeaders, header: 'Headers'); } @@ -197,8 +195,7 @@ class LoggingInterceptor extends Interceptor { final method = response.requestOptions.method; _printBoxed( logPrint, - header: - 'Response ║ $method ║ Status: ${response.statusCode} ${response.statusMessage}', + header: 'Response ║ $method ║ Status: ${response.statusCode} ${response.statusMessage}', text: uri.toString(), ); } @@ -216,8 +213,7 @@ class LoggingInterceptor extends Interceptor { void Function(Object) logPrint, [ String pre = '', String suf = '╝', - ]) => - logPrint('$pre${'═' * maxWidth}$suf'); + ]) => logPrint('$pre${'═' * maxWidth}$suf'); void _printKV(void Function(Object) logPrint, String? key, Object? v) { final pre = '╟ $key: '; @@ -234,11 +230,13 @@ class LoggingInterceptor extends Interceptor { void _printBlock(void Function(Object) logPrint, String msg) { final lines = (msg.length / maxWidth).ceil(); for (var i = 0; i < lines; ++i) { - logPrint((i >= 0 ? '║ ' : '') + - msg.substring( - i * maxWidth, - math.min(i * maxWidth + maxWidth, msg.length), - )); + logPrint( + (i >= 0 ? '║ ' : '') + + msg.substring( + i * maxWidth, + math.min(i * maxWidth + maxWidth, msg.length), + ), + ); } } @@ -286,10 +284,12 @@ class LoggingInterceptor extends Interceptor { if (msg.length + indent.length > linWidth) { final lines = (msg.length / linWidth).ceil(); for (var i = 0; i < lines; ++i) { - logPrint('║${_indent(_tabs)} ${msg.substring( - i * linWidth, - math.min(i * linWidth + linWidth, msg.length), - )}'); + logPrint( + '║${_indent(_tabs)} ${msg.substring( + i * linWidth, + math.min(i * linWidth + linWidth, msg.length), + )}', + ); } } else { logPrint('║${_indent(_tabs)} $key: $msg${!isLast ? ',' : ''}'); @@ -332,16 +332,13 @@ class LoggingInterceptor extends Interceptor { }) { if (map == null || map.isEmpty) return; logPrint('╔ $header '); - map.forEach((dynamic key, dynamic value) => - _printKV(logPrint, key.toString(), value)); + map.forEach((dynamic key, dynamic value) => _printKV(logPrint, key.toString(), value)); _printLine(logPrint, '╚'); } - void _logPrintRequest(Object object) => - logPrint(InterceptStep.request, object); + void _logPrintRequest(Object object) => logPrint(InterceptStep.request, object); - void _logPrintResponse(Object object) => - logPrint(InterceptStep.response, object); + void _logPrintResponse(Object object) => logPrint(InterceptStep.response, object); void _logPrintError(Object object) => logPrint(InterceptStep.error, object); } diff --git a/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart index d59a032c63..25b75fd82e 100644 --- a/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart +++ b/packages/stream_chat/lib/src/core/http/stream_chat_dio_error.dart @@ -12,9 +12,9 @@ class StreamChatDioError extends DioException { StackTrace? stackTrace, super.message, }) : super( - error: error, - stackTrace: stackTrace ?? StackTrace.current, - ); + error: error, + stackTrace: stackTrace ?? StackTrace.current, + ); @override final StreamChatNetworkError error; diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client.dart b/packages/stream_chat/lib/src/core/http/stream_http_client.dart index 16141c0a26..7c887246e0 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client.dart @@ -29,8 +29,8 @@ class StreamHttpClient { Logger? logger, Iterable? interceptors, HttpClientAdapter? httpClientAdapter, - }) : _options = options ?? const StreamHttpClientOptions(), - httpClient = dio ?? Dio() { + }) : _options = options ?? const StreamHttpClientOptions(), + httpClient = dio ?? Dio() { httpClient ..options.baseUrl = _options.baseUrl ..options.receiveTimeout = _options.receiveTimeout @@ -47,8 +47,7 @@ class StreamHttpClient { ..interceptors.addAll([ AdditionalHeadersInterceptor(systemEnvironmentManager), if (tokenManager != null) AuthInterceptor(this, tokenManager), - if (connectionIdManager != null) - ConnectionIdInterceptor(connectionIdManager), + if (connectionIdManager != null) ConnectionIdInterceptor(connectionIdManager), ...interceptors ?? [ // Add a default logging interceptor if no interceptors are diff --git a/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart b/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart index ad18c2544f..a98a7904c2 100644 --- a/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart +++ b/packages/stream_chat/lib/src/core/http/stream_http_client_options.dart @@ -2,13 +2,19 @@ part of 'stream_http_client.dart'; const _defaultBaseURL = 'https://chat.stream-io-api.com'; +/// The default connect timeout for the api request +const kDefaultConnectTimeout = Duration(seconds: 30); + +/// The default receive timeout for the api request +const kDefaultReceiveTimeout = Duration(seconds: 30); + /// Client options to modify [StreamHttpClient] class StreamHttpClientOptions { /// Instantiates a new [StreamHttpClientOptions] const StreamHttpClientOptions({ String? baseUrl, - this.connectTimeout = const Duration(seconds: 30), - this.receiveTimeout = const Duration(seconds: 30), + this.connectTimeout = kDefaultConnectTimeout, + this.receiveTimeout = kDefaultReceiveTimeout, this.queryParameters = const {}, this.headers = const {}, }) : baseUrl = baseUrl ?? _defaultBaseURL; diff --git a/packages/stream_chat/lib/src/core/http/system_environment_manager.dart b/packages/stream_chat/lib/src/core/http/system_environment_manager.dart index 42bd01c3ea..bb4648e7d3 100644 --- a/packages/stream_chat/lib/src/core/http/system_environment_manager.dart +++ b/packages/stream_chat/lib/src/core/http/system_environment_manager.dart @@ -12,14 +12,14 @@ class SystemEnvironmentManager { SystemEnvironmentManager({ SystemEnvironment? environment, }) : _environment = switch (environment) { - final env? => env, - _ => SystemEnvironment( - sdkName: 'stream-chat', - sdkIdentifier: 'dart', - sdkVersion: PACKAGE_VERSION, - osName: CurrentPlatform.name, - ), - }; + final env? => env, + _ => SystemEnvironment( + sdkName: 'stream-chat', + sdkIdentifier: 'dart', + sdkVersion: PACKAGE_VERSION, + osName: CurrentPlatform.name, + ), + }; /// Returns the Stream client user agent string based on the current /// [environment] value. diff --git a/packages/stream_chat/lib/src/core/http/token.dart b/packages/stream_chat/lib/src/core/http/token.dart index 2472a1f7f5..df1114a0ca 100644 --- a/packages/stream_chat/lib/src/core/http/token.dart +++ b/packages/stream_chat/lib/src/core/http/token.dart @@ -29,10 +29,10 @@ class Token extends Equatable { /// The token that can be used when user is unknown. /// Is used by `anonymous` token provider. factory Token.anonymous({String? userId}) => Token._( - rawValue: '', - userId: userId ?? randomId(), - authType: AuthType.anonymous, - ); + rawValue: '', + userId: userId ?? randomId(), + authType: AuthType.anonymous, + ); /// Creates a [Token] instance from the provided [rawValue] if it's valid. factory Token.fromRawValue(String rawValue) { diff --git a/packages/stream_chat/lib/src/core/http/token_manager.dart b/packages/stream_chat/lib/src/core/http/token_manager.dart index e5af0ddc33..96b90bdcc0 100644 --- a/packages/stream_chat/lib/src/core/http/token_manager.dart +++ b/packages/stream_chat/lib/src/core/http/token_manager.dart @@ -12,9 +12,9 @@ class TokenManager { String? userId, Token? token, TokenProvider? tokenProvider, - }) : _userId = userId, - _token = token, - _provider = tokenProvider; + }) : _userId = userId, + _token = token, + _provider = tokenProvider; String? _type; Token? _token; diff --git a/packages/stream_chat/lib/src/core/models/action.g.dart b/packages/stream_chat/lib/src/core/models/action.g.dart index 2c9148d92b..ec604d553f 100644 --- a/packages/stream_chat/lib/src/core/models/action.g.dart +++ b/packages/stream_chat/lib/src/core/models/action.g.dart @@ -7,17 +7,17 @@ part of 'action.dart'; // ************************************************************************** Action _$ActionFromJson(Map json) => Action( - name: json['name'] as String, - style: json['style'] as String? ?? 'default', - text: json['text'] as String, - type: json['type'] as String, - value: json['value'] as String?, - ); + name: json['name'] as String, + style: json['style'] as String? ?? 'default', + text: json['text'] as String, + type: json['type'] as String, + value: json['value'] as String?, +); Map _$ActionToJson(Action instance) => { - 'name': instance.name, - 'style': instance.style, - 'text': instance.text, - 'type': instance.type, - 'value': instance.value, - }; + 'name': instance.name, + 'style': instance.style, + 'text': instance.text, + 'type': instance.type, + 'value': instance.value, +}; diff --git a/packages/stream_chat/lib/src/core/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart index a4e15241c8..486750cf45 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.dart @@ -39,49 +39,48 @@ class Attachment extends Equatable { Map extraData = const {}, this.file, this.uploadState = const UploadState.preparing(), - }) : id = id ?? const Uuid().v4(), - _type = switch (type) { - String() => AttachmentType(type), - _ => null, - }, - title = title ?? file?.name, - localUri = file?.path != null ? Uri.parse(file!.path!) : null, - // For backwards compatibility, - // set 'file_size', 'mime_type' in [extraData]. - extraData = { - ...extraData, - if (file?.size != null) 'file_size': file?.size, - if (file?.mediaType != null) 'mime_type': file?.mediaType?.mimeType, - }; + }) : id = id ?? const Uuid().v4(), + _type = switch (type) { + String() => AttachmentType(type), + _ => null, + }, + title = title ?? file?.name, + localUri = file?.path != null ? Uri.parse(file!.path!) : null, + // For backwards compatibility, + // set 'file_size', 'mime_type' in [extraData]. + extraData = { + ...extraData, + if (file?.size != null) 'file_size': file?.size, + if (file?.mediaType != null) 'mime_type': file?.mediaType?.mimeType, + }; /// Create a new instance from a json - factory Attachment.fromJson(Map json) => - _$AttachmentFromJson( - Serializer.moveToExtraDataFromRoot(json, topLevelFields), - ); + factory Attachment.fromJson(Map json) => _$AttachmentFromJson( + Serializer.moveToExtraDataFromRoot(json, topLevelFields), + ); /// Create a new instance from a db data - factory Attachment.fromData(Map json) => - _$AttachmentFromJson(Serializer.moveToExtraDataFromRoot( - json, - topLevelFields + dbSpecificTopLevelFields, - )); - - factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) => - Attachment( - // If the type is not specified, we default to urlPreview. - type: ogAttachment.type ?? AttachmentType.urlPreview, - title: ogAttachment.title, - titleLink: ogAttachment.titleLink, - text: ogAttachment.text, - imageUrl: ogAttachment.imageUrl, - thumbUrl: ogAttachment.thumbUrl, - authorName: ogAttachment.authorName, - authorLink: ogAttachment.authorLink, - assetUrl: ogAttachment.assetUrl, - ogScrapeUrl: ogAttachment.ogScrapeUrl, - uploadState: const UploadState.success(), - ); + factory Attachment.fromData(Map json) => _$AttachmentFromJson( + Serializer.moveToExtraDataFromRoot( + json, + topLevelFields + dbSpecificTopLevelFields, + ), + ); + + factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) => Attachment( + // If the type is not specified, we default to urlPreview. + type: ogAttachment.type ?? AttachmentType.urlPreview, + title: ogAttachment.title, + titleLink: ogAttachment.titleLink, + text: ogAttachment.text, + imageUrl: ogAttachment.imageUrl, + thumbUrl: ogAttachment.thumbUrl, + authorName: ogAttachment.authorName, + authorLink: ogAttachment.authorLink, + assetUrl: ogAttachment.assetUrl, + ogScrapeUrl: ogAttachment.ogScrapeUrl, + uploadState: const UploadState.success(), + ); ///The attachment type based on the URL resource. This can be: audio, ///image or video @@ -219,8 +218,7 @@ class Attachment extends Equatable { ..removeWhere((key, value) => dbSpecificTopLevelFields.contains(key)); /// Serialize to db data - Map toData() => - Serializer.moveFromExtraDataToRoot(_$AttachmentToJson(this)); + Map toData() => Serializer.moveFromExtraDataToRoot(_$AttachmentToJson(this)); Attachment copyWith({ String? id, @@ -247,33 +245,32 @@ class Attachment extends Equatable { AttachmentFile? file, UploadState? uploadState, Map? extraData, - }) => - Attachment( - id: id ?? this.id, - type: type ?? this.type, - titleLink: titleLink ?? this.titleLink, - title: title ?? this.title, - thumbUrl: thumbUrl ?? this.thumbUrl, - text: text ?? this.text, - pretext: pretext ?? this.pretext, - ogScrapeUrl: ogScrapeUrl ?? this.ogScrapeUrl, - imageUrl: imageUrl ?? this.imageUrl, - footerIcon: footerIcon ?? this.footerIcon, - footer: footer ?? this.footer, - fields: fields ?? this.fields, - fallback: fallback ?? this.fallback, - color: color ?? this.color, - authorName: authorName ?? this.authorName, - authorLink: authorLink ?? this.authorLink, - authorIcon: authorIcon ?? this.authorIcon, - assetUrl: assetUrl ?? this.assetUrl, - actions: actions ?? this.actions, - originalWidth: originalWidth ?? this.originalWidth, - originalHeight: originalHeight ?? this.originalHeight, - file: file ?? this.file, - uploadState: uploadState ?? this.uploadState, - extraData: extraData ?? this.extraData, - ); + }) => Attachment( + id: id ?? this.id, + type: type ?? this.type, + titleLink: titleLink ?? this.titleLink, + title: title ?? this.title, + thumbUrl: thumbUrl ?? this.thumbUrl, + text: text ?? this.text, + pretext: pretext ?? this.pretext, + ogScrapeUrl: ogScrapeUrl ?? this.ogScrapeUrl, + imageUrl: imageUrl ?? this.imageUrl, + footerIcon: footerIcon ?? this.footerIcon, + footer: footer ?? this.footer, + fields: fields ?? this.fields, + fallback: fallback ?? this.fallback, + color: color ?? this.color, + authorName: authorName ?? this.authorName, + authorLink: authorLink ?? this.authorLink, + authorIcon: authorIcon ?? this.authorIcon, + assetUrl: assetUrl ?? this.assetUrl, + actions: actions ?? this.actions, + originalWidth: originalWidth ?? this.originalWidth, + originalHeight: originalHeight ?? this.originalHeight, + file: file ?? this.file, + uploadState: uploadState ?? this.uploadState, + extraData: extraData ?? this.extraData, + ); Attachment merge(Attachment? other) { if (other == null) return this; @@ -306,31 +303,31 @@ class Attachment extends Equatable { @override List get props => [ - id, - type, - titleLink, - title, - thumbUrl, - text, - pretext, - ogScrapeUrl, - imageUrl, - footerIcon, - footer, - fields, - fallback, - color, - authorName, - authorLink, - authorIcon, - assetUrl, - actions, - originalWidth, - originalHeight, - file, - uploadState, - extraData, - ]; + id, + type, + titleLink, + title, + thumbUrl, + text, + pretext, + ogScrapeUrl, + imageUrl, + footerIcon, + footer, + fields, + fallback, + color, + authorName, + authorLink, + authorIcon, + assetUrl, + actions, + originalWidth, + originalHeight, + file, + uploadState, + extraData, + ]; } /// {@template attachmentType} diff --git a/packages/stream_chat/lib/src/core/models/attachment.g.dart b/packages/stream_chat/lib/src/core/models/attachment.g.dart index 8fcc4fe4c6..8dd8e75a8c 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.g.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.g.dart @@ -7,64 +7,58 @@ part of 'attachment.dart'; // ************************************************************************** Attachment _$AttachmentFromJson(Map json) => Attachment( - id: json['id'] as String?, - type: AttachmentType.fromJson(json['type'] as String?), - titleLink: json['title_link'] as String?, - title: json['title'] as String?, - thumbUrl: json['thumb_url'] as String?, - text: json['text'] as String?, - pretext: json['pretext'] as String?, - ogScrapeUrl: json['og_scrape_url'] as String?, - imageUrl: json['image_url'] as String?, - footerIcon: json['footer_icon'] as String?, - footer: json['footer'] as String?, - fields: json['fields'], - fallback: json['fallback'] as String?, - color: json['color'] as String?, - authorName: json['author_name'] as String?, - authorLink: json['author_link'] as String?, - authorIcon: json['author_icon'] as String?, - assetUrl: json['asset_url'] as String?, - actions: (json['actions'] as List?) - ?.map((e) => Action.fromJson(e as Map)) - .toList() ?? - const [], - originalWidth: (json['original_width'] as num?)?.toInt(), - originalHeight: (json['original_height'] as num?)?.toInt(), - extraData: json['extra_data'] as Map? ?? const {}, - file: json['file'] == null - ? null - : AttachmentFile.fromJson(json['file'] as Map), - uploadState: json['upload_state'] == null - ? const UploadState.success() - : UploadState.fromJson(json['upload_state'] as Map), - ); + id: json['id'] as String?, + type: AttachmentType.fromJson(json['type'] as String?), + titleLink: json['title_link'] as String?, + title: json['title'] as String?, + thumbUrl: json['thumb_url'] as String?, + text: json['text'] as String?, + pretext: json['pretext'] as String?, + ogScrapeUrl: json['og_scrape_url'] as String?, + imageUrl: json['image_url'] as String?, + footerIcon: json['footer_icon'] as String?, + footer: json['footer'] as String?, + fields: json['fields'], + fallback: json['fallback'] as String?, + color: json['color'] as String?, + authorName: json['author_name'] as String?, + authorLink: json['author_link'] as String?, + authorIcon: json['author_icon'] as String?, + assetUrl: json['asset_url'] as String?, + actions: + (json['actions'] as List?)?.map((e) => Action.fromJson(e as Map)).toList() ?? const [], + originalWidth: (json['original_width'] as num?)?.toInt(), + originalHeight: (json['original_height'] as num?)?.toInt(), + extraData: json['extra_data'] as Map? ?? const {}, + file: json['file'] == null ? null : AttachmentFile.fromJson(json['file'] as Map), + uploadState: json['upload_state'] == null + ? const UploadState.success() + : UploadState.fromJson(json['upload_state'] as Map), +); -Map _$AttachmentToJson(Attachment instance) => - { - if (AttachmentType.toJson(instance.type) case final value?) 'type': value, - if (instance.titleLink case final value?) 'title_link': value, - if (instance.title case final value?) 'title': value, - if (instance.thumbUrl case final value?) 'thumb_url': value, - if (instance.text case final value?) 'text': value, - if (instance.pretext case final value?) 'pretext': value, - if (instance.ogScrapeUrl case final value?) 'og_scrape_url': value, - if (instance.imageUrl case final value?) 'image_url': value, - if (instance.footerIcon case final value?) 'footer_icon': value, - if (instance.footer case final value?) 'footer': value, - if (instance.fields case final value?) 'fields': value, - if (instance.fallback case final value?) 'fallback': value, - if (instance.color case final value?) 'color': value, - if (instance.authorName case final value?) 'author_name': value, - if (instance.authorLink case final value?) 'author_link': value, - if (instance.authorIcon case final value?) 'author_icon': value, - if (instance.assetUrl case final value?) 'asset_url': value, - if (instance.actions?.map((e) => e.toJson()).toList() case final value?) - 'actions': value, - if (instance.originalWidth case final value?) 'original_width': value, - if (instance.originalHeight case final value?) 'original_height': value, - if (instance.file?.toJson() case final value?) 'file': value, - 'upload_state': instance.uploadState.toJson(), - 'extra_data': instance.extraData, - 'id': instance.id, - }; +Map _$AttachmentToJson(Attachment instance) => { + if (AttachmentType.toJson(instance.type) case final value?) 'type': value, + if (instance.titleLink case final value?) 'title_link': value, + if (instance.title case final value?) 'title': value, + if (instance.thumbUrl case final value?) 'thumb_url': value, + if (instance.text case final value?) 'text': value, + if (instance.pretext case final value?) 'pretext': value, + if (instance.ogScrapeUrl case final value?) 'og_scrape_url': value, + if (instance.imageUrl case final value?) 'image_url': value, + if (instance.footerIcon case final value?) 'footer_icon': value, + if (instance.footer case final value?) 'footer': value, + if (instance.fields case final value?) 'fields': value, + if (instance.fallback case final value?) 'fallback': value, + if (instance.color case final value?) 'color': value, + if (instance.authorName case final value?) 'author_name': value, + if (instance.authorLink case final value?) 'author_link': value, + if (instance.authorIcon case final value?) 'author_icon': value, + if (instance.assetUrl case final value?) 'asset_url': value, + if (instance.actions?.map((e) => e.toJson()).toList() case final value?) 'actions': value, + if (instance.originalWidth case final value?) 'original_width': value, + if (instance.originalHeight case final value?) 'original_height': value, + if (instance.file?.toJson() case final value?) 'file': value, + 'upload_state': instance.uploadState.toJson(), + 'extra_data': instance.extraData, + 'id': instance.id, +}; diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.dart b/packages/stream_chat/lib/src/core/models/attachment_file.dart index b5bb007633..9d476090fa 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.dart @@ -19,23 +19,22 @@ class AttachmentFile { this.path, String? name, this.bytes, - }) : assert( - path != null || bytes != null, - 'Either path or bytes should be != null', - ), - assert( - !CurrentPlatform.isWeb || bytes != null, - 'File by path is not supported in web, Please provide bytes', - ), - assert( - name == null || name.isEmpty || name.contains('.'), - 'Invalid file name, should also contain file extension', - ), - _name = name; + }) : assert( + path != null || bytes != null, + 'Either path or bytes should be != null', + ), + assert( + !CurrentPlatform.isWeb || bytes != null, + 'File by path is not supported in web, Please provide bytes', + ), + assert( + name == null || name.isEmpty || name.contains('.'), + 'Invalid file name, should also contain file extension', + ), + _name = name; /// Create a new instance from a json - factory AttachmentFile.fromJson(Map json) => - _$AttachmentFileFromJson(json); + factory AttachmentFile.fromJson(Map json) => _$AttachmentFileFromJson(json); /// The absolute path for a cached copy of this file. It can be used to /// create a file instance with a descriptor for the given path. @@ -73,15 +72,15 @@ class AttachmentFile { Future toMultipartFile() async { return switch (CurrentPlatform.type) { PlatformType.web => MultipartFile.fromBytes( - bytes!, - filename: name, - contentType: mediaType, - ), + bytes!, + filename: name, + contentType: mediaType, + ), _ => await MultipartFile.fromFile( - path!, - filename: name, - contentType: mediaType, - ), + path!, + filename: name, + contentType: mediaType, + ), }; } @@ -124,8 +123,7 @@ sealed class UploadState with _$UploadState { const factory UploadState.failed({required String error}) = Failed; /// Creates a new instance from a json - factory UploadState.fromJson(Map json) => - _$UploadStateFromJson(json); + factory UploadState.fromJson(Map json) => _$UploadStateFromJson(json); /// Returns true if state is [Preparing] bool get isPreparing => this is Preparing; diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.g.dart b/packages/stream_chat/lib/src/core/models/attachment_file.g.dart index 256713cae0..7ebe7ea111 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.g.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.g.dart @@ -6,55 +6,52 @@ part of 'attachment_file.dart'; // JsonSerializableGenerator // ************************************************************************** -AttachmentFile _$AttachmentFileFromJson(Map json) => - AttachmentFile( - size: (json['size'] as num?)?.toInt(), - path: json['path'] as String?, - name: json['name'] as String?, - ); - -Map _$AttachmentFileToJson(AttachmentFile instance) => - { - 'path': instance.path, - 'name': instance.name, - 'size': instance.size, - }; +AttachmentFile _$AttachmentFileFromJson(Map json) => AttachmentFile( + size: (json['size'] as num?)?.toInt(), + path: json['path'] as String?, + name: json['name'] as String?, +); + +Map _$AttachmentFileToJson(AttachmentFile instance) => { + 'path': instance.path, + 'name': instance.name, + 'size': instance.size, +}; Preparing _$PreparingFromJson(Map json) => Preparing( - $type: json['runtimeType'] as String?, - ); + $type: json['runtimeType'] as String?, +); Map _$PreparingToJson(Preparing instance) => { - 'runtimeType': instance.$type, - }; + 'runtimeType': instance.$type, +}; InProgress _$InProgressFromJson(Map json) => InProgress( - uploaded: (json['uploaded'] as num).toInt(), - total: (json['total'] as num).toInt(), - $type: json['runtimeType'] as String?, - ); - -Map _$InProgressToJson(InProgress instance) => - { - 'uploaded': instance.uploaded, - 'total': instance.total, - 'runtimeType': instance.$type, - }; + uploaded: (json['uploaded'] as num).toInt(), + total: (json['total'] as num).toInt(), + $type: json['runtimeType'] as String?, +); + +Map _$InProgressToJson(InProgress instance) => { + 'uploaded': instance.uploaded, + 'total': instance.total, + 'runtimeType': instance.$type, +}; Success _$SuccessFromJson(Map json) => Success( - $type: json['runtimeType'] as String?, - ); + $type: json['runtimeType'] as String?, +); Map _$SuccessToJson(Success instance) => { - 'runtimeType': instance.$type, - }; + 'runtimeType': instance.$type, +}; Failed _$FailedFromJson(Map json) => Failed( - error: json['error'] as String, - $type: json['runtimeType'] as String?, - ); + error: json['error'] as String, + $type: json['runtimeType'] as String?, +); Map _$FailedToJson(Failed instance) => { - 'error': instance.error, - 'runtimeType': instance.$type, - }; + 'error': instance.error, + 'runtimeType': instance.$type, +}; diff --git a/packages/stream_chat/lib/src/core/models/attachment_giphy_info.dart b/packages/stream_chat/lib/src/core/models/attachment_giphy_info.dart index bc5d59a326..26ffbb9bce 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_giphy_info.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_giphy_info.dart @@ -17,7 +17,8 @@ enum GiphyInfoType { /// Lower quality with a fixed height with width adjusted according to the /// aspect ratio and played at a lower frame rate. Significantly lower size, /// but visually less appealing. - fixedHeightDownsampled('fixed_height_downsampled'); + fixedHeightDownsampled('fixed_height_downsampled') + ; /// {@macro giphy_info_type} const GiphyInfoType(this.value); diff --git a/packages/stream_chat/lib/src/core/models/banned_user.dart b/packages/stream_chat/lib/src/core/models/banned_user.dart index 4f25ac75fe..7717593b1a 100644 --- a/packages/stream_chat/lib/src/core/models/banned_user.dart +++ b/packages/stream_chat/lib/src/core/models/banned_user.dart @@ -21,8 +21,7 @@ class BannedUser extends Equatable implements ComparableFieldProvider { }); /// Create a new instance from a json - factory BannedUser.fromJson(Map json) => - _$BannedUserFromJson(json); + factory BannedUser.fromJson(Map json) => _$BannedUserFromJson(json); /// Banned user. final User user; @@ -57,27 +56,26 @@ class BannedUser extends Equatable implements ComparableFieldProvider { DateTime? expires, bool? shadow, String? reason, - }) => - BannedUser( - user: user ?? this.user, - bannedBy: bannedBy ?? this.bannedBy, - channel: channel ?? this.channel, - createdAt: createdAt ?? this.createdAt, - expires: expires ?? this.expires, - shadow: shadow ?? this.shadow, - reason: reason ?? this.reason, - ); + }) => BannedUser( + user: user ?? this.user, + bannedBy: bannedBy ?? this.bannedBy, + channel: channel ?? this.channel, + createdAt: createdAt ?? this.createdAt, + expires: expires ?? this.expires, + shadow: shadow ?? this.shadow, + reason: reason ?? this.reason, + ); @override List get props => [ - user, - bannedBy, - channel, - createdAt, - expires, - shadow, - reason, - ]; + user, + bannedBy, + channel, + createdAt, + expires, + shadow, + reason, + ]; @override ComparableField? getComparableField(String sortKey) { diff --git a/packages/stream_chat/lib/src/core/models/banned_user.g.dart b/packages/stream_chat/lib/src/core/models/banned_user.g.dart index 1f2a335b34..024a8fd912 100644 --- a/packages/stream_chat/lib/src/core/models/banned_user.g.dart +++ b/packages/stream_chat/lib/src/core/models/banned_user.g.dart @@ -7,30 +7,21 @@ part of 'banned_user.dart'; // ************************************************************************** BannedUser _$BannedUserFromJson(Map json) => BannedUser( - user: User.fromJson(json['user'] as Map), - bannedBy: json['banned_by'] == null - ? null - : User.fromJson(json['banned_by'] as Map), - channel: json['channel'] == null - ? null - : ChannelModel.fromJson(json['channel'] as Map), - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), - expires: json['expires'] == null - ? null - : DateTime.parse(json['expires'] as String), - shadow: json['shadow'] as bool? ?? false, - reason: json['reason'] as String?, - ); + user: User.fromJson(json['user'] as Map), + bannedBy: json['banned_by'] == null ? null : User.fromJson(json['banned_by'] as Map), + channel: json['channel'] == null ? null : ChannelModel.fromJson(json['channel'] as Map), + createdAt: json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), + expires: json['expires'] == null ? null : DateTime.parse(json['expires'] as String), + shadow: json['shadow'] as bool? ?? false, + reason: json['reason'] as String?, +); -Map _$BannedUserToJson(BannedUser instance) => - { - 'user': instance.user.toJson(), - 'banned_by': instance.bannedBy?.toJson(), - 'channel': instance.channel?.toJson(), - 'created_at': instance.createdAt?.toIso8601String(), - 'expires': instance.expires?.toIso8601String(), - 'shadow': instance.shadow, - 'reason': instance.reason, - }; +Map _$BannedUserToJson(BannedUser instance) => { + 'user': instance.user.toJson(), + 'banned_by': instance.bannedBy?.toJson(), + 'channel': instance.channel?.toJson(), + 'created_at': instance.createdAt?.toIso8601String(), + 'expires': instance.expires?.toIso8601String(), + 'shadow': instance.shadow, + 'reason': instance.reason, +}; 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 40c11630ec..32e4760d6c 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.dart @@ -28,12 +28,12 @@ class ChannelConfig { this.userMessageReminders = false, this.markMessagesPending = false, this.deliveryEvents = false, - }) : createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(); + this.sharedLocations = false, + }) : createdAt = createdAt ?? DateTime.now(), + updatedAt = updatedAt ?? DateTime.now(); /// Create a new instance from a json - factory ChannelConfig.fromJson(Map json) => - _$ChannelConfigFromJson(json); + factory ChannelConfig.fromJson(Map json) => _$ChannelConfigFromJson(json); /// Moderation configuration final String automod; @@ -99,6 +99,9 @@ class ChannelConfig { /// Whether delivery events are enabled for this channel. final bool deliveryEvents; + /// 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 38b2d0d24f..9bae9b1ba1 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 @@ -6,59 +6,52 @@ part of 'channel_config.dart'; // JsonSerializableGenerator // ************************************************************************** -ChannelConfig _$ChannelConfigFromJson(Map json) => - ChannelConfig( - automod: json['automod'] as String? ?? 'flag', - commands: (json['commands'] as List?) - ?.map((e) => Command.fromJson(e as Map)) - .toList() ?? - const [], - connectEvents: json['connect_events'] as bool? ?? false, - 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), - maxMessageLength: (json['max_message_length'] as num?)?.toInt() ?? 0, - messageRetention: json['message_retention'] as String? ?? '', - mutes: json['mutes'] as bool? ?? false, - reactions: json['reactions'] as bool? ?? false, - readEvents: json['read_events'] as bool? ?? false, - replies: json['replies'] as bool? ?? false, - search: json['search'] as bool? ?? false, - polls: json['polls'] as bool? ?? false, - typingEvents: json['typing_events'] as bool? ?? false, - uploads: json['uploads'] as bool? ?? false, - urlEnrichment: json['url_enrichment'] as bool? ?? false, - skipLastMsgUpdateForSystemMsgs: - json['skip_last_msg_update_for_system_msgs'] as bool? ?? false, - userMessageReminders: json['user_message_reminders'] as bool? ?? false, - markMessagesPending: json['mark_messages_pending'] as bool? ?? false, - deliveryEvents: json['delivery_events'] as bool? ?? false, - ); +ChannelConfig _$ChannelConfigFromJson(Map json) => ChannelConfig( + automod: json['automod'] as String? ?? 'flag', + commands: + (json['commands'] as List?)?.map((e) => Command.fromJson(e as Map)).toList() ?? + const [], + connectEvents: json['connect_events'] as bool? ?? false, + 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), + maxMessageLength: (json['max_message_length'] as num?)?.toInt() ?? 0, + messageRetention: json['message_retention'] as String? ?? '', + mutes: json['mutes'] as bool? ?? false, + reactions: json['reactions'] as bool? ?? false, + readEvents: json['read_events'] as bool? ?? false, + replies: json['replies'] as bool? ?? false, + search: json['search'] as bool? ?? false, + polls: json['polls'] as bool? ?? false, + typingEvents: json['typing_events'] as bool? ?? false, + uploads: json['uploads'] as bool? ?? false, + urlEnrichment: json['url_enrichment'] as bool? ?? false, + skipLastMsgUpdateForSystemMsgs: json['skip_last_msg_update_for_system_msgs'] as bool? ?? false, + userMessageReminders: json['user_message_reminders'] as bool? ?? false, + markMessagesPending: json['mark_messages_pending'] as bool? ?? false, + deliveryEvents: json['delivery_events'] as bool? ?? false, + sharedLocations: json['shared_locations'] as bool? ?? false, +); -Map _$ChannelConfigToJson(ChannelConfig instance) => - { - 'automod': instance.automod, - 'commands': instance.commands.map((e) => e.toJson()).toList(), - 'connect_events': instance.connectEvents, - 'created_at': instance.createdAt.toIso8601String(), - 'updated_at': instance.updatedAt.toIso8601String(), - 'max_message_length': instance.maxMessageLength, - 'message_retention': instance.messageRetention, - 'mutes': instance.mutes, - 'reactions': instance.reactions, - 'read_events': instance.readEvents, - 'replies': instance.replies, - 'search': instance.search, - 'polls': instance.polls, - 'typing_events': instance.typingEvents, - 'uploads': instance.uploads, - 'url_enrichment': instance.urlEnrichment, - 'skip_last_msg_update_for_system_msgs': - instance.skipLastMsgUpdateForSystemMsgs, - 'user_message_reminders': instance.userMessageReminders, - 'mark_messages_pending': instance.markMessagesPending, - 'delivery_events': instance.deliveryEvents, - }; +Map _$ChannelConfigToJson(ChannelConfig instance) => { + 'automod': instance.automod, + 'commands': instance.commands.map((e) => e.toJson()).toList(), + 'connect_events': instance.connectEvents, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'max_message_length': instance.maxMessageLength, + 'message_retention': instance.messageRetention, + 'mutes': instance.mutes, + 'reactions': instance.reactions, + 'read_events': instance.readEvents, + 'replies': instance.replies, + 'search': instance.search, + 'polls': instance.polls, + 'typing_events': instance.typingEvents, + 'uploads': instance.uploads, + 'url_enrichment': instance.urlEnrichment, + 'skip_last_msg_update_for_system_msgs': instance.skipLastMsgUpdateForSystemMsgs, + 'user_message_reminders': instance.userMessageReminders, + 'mark_messages_pending': instance.markMessagesPending, + 'delivery_events': instance.deliveryEvents, + '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 04b1096503..846b91958e 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -33,33 +33,31 @@ class ChannelModel { DateTime? truncatedAt, this.messageCount, this.filterTags, - }) : assert( - (cid != null && cid.contains(':')) || (id != null && type != null), - 'provide either a cid or an id and type', - ), - id = id ?? cid!.split(':')[1], - type = type ?? cid!.split(':')[0], - cid = cid ?? '$type:$id', - config = config ?? ChannelConfig(), - createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(), - ownCapabilities = ownCapabilities?.map(ChannelCapability.new).toList(), - - // For backwards compatibility, set 'disabled', 'hidden' - // and 'truncated_at' in [extraData]. - extraData = { - ...extraData, - if (disabled != null) 'disabled': disabled, - if (hidden != null) 'hidden': hidden, - if (truncatedAt != null) - 'truncated_at': truncatedAt.toIso8601String(), - }; + }) : assert( + (cid != null && cid.contains(':')) || (id != null && type != null), + 'provide either a cid or an id and type', + ), + id = id ?? cid!.split(':')[1], + type = type ?? cid!.split(':')[0], + cid = cid ?? '$type:$id', + config = config ?? ChannelConfig(), + createdAt = createdAt ?? DateTime.now(), + updatedAt = updatedAt ?? DateTime.now(), + ownCapabilities = ownCapabilities?.map(ChannelCapability.new).toList(), + + // For backwards compatibility, set 'disabled', 'hidden' + // and 'truncated_at' in [extraData]. + extraData = { + ...extraData, + if (disabled != null) 'disabled': disabled, + if (hidden != null) 'hidden': hidden, + if (truncatedAt != null) 'truncated_at': truncatedAt.toIso8601String(), + }; /// Create a new instance from a json - factory ChannelModel.fromJson(Map json) => - _$ChannelModelFromJson( - Serializer.moveToExtraDataFromRoot(json, topLevelFields), - ); + factory ChannelModel.fromJson(Map json) => _$ChannelModelFromJson( + Serializer.moveToExtraDataFromRoot(json, topLevelFields), + ); /// The id of this channel final String id; @@ -190,8 +188,8 @@ class ChannelModel { /// Serialize to json Map toJson() => Serializer.moveFromExtraDataToRoot( - _$ChannelModelToJson(this), - ); + _$ChannelModelToJson(this), + ); /// Creates a copy of [ChannelModel] with specified attributes overridden. ChannelModel copyWith({ @@ -216,35 +214,35 @@ class ChannelModel { DateTime? truncatedAt, int? messageCount, List? filterTags, - }) => - ChannelModel( - id: id ?? this.id, - type: type ?? this.type, - cid: cid ?? this.cid, - ownCapabilities: ownCapabilities ?? this.ownCapabilities, - config: config ?? this.config, - createdBy: createdBy ?? this.createdBy, - frozen: frozen ?? this.frozen, - lastMessageAt: lastMessageAt ?? this.lastMessageAt, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - deletedAt: deletedAt ?? this.deletedAt, - memberCount: memberCount ?? this.memberCount, - members: members ?? this.members, - extraData: extraData ?? this.extraData, - team: team ?? this.team, - cooldown: cooldown ?? this.cooldown, - disabled: disabled ?? extraData?['disabled'] as bool? ?? this.disabled, - hidden: hidden ?? extraData?['hidden'] as bool? ?? this.hidden, - truncatedAt: truncatedAt ?? - (extraData?['truncated_at'] == null - ? null - // ignore: cast_nullable_to_non_nullable - : DateTime.parse(extraData?['truncated_at'] as String)) ?? - this.truncatedAt, - messageCount: messageCount ?? this.messageCount, - filterTags: filterTags ?? this.filterTags, - ); + }) => ChannelModel( + id: id ?? this.id, + type: type ?? this.type, + cid: cid ?? this.cid, + ownCapabilities: ownCapabilities ?? this.ownCapabilities, + config: config ?? this.config, + createdBy: createdBy ?? this.createdBy, + frozen: frozen ?? this.frozen, + lastMessageAt: lastMessageAt ?? this.lastMessageAt, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + memberCount: memberCount ?? this.memberCount, + members: members ?? this.members, + extraData: extraData ?? this.extraData, + team: team ?? this.team, + cooldown: cooldown ?? this.cooldown, + disabled: disabled ?? extraData?['disabled'] as bool? ?? this.disabled, + hidden: hidden ?? extraData?['hidden'] as bool? ?? this.hidden, + truncatedAt: + truncatedAt ?? + (extraData?['truncated_at'] == null + ? null + // ignore: cast_nullable_to_non_nullable + : DateTime.parse(extraData?['truncated_at'] as String)) ?? + this.truncatedAt, + messageCount: messageCount ?? this.messageCount, + filterTags: filterTags ?? this.filterTags, + ); /// Returns a new [ChannelModel] that is a combination of this channelModel /// and the given [other] channelModel. @@ -393,4 +391,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_model.g.dart b/packages/stream_chat/lib/src/core/models/channel_model.g.dart index cc64ccc034..d2c1ce62eb 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.g.dart @@ -7,49 +7,30 @@ part of 'channel_model.dart'; // ************************************************************************** ChannelModel _$ChannelModelFromJson(Map json) => ChannelModel( - id: json['id'] as String?, - type: json['type'] as String?, - cid: json['cid'] as String?, - ownCapabilities: (json['own_capabilities'] as List?) - ?.map((e) => e as String) - .toList(), - config: json['config'] == null - ? null - : ChannelConfig.fromJson(json['config'] as Map), - createdBy: json['created_by'] == null - ? null - : User.fromJson(json['created_by'] as Map), - frozen: json['frozen'] as bool? ?? false, - lastMessageAt: json['last_message_at'] == null - ? null - : DateTime.parse(json['last_message_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), - deletedAt: json['deleted_at'] == null - ? null - : DateTime.parse(json['deleted_at'] as String), - memberCount: (json['member_count'] as num?)?.toInt() ?? 0, - members: (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList(), - extraData: json['extra_data'] as Map? ?? const {}, - team: json['team'] as String?, - cooldown: (json['cooldown'] as num?)?.toInt() ?? 0, - messageCount: (json['message_count'] as num?)?.toInt(), - filterTags: (json['filter_tags'] as List?) - ?.map((e) => e as String) - .toList(), - ); + id: json['id'] as String?, + type: json['type'] as String?, + cid: json['cid'] as String?, + ownCapabilities: (json['own_capabilities'] as List?)?.map((e) => e as String).toList(), + config: json['config'] == null ? null : ChannelConfig.fromJson(json['config'] as Map), + createdBy: json['created_by'] == null ? null : User.fromJson(json['created_by'] as Map), + frozen: json['frozen'] as bool? ?? false, + lastMessageAt: json['last_message_at'] == null ? null : DateTime.parse(json['last_message_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), + deletedAt: json['deleted_at'] == null ? null : DateTime.parse(json['deleted_at'] as String), + memberCount: (json['member_count'] as num?)?.toInt() ?? 0, + members: (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList(), + extraData: json['extra_data'] as Map? ?? const {}, + team: json['team'] as String?, + cooldown: (json['cooldown'] as num?)?.toInt() ?? 0, + messageCount: (json['message_count'] as num?)?.toInt(), + filterTags: (json['filter_tags'] as List?)?.map((e) => e as String).toList(), +); -Map _$ChannelModelToJson(ChannelModel instance) => - { - 'id': instance.id, - 'type': instance.type, - 'frozen': instance.frozen, - 'cooldown': instance.cooldown, - 'extra_data': instance.extraData, - }; +Map _$ChannelModelToJson(ChannelModel instance) => { + 'id': instance.id, + 'type': instance.type, + 'frozen': instance.frozen, + 'cooldown': instance.cooldown, + 'extra_data': instance.extraData, +}; diff --git a/packages/stream_chat/lib/src/core/models/channel_mute.dart b/packages/stream_chat/lib/src/core/models/channel_mute.dart index c0d29a57c0..a393ae14da 100644 --- a/packages/stream_chat/lib/src/core/models/channel_mute.dart +++ b/packages/stream_chat/lib/src/core/models/channel_mute.dart @@ -17,8 +17,7 @@ class ChannelMute { }); /// Create a new instance from a json - factory ChannelMute.fromJson(Map json) => - _$ChannelMuteFromJson(json); + factory ChannelMute.fromJson(Map json) => _$ChannelMuteFromJson(json); /// The user that performed the muting action final User user; diff --git a/packages/stream_chat/lib/src/core/models/channel_mute.g.dart b/packages/stream_chat/lib/src/core/models/channel_mute.g.dart index f0b973fcbf..4b3576c0ae 100644 --- a/packages/stream_chat/lib/src/core/models/channel_mute.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_mute.g.dart @@ -7,20 +7,17 @@ part of 'channel_mute.dart'; // ************************************************************************** ChannelMute _$ChannelMuteFromJson(Map json) => ChannelMute( - user: User.fromJson(json['user'] as Map), - channel: ChannelModel.fromJson(json['channel'] as Map), - createdAt: DateTime.parse(json['created_at'] as String), - updatedAt: DateTime.parse(json['updated_at'] as String), - expires: json['expires'] == null - ? null - : DateTime.parse(json['expires'] as String), - ); + user: User.fromJson(json['user'] as Map), + channel: ChannelModel.fromJson(json['channel'] as Map), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + expires: json['expires'] == null ? null : DateTime.parse(json['expires'] as String), +); -Map _$ChannelMuteToJson(ChannelMute instance) => - { - 'user': instance.user.toJson(), - 'channel': instance.channel.toJson(), - 'created_at': instance.createdAt.toIso8601String(), - 'updated_at': instance.updatedAt.toIso8601String(), - 'expires': instance.expires?.toIso8601String(), - }; +Map _$ChannelMuteToJson(ChannelMute instance) => { + 'user': instance.user.toJson(), + 'channel': instance.channel.toJson(), + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'expires': instance.expires?.toIso8601String(), +}; 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 e3dc81ad27..718e4b74f5 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'; @@ -32,6 +33,7 @@ class ChannelState implements ComparableFieldProvider { this.draft, this.pendingMessages, this.pushPreferences, + this.activeLiveLocations, }); /// The channel to which this state belongs @@ -86,9 +88,11 @@ 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); + static ChannelState fromJson(Map json) => _$ChannelStateFromJson(json); /// Serialize to json Map toJson() => _$ChannelStateToJson(this); @@ -106,20 +110,21 @@ class ChannelState implements ComparableFieldProvider { Object? draft = _nullConst, List? pendingMessages, ChannelPushPreference? pushPreferences, - }) => - ChannelState( - channel: channel ?? this.channel, - messages: messages ?? this.messages, - members: members ?? this.members, - pinnedMessages: pinnedMessages ?? this.pinnedMessages, - watcherCount: watcherCount ?? this.watcherCount, - watchers: watchers ?? this.watchers, - read: read ?? this.read, - membership: membership ?? this.membership, - draft: draft == _nullConst ? this.draft : draft as Draft?, - pendingMessages: pendingMessages ?? this.pendingMessages, - pushPreferences: pushPreferences ?? this.pushPreferences, - ); + List? activeLiveLocations, + }) => ChannelState( + channel: channel ?? this.channel, + messages: messages ?? this.messages, + members: members ?? this.members, + pinnedMessages: pinnedMessages ?? this.pinnedMessages, + watcherCount: watcherCount ?? this.watcherCount, + watchers: watchers ?? this.watchers, + read: read ?? this.read, + membership: membership ?? this.membership, + draft: draft == _nullConst ? this.draft : draft as Draft?, + pendingMessages: pendingMessages ?? this.pendingMessages, + pushPreferences: pushPreferences ?? this.pushPreferences, + activeLiveLocations: activeLiveLocations ?? this.activeLiveLocations, + ); @override ComparableField? getComparableField(String sortKey) { 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 b7499ba8b9..a3c6c2692d 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 @@ -7,55 +7,39 @@ part of 'channel_state.dart'; // ************************************************************************** ChannelState _$ChannelStateFromJson(Map json) => ChannelState( - channel: json['channel'] == null - ? null - : ChannelModel.fromJson(json['channel'] as Map), - messages: (json['messages'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList(), - members: (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList(), - pinnedMessages: (json['pinned_messages'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList(), - watcherCount: (json['watcher_count'] as num?)?.toInt(), - watchers: (json['watchers'] as List?) - ?.map((e) => User.fromJson(e as Map)) - .toList(), - read: (json['read'] as List?) - ?.map((e) => Read.fromJson(e as Map)) - .toList(), - membership: json['membership'] == null - ? null - : Member.fromJson(json['membership'] as Map), - draft: json['draft'] == null - ? null - : Draft.fromJson(json['draft'] as Map), - pendingMessages: - (ChannelState._pendingMessagesReadValue(json, 'pending_messages') - as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList(), - pushPreferences: json['push_preferences'] == null - ? null - : ChannelPushPreference.fromJson( - json['push_preferences'] as Map), - ); + channel: json['channel'] == null ? null : ChannelModel.fromJson(json['channel'] as Map), + messages: (json['messages'] as List?)?.map((e) => Message.fromJson(e as Map)).toList(), + members: (json['members'] as List?)?.map((e) => Member.fromJson(e as Map)).toList(), + pinnedMessages: (json['pinned_messages'] as List?) + ?.map((e) => Message.fromJson(e as Map)) + .toList(), + watcherCount: (json['watcher_count'] as num?)?.toInt(), + watchers: (json['watchers'] as List?)?.map((e) => User.fromJson(e as Map)).toList(), + read: (json['read'] as List?)?.map((e) => Read.fromJson(e as Map)).toList(), + membership: json['membership'] == null ? null : Member.fromJson(json['membership'] as Map), + draft: json['draft'] == null ? null : Draft.fromJson(json['draft'] as Map), + pendingMessages: (ChannelState._pendingMessagesReadValue(json, 'pending_messages') as List?) + ?.map((e) => Message.fromJson(e as Map)) + .toList(), + pushPreferences: json['push_preferences'] == null + ? 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) => - { - 'channel': instance.channel?.toJson(), - 'messages': instance.messages?.map((e) => e.toJson()).toList(), - 'members': instance.members?.map((e) => e.toJson()).toList(), - 'pinned_messages': - instance.pinnedMessages?.map((e) => e.toJson()).toList(), - 'watcher_count': instance.watcherCount, - 'watchers': instance.watchers?.map((e) => e.toJson()).toList(), - 'read': instance.read?.map((e) => e.toJson()).toList(), - 'membership': instance.membership?.toJson(), - 'draft': instance.draft?.toJson(), - 'pending_messages': - instance.pendingMessages?.map((e) => e.toJson()).toList(), - 'push_preferences': instance.pushPreferences?.toJson(), - }; +Map _$ChannelStateToJson(ChannelState instance) => { + 'channel': instance.channel?.toJson(), + 'messages': instance.messages?.map((e) => e.toJson()).toList(), + 'members': instance.members?.map((e) => e.toJson()).toList(), + 'pinned_messages': instance.pinnedMessages?.map((e) => e.toJson()).toList(), + 'watcher_count': instance.watcherCount, + 'watchers': instance.watchers?.map((e) => e.toJson()).toList(), + 'read': instance.read?.map((e) => e.toJson()).toList(), + 'membership': instance.membership?.toJson(), + 'draft': instance.draft?.toJson(), + 'pending_messages': instance.pendingMessages?.map((e) => e.toJson()).toList(), + '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/command.dart b/packages/stream_chat/lib/src/core/models/command.dart index 5ba0043c18..0247637560 100644 --- a/packages/stream_chat/lib/src/core/models/command.dart +++ b/packages/stream_chat/lib/src/core/models/command.dart @@ -13,8 +13,7 @@ class Command { }); /// Create a new instance from a json - factory Command.fromJson(Map json) => - _$CommandFromJson(json); + factory Command.fromJson(Map json) => _$CommandFromJson(json); /// The name of the command final String name; diff --git a/packages/stream_chat/lib/src/core/models/command.g.dart b/packages/stream_chat/lib/src/core/models/command.g.dart index 0fe9d54c6e..24c9362156 100644 --- a/packages/stream_chat/lib/src/core/models/command.g.dart +++ b/packages/stream_chat/lib/src/core/models/command.g.dart @@ -7,13 +7,13 @@ part of 'command.dart'; // ************************************************************************** Command _$CommandFromJson(Map json) => Command( - name: json['name'] as String, - description: json['description'] as String, - args: json['args'] as String, - ); + name: json['name'] as String, + description: json['description'] as String, + args: json['args'] as String, +); Map _$CommandToJson(Command instance) => { - 'name': instance.name, - 'description': instance.description, - 'args': instance.args, - }; + 'name': instance.name, + 'description': instance.description, + 'args': instance.args, +}; diff --git a/packages/stream_chat/lib/src/core/models/comparable_field.dart b/packages/stream_chat/lib/src/core/models/comparable_field.dart index f43df58a33..5b192541b2 100644 --- a/packages/stream_chat/lib/src/core/models/comparable_field.dart +++ b/packages/stream_chat/lib/src/core/models/comparable_field.dart @@ -28,7 +28,7 @@ class ComparableField implements Comparable> { (final DateTime a, final DateTime b) => a.compareTo(b), (final bool a, final bool b) when a == b => 0, (final bool a, final bool b) => a && !b ? 1 : -1, // true > false - _ => 0 // All comparisons were equal or incomparable types + _ => 0, // All comparisons were equal or incomparable types }; } } diff --git a/packages/stream_chat/lib/src/core/models/device.g.dart b/packages/stream_chat/lib/src/core/models/device.g.dart index 4de1f064af..2725a082f1 100644 --- a/packages/stream_chat/lib/src/core/models/device.g.dart +++ b/packages/stream_chat/lib/src/core/models/device.g.dart @@ -7,11 +7,11 @@ part of 'device.dart'; // ************************************************************************** Device _$DeviceFromJson(Map json) => Device( - id: json['id'] as String, - pushProvider: json['push_provider'] as String, - ); + id: json['id'] as String, + pushProvider: json['push_provider'] as String, +); Map _$DeviceToJson(Device instance) => { - 'id': instance.id, - 'push_provider': instance.pushProvider, - }; + 'id': instance.id, + 'push_provider': instance.pushProvider, +}; diff --git a/packages/stream_chat/lib/src/core/models/draft.dart b/packages/stream_chat/lib/src/core/models/draft.dart index fa4520114c..c6fb6139b2 100644 --- a/packages/stream_chat/lib/src/core/models/draft.dart +++ b/packages/stream_chat/lib/src/core/models/draft.dart @@ -73,14 +73,14 @@ class Draft extends Equatable implements ComparableFieldProvider { @override List get props => [ - channelCid, - createdAt, - message, - channel, - parentId, - parentMessage, - quotedMessage, - ]; + channelCid, + createdAt, + message, + channel, + parentId, + parentMessage, + quotedMessage, + ]; @override ComparableField? getComparableField(String sortKey) { diff --git a/packages/stream_chat/lib/src/core/models/draft.g.dart b/packages/stream_chat/lib/src/core/models/draft.g.dart index 9a7b7de9ce..1ea7feafc3 100644 --- a/packages/stream_chat/lib/src/core/models/draft.g.dart +++ b/packages/stream_chat/lib/src/core/models/draft.g.dart @@ -7,29 +7,25 @@ part of 'draft.dart'; // ************************************************************************** Draft _$DraftFromJson(Map json) => Draft( - channelCid: json['channel_cid'] as String, - createdAt: DateTime.parse(json['created_at'] as String), - message: DraftMessage.fromJson(json['message'] as Map), - channel: json['channel'] == null - ? null - : ChannelModel.fromJson(json['channel'] as Map), - parentId: json['parent_id'] as String?, - parentMessage: json['parent_message'] == null - ? null - : Message.fromJson(json['parent_message'] as Map), - quotedMessage: json['quoted_message'] == null - ? null - : Message.fromJson(json['quoted_message'] as Map), - ); + channelCid: json['channel_cid'] as String, + createdAt: DateTime.parse(json['created_at'] as String), + message: DraftMessage.fromJson(json['message'] as Map), + channel: json['channel'] == null ? null : ChannelModel.fromJson(json['channel'] as Map), + parentId: json['parent_id'] as String?, + parentMessage: json['parent_message'] == null + ? null + : Message.fromJson(json['parent_message'] as Map), + quotedMessage: json['quoted_message'] == null + ? null + : Message.fromJson(json['quoted_message'] as Map), +); Map _$DraftToJson(Draft instance) => { - 'channel_cid': instance.channelCid, - 'created_at': instance.createdAt.toIso8601String(), - 'message': instance.message.toJson(), - if (instance.channel?.toJson() case final value?) 'channel': value, - if (instance.parentId case final value?) 'parent_id': value, - if (instance.parentMessage?.toJson() case final value?) - 'parent_message': value, - if (instance.quotedMessage?.toJson() case final value?) - 'quoted_message': value, - }; + 'channel_cid': instance.channelCid, + 'created_at': instance.createdAt.toIso8601String(), + 'message': instance.message.toJson(), + if (instance.channel?.toJson() case final value?) 'channel': value, + if (instance.parentId case final value?) 'parent_id': value, + if (instance.parentMessage?.toJson() case final value?) 'parent_message': value, + if (instance.quotedMessage?.toJson() case final value?) 'quoted_message': value, +}; diff --git a/packages/stream_chat/lib/src/core/models/draft_message.dart b/packages/stream_chat/lib/src/core/models/draft_message.dart index 13c373475b..b6ae3dede0 100644 --- a/packages/stream_chat/lib/src/core/models/draft_message.dart +++ b/packages/stream_chat/lib/src/core/models/draft_message.dart @@ -28,16 +28,15 @@ class DraftMessage extends Equatable { this.poll, String? pollId, this.extraData = const {}, - }) : id = id ?? const Uuid().v4(), - type = MessageType(type), - _quotedMessageId = quotedMessageId, - _pollId = pollId; + }) : id = id ?? const Uuid().v4(), + type = MessageType(type), + _quotedMessageId = quotedMessageId, + _pollId = pollId; /// Create a new instance from JSON. - factory DraftMessage.fromJson(Map json) => - _$DraftMessageFromJson( - Serializer.moveToExtraDataFromRoot(json, topLevelFields), - ); + factory DraftMessage.fromJson(Map json) => _$DraftMessageFromJson( + Serializer.moveToExtraDataFromRoot(json, topLevelFields), + ); /// The message ID. This is either created by Stream or set client side when /// the message is added. @@ -167,19 +166,19 @@ class DraftMessage extends Equatable { @override List get props => [ - id, - text, - type, - attachments, - parentId, - showInChannel, - mentionedUsers, - quotedMessageId, - silent, - command, - pollId, - extraData, - ]; + id, + text, + type, + attachments, + parentId, + showInChannel, + mentionedUsers, + quotedMessageId, + silent, + command, + pollId, + extraData, + ]; } /// Extension on [Message] to convert it to a [DraftMessage]. diff --git a/packages/stream_chat/lib/src/core/models/draft_message.g.dart b/packages/stream_chat/lib/src/core/models/draft_message.g.dart index 910ca3b090..b1b96bf505 100644 --- a/packages/stream_chat/lib/src/core/models/draft_message.g.dart +++ b/packages/stream_chat/lib/src/core/models/draft_message.g.dart @@ -7,47 +7,38 @@ part of 'draft_message.dart'; // ************************************************************************** DraftMessage _$DraftMessageFromJson(Map json) => DraftMessage( - id: json['id'] as String?, - text: json['text'] as String?, - type: json['type'] == null - ? MessageType.regular - : MessageType.fromJson(json['type'] as String), - attachments: (json['attachments'] as List?) - ?.map((e) => Attachment.fromJson(e as Map)) - .toList() ?? - const [], - parentId: json['parent_id'] as String?, - showInChannel: json['show_in_channel'] as bool?, - mentionedUsers: (json['mentioned_users'] as List?) - ?.map((e) => User.fromJson(e as Map)) - .toList() ?? - const [], - quotedMessage: json['quoted_message'] == null - ? null - : Message.fromJson(json['quoted_message'] as Map), - quotedMessageId: json['quoted_message_id'] as String?, - silent: json['silent'] as bool? ?? false, - command: json['command'] as String?, - poll: json['poll'] == null - ? null - : Poll.fromJson(json['poll'] as Map), - pollId: json['poll_id'] as String?, - extraData: json['extra_data'] as Map? ?? const {}, - ); + id: json['id'] as String?, + text: json['text'] as String?, + type: json['type'] == null ? MessageType.regular : MessageType.fromJson(json['type'] as String), + attachments: + (json['attachments'] as List?)?.map((e) => Attachment.fromJson(e as Map)).toList() ?? + const [], + parentId: json['parent_id'] as String?, + showInChannel: json['show_in_channel'] as bool?, + mentionedUsers: + (json['mentioned_users'] as List?)?.map((e) => User.fromJson(e as Map)).toList() ?? + const [], + quotedMessage: json['quoted_message'] == null + ? null + : Message.fromJson(json['quoted_message'] as Map), + quotedMessageId: json['quoted_message_id'] as String?, + silent: json['silent'] as bool? ?? false, + command: json['command'] as String?, + poll: json['poll'] == null ? null : Poll.fromJson(json['poll'] as Map), + pollId: json['poll_id'] as String?, + extraData: json['extra_data'] as Map? ?? const {}, +); -Map _$DraftMessageToJson(DraftMessage instance) => - { - 'id': instance.id, - if (instance.text case final value?) 'text': value, - if (MessageType.toJson(instance.type) case final value?) 'type': value, - 'attachments': instance.attachments.map((e) => e.toJson()).toList(), - if (instance.parentId case final value?) 'parent_id': value, - if (instance.showInChannel case final value?) 'show_in_channel': value, - if (User.toIds(instance.mentionedUsers) case final value?) - 'mentioned_users': value, - if (instance.quotedMessageId case final value?) - 'quoted_message_id': value, - 'silent': instance.silent, - if (instance.pollId case final value?) 'poll_id': value, - 'extra_data': instance.extraData, - }; +Map _$DraftMessageToJson(DraftMessage instance) => { + 'id': instance.id, + if (instance.text case final value?) 'text': value, + if (MessageType.toJson(instance.type) case final value?) 'type': value, + 'attachments': instance.attachments.map((e) => e.toJson()).toList(), + if (instance.parentId case final value?) 'parent_id': value, + if (instance.showInChannel case final value?) 'show_in_channel': value, + if (User.toIds(instance.mentionedUsers) case final value?) 'mentioned_users': value, + if (instance.quotedMessageId case final value?) 'quoted_message_id': value, + 'silent': instance.silent, + if (instance.pollId case final value?) 'poll_id': value, + 'extra_data': instance.extraData, +}; diff --git a/packages/stream_chat/lib/src/core/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart index 57d2aaeec2..bc2eb27986 100644 --- a/packages/stream_chat/lib/src/core/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -30,6 +30,7 @@ class Event { this.channelLastMessageAt, this.parentId, this.hardDelete, + this.deletedForMe, this.aiState, this.aiMessage, this.messageId, @@ -51,11 +52,12 @@ class Event { }) : createdAt = createdAt?.toUtc() ?? DateTime.now().toUtc(); /// Create a new instance from a json - factory Event.fromJson(Map json) => - _$EventFromJson(Serializer.moveToExtraDataFromRoot( - json, - topLevelFields, - )); + factory Event.fromJson(Map json) => _$EventFromJson( + Serializer.moveToExtraDataFromRoot( + json, + topLevelFields, + ), + ); /// The type of the event /// [EventType] contains some predefined constant types @@ -125,6 +127,9 @@ class Event { /// This is true if the message has been hard deleted final bool? hardDelete; + /// Whether the message was deleted only for the current user. + final bool? deletedForMe; + /// The current state of the AI assistant. @JsonKey(unknownEnumValue: AITypingState.idle) final AITypingState? aiState; @@ -201,6 +206,7 @@ class Event { 'channel_last_message_at', 'parent_id', 'hard_delete', + 'deleted_for_me', 'is_local', 'ai_state', 'ai_message', @@ -222,8 +228,8 @@ class Event { /// Serialize to json Map toJson() => Serializer.moveFromExtraDataToRoot( - _$EventToJson(this), - ); + _$EventToJson(this), + ); /// Creates a copy of [Event] with specified attributes overridden. Event copyWith({ @@ -248,6 +254,7 @@ class Event { bool? online, String? parentId, bool? hardDelete, + bool? deletedForMe, AITypingState? aiState, String? aiMessage, String? messageId, @@ -265,50 +272,48 @@ class Event { DateTime? lastDeliveredAt, String? lastDeliveredMessageId, Map? extraData, - }) => - Event( - type: type ?? this.type, - userId: userId ?? this.userId, - cid: cid ?? this.cid, - connectionId: connectionId ?? this.connectionId, - createdAt: createdAt ?? this.createdAt, - me: me ?? this.me, - user: user ?? this.user, - message: message ?? this.message, - poll: poll ?? this.poll, - pollVote: pollVote ?? this.pollVote, - totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, - unreadChannels: unreadChannels ?? this.unreadChannels, - reaction: reaction ?? this.reaction, - online: online ?? this.online, - channel: channel ?? this.channel, - member: member ?? this.member, - channelId: channelId ?? this.channelId, - channelType: channelType ?? this.channelType, - channelLastMessageAt: channelLastMessageAt ?? this.channelLastMessageAt, - parentId: parentId ?? this.parentId, - hardDelete: hardDelete ?? this.hardDelete, - aiState: aiState ?? this.aiState, - aiMessage: aiMessage ?? this.aiMessage, - messageId: messageId ?? this.messageId, - thread: thread ?? this.thread, - unreadThreadMessages: unreadThreadMessages ?? this.unreadThreadMessages, - unreadThreads: unreadThreads ?? this.unreadThreads, - lastReadAt: lastReadAt ?? this.lastReadAt, - unreadMessages: unreadMessages ?? this.unreadMessages, - lastReadMessageId: lastReadMessageId ?? this.lastReadMessageId, - draft: draft ?? this.draft, - reminder: reminder ?? this.reminder, - pushPreference: pushPreference ?? this.pushPreference, - channelPushPreference: - channelPushPreference ?? this.channelPushPreference, - channelMessageCount: channelMessageCount ?? this.channelMessageCount, - lastDeliveredAt: lastDeliveredAt ?? this.lastDeliveredAt, - lastDeliveredMessageId: - lastDeliveredMessageId ?? this.lastDeliveredMessageId, - isLocal: isLocal, - extraData: extraData ?? this.extraData, - ); + }) => Event( + type: type ?? this.type, + userId: userId ?? this.userId, + cid: cid ?? this.cid, + connectionId: connectionId ?? this.connectionId, + createdAt: createdAt ?? this.createdAt, + me: me ?? this.me, + user: user ?? this.user, + message: message ?? this.message, + poll: poll ?? this.poll, + pollVote: pollVote ?? this.pollVote, + totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, + unreadChannels: unreadChannels ?? this.unreadChannels, + reaction: reaction ?? this.reaction, + online: online ?? this.online, + channel: channel ?? this.channel, + member: member ?? this.member, + channelId: channelId ?? this.channelId, + channelType: channelType ?? this.channelType, + channelLastMessageAt: channelLastMessageAt ?? this.channelLastMessageAt, + parentId: parentId ?? this.parentId, + hardDelete: hardDelete ?? this.hardDelete, + deletedForMe: deletedForMe ?? this.deletedForMe, + aiState: aiState ?? this.aiState, + aiMessage: aiMessage ?? this.aiMessage, + messageId: messageId ?? this.messageId, + thread: thread ?? this.thread, + unreadThreadMessages: unreadThreadMessages ?? this.unreadThreadMessages, + unreadThreads: unreadThreads ?? this.unreadThreads, + lastReadAt: lastReadAt ?? this.lastReadAt, + unreadMessages: unreadMessages ?? this.unreadMessages, + lastReadMessageId: lastReadMessageId ?? this.lastReadMessageId, + draft: draft ?? this.draft, + reminder: reminder ?? this.reminder, + pushPreference: pushPreference ?? this.pushPreference, + channelPushPreference: channelPushPreference ?? this.channelPushPreference, + channelMessageCount: channelMessageCount ?? this.channelMessageCount, + lastDeliveredAt: lastDeliveredAt ?? this.lastDeliveredAt, + lastDeliveredMessageId: lastDeliveredMessageId ?? this.lastDeliveredMessageId, + isLocal: isLocal, + extraData: extraData ?? this.extraData, + ); } /// {@template aiState} diff --git a/packages/stream_chat/lib/src/core/models/event.g.dart b/packages/stream_chat/lib/src/core/models/event.g.dart index 7bc820b33e..c3aa215012 100644 --- a/packages/stream_chat/lib/src/core/models/event.g.dart +++ b/packages/stream_chat/lib/src/core/models/event.g.dart @@ -7,136 +7,96 @@ part of 'event.dart'; // ************************************************************************** Event _$EventFromJson(Map json) => Event( - type: json['type'] as String? ?? 'local.event', - userId: json['user_id'] as String?, - cid: json['cid'] as String?, - connectionId: json['connection_id'] as String?, - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), - me: json['me'] == null - ? null - : OwnUser.fromJson(json['me'] as Map), - user: json['user'] == null - ? null - : User.fromJson(json['user'] as Map), - message: json['message'] == null - ? null - : Message.fromJson(json['message'] as Map), - poll: json['poll'] == null - ? null - : Poll.fromJson(json['poll'] as Map), - pollVote: json['poll_vote'] == null - ? null - : PollVote.fromJson(json['poll_vote'] as Map), - totalUnreadCount: (json['total_unread_count'] as num?)?.toInt(), - unreadChannels: (json['unread_channels'] as num?)?.toInt(), - reaction: json['reaction'] == null - ? null - : Reaction.fromJson(json['reaction'] as Map), - online: json['online'] as bool?, - channel: json['channel'] == null - ? null - : ChannelModel.fromJson(json['channel'] as Map), - member: json['member'] == null - ? null - : Member.fromJson(json['member'] as Map), - channelId: json['channel_id'] as String?, - channelType: json['channel_type'] as String?, - channelLastMessageAt: json['channel_last_message_at'] == null - ? null - : DateTime.parse(json['channel_last_message_at'] as String), - parentId: json['parent_id'] as String?, - hardDelete: json['hard_delete'] as bool?, - aiState: $enumDecodeNullable(_$AITypingStateEnumMap, json['ai_state'], - unknownValue: AITypingState.idle), - aiMessage: json['ai_message'] as String?, - messageId: json['message_id'] as String?, - thread: json['thread'] == null - ? null - : Thread.fromJson(json['thread'] as Map), - unreadThreadMessages: (json['unread_thread_messages'] as num?)?.toInt(), - unreadThreads: (json['unread_threads'] as num?)?.toInt(), - lastReadAt: json['last_read_at'] == null - ? null - : DateTime.parse(json['last_read_at'] as String), - unreadMessages: (json['unread_messages'] as num?)?.toInt(), - lastReadMessageId: json['last_read_message_id'] as String?, - draft: json['draft'] == null - ? null - : Draft.fromJson(json['draft'] as Map), - reminder: json['reminder'] == null - ? null - : MessageReminder.fromJson(json['reminder'] as Map), - pushPreference: json['push_preference'] == null - ? null - : PushPreference.fromJson( - json['push_preference'] as Map), - channelPushPreference: json['channel_push_preference'] == null - ? null - : ChannelPushPreference.fromJson( - json['channel_push_preference'] as Map), - channelMessageCount: (json['channel_message_count'] as num?)?.toInt(), - lastDeliveredAt: json['last_delivered_at'] == null - ? null - : DateTime.parse(json['last_delivered_at'] as String), - lastDeliveredMessageId: json['last_delivered_message_id'] as String?, - extraData: json['extra_data'] as Map? ?? const {}, - isLocal: json['is_local'] as bool? ?? false, - ); + type: json['type'] as String? ?? 'local.event', + userId: json['user_id'] as String?, + cid: json['cid'] as String?, + connectionId: json['connection_id'] as String?, + createdAt: json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), + me: json['me'] == null ? null : OwnUser.fromJson(json['me'] as Map), + user: json['user'] == null ? null : User.fromJson(json['user'] as Map), + message: json['message'] == null ? null : Message.fromJson(json['message'] as Map), + poll: json['poll'] == null ? null : Poll.fromJson(json['poll'] as Map), + pollVote: json['poll_vote'] == null ? null : PollVote.fromJson(json['poll_vote'] as Map), + totalUnreadCount: (json['total_unread_count'] as num?)?.toInt(), + unreadChannels: (json['unread_channels'] as num?)?.toInt(), + reaction: json['reaction'] == null ? null : Reaction.fromJson(json['reaction'] as Map), + online: json['online'] as bool?, + channel: json['channel'] == null ? null : ChannelModel.fromJson(json['channel'] as Map), + member: json['member'] == null ? null : Member.fromJson(json['member'] as Map), + channelId: json['channel_id'] as String?, + channelType: json['channel_type'] as String?, + channelLastMessageAt: json['channel_last_message_at'] == null + ? null + : DateTime.parse(json['channel_last_message_at'] as String), + parentId: json['parent_id'] as String?, + hardDelete: json['hard_delete'] as bool?, + deletedForMe: json['deleted_for_me'] as bool?, + aiState: $enumDecodeNullable(_$AITypingStateEnumMap, json['ai_state'], unknownValue: AITypingState.idle), + aiMessage: json['ai_message'] as String?, + messageId: json['message_id'] as String?, + thread: json['thread'] == null ? null : Thread.fromJson(json['thread'] as Map), + unreadThreadMessages: (json['unread_thread_messages'] as num?)?.toInt(), + unreadThreads: (json['unread_threads'] as num?)?.toInt(), + lastReadAt: json['last_read_at'] == null ? null : DateTime.parse(json['last_read_at'] as String), + unreadMessages: (json['unread_messages'] as num?)?.toInt(), + lastReadMessageId: json['last_read_message_id'] as String?, + draft: json['draft'] == null ? null : Draft.fromJson(json['draft'] as Map), + reminder: json['reminder'] == null ? null : MessageReminder.fromJson(json['reminder'] as Map), + pushPreference: json['push_preference'] == null + ? null + : PushPreference.fromJson(json['push_preference'] as Map), + channelPushPreference: json['channel_push_preference'] == null + ? null + : ChannelPushPreference.fromJson(json['channel_push_preference'] as Map), + channelMessageCount: (json['channel_message_count'] as num?)?.toInt(), + lastDeliveredAt: json['last_delivered_at'] == null ? null : DateTime.parse(json['last_delivered_at'] as String), + lastDeliveredMessageId: json['last_delivered_message_id'] as String?, + extraData: json['extra_data'] as Map? ?? const {}, + isLocal: json['is_local'] as bool? ?? false, +); Map _$EventToJson(Event instance) => { - 'type': instance.type, - if (instance.userId case final value?) 'user_id': value, - if (instance.cid case final value?) 'cid': value, - if (instance.channelId case final value?) 'channel_id': value, - if (instance.channelType case final value?) 'channel_type': value, - if (instance.channelLastMessageAt?.toIso8601String() case final value?) - 'channel_last_message_at': value, - if (instance.connectionId case final value?) 'connection_id': value, - 'created_at': instance.createdAt.toIso8601String(), - if (instance.me?.toJson() case final value?) 'me': value, - if (instance.user?.toJson() case final value?) 'user': value, - if (instance.message?.toJson() case final value?) 'message': value, - if (instance.poll?.toJson() case final value?) 'poll': value, - if (instance.pollVote?.toJson() case final value?) 'poll_vote': value, - if (instance.channel?.toJson() case final value?) 'channel': value, - if (instance.member?.toJson() case final value?) 'member': value, - if (instance.reaction?.toJson() case final value?) 'reaction': value, - if (instance.totalUnreadCount case final value?) - 'total_unread_count': value, - if (instance.unreadChannels case final value?) 'unread_channels': value, - if (instance.online case final value?) 'online': value, - if (instance.parentId case final value?) 'parent_id': value, - 'is_local': instance.isLocal, - if (instance.hardDelete case final value?) 'hard_delete': value, - if (_$AITypingStateEnumMap[instance.aiState] case final value?) - 'ai_state': value, - if (instance.aiMessage case final value?) 'ai_message': value, - if (instance.messageId case final value?) 'message_id': value, - if (instance.thread?.toJson() case final value?) 'thread': value, - if (instance.unreadThreadMessages case final value?) - 'unread_thread_messages': value, - if (instance.unreadThreads case final value?) 'unread_threads': value, - if (instance.lastReadAt?.toIso8601String() case final value?) - 'last_read_at': value, - if (instance.unreadMessages case final value?) 'unread_messages': value, - if (instance.lastReadMessageId case final value?) - 'last_read_message_id': value, - if (instance.draft?.toJson() case final value?) 'draft': value, - if (instance.reminder?.toJson() case final value?) 'reminder': value, - if (instance.pushPreference?.toJson() case final value?) - 'push_preference': value, - if (instance.channelPushPreference?.toJson() case final value?) - 'channel_push_preference': value, - if (instance.channelMessageCount case final value?) - 'channel_message_count': value, - if (instance.lastDeliveredAt?.toIso8601String() case final value?) - 'last_delivered_at': value, - if (instance.lastDeliveredMessageId case final value?) - 'last_delivered_message_id': value, - 'extra_data': instance.extraData, - }; + 'type': instance.type, + if (instance.userId case final value?) 'user_id': value, + if (instance.cid case final value?) 'cid': value, + if (instance.channelId case final value?) 'channel_id': value, + if (instance.channelType case final value?) 'channel_type': value, + if (instance.channelLastMessageAt?.toIso8601String() case final value?) 'channel_last_message_at': value, + if (instance.connectionId case final value?) 'connection_id': value, + 'created_at': instance.createdAt.toIso8601String(), + if (instance.me?.toJson() case final value?) 'me': value, + if (instance.user?.toJson() case final value?) 'user': value, + if (instance.message?.toJson() case final value?) 'message': value, + if (instance.poll?.toJson() case final value?) 'poll': value, + if (instance.pollVote?.toJson() case final value?) 'poll_vote': value, + if (instance.channel?.toJson() case final value?) 'channel': value, + if (instance.member?.toJson() case final value?) 'member': value, + if (instance.reaction?.toJson() case final value?) 'reaction': value, + if (instance.totalUnreadCount case final value?) 'total_unread_count': value, + if (instance.unreadChannels case final value?) 'unread_channels': value, + if (instance.online case final value?) 'online': value, + if (instance.parentId case final value?) 'parent_id': value, + 'is_local': instance.isLocal, + if (instance.hardDelete case final value?) 'hard_delete': value, + if (instance.deletedForMe case final value?) 'deleted_for_me': value, + if (_$AITypingStateEnumMap[instance.aiState] case final value?) 'ai_state': value, + if (instance.aiMessage case final value?) 'ai_message': value, + if (instance.messageId case final value?) 'message_id': value, + if (instance.thread?.toJson() case final value?) 'thread': value, + if (instance.unreadThreadMessages case final value?) 'unread_thread_messages': value, + if (instance.unreadThreads case final value?) 'unread_threads': value, + if (instance.lastReadAt?.toIso8601String() case final value?) 'last_read_at': value, + if (instance.unreadMessages case final value?) 'unread_messages': value, + if (instance.lastReadMessageId case final value?) 'last_read_message_id': value, + if (instance.draft?.toJson() case final value?) 'draft': value, + if (instance.reminder?.toJson() case final value?) 'reminder': value, + if (instance.pushPreference?.toJson() case final value?) 'push_preference': value, + if (instance.channelPushPreference?.toJson() case final value?) 'channel_push_preference': value, + if (instance.channelMessageCount case final value?) 'channel_message_count': value, + if (instance.lastDeliveredAt?.toIso8601String() case final value?) 'last_delivered_at': value, + if (instance.lastDeliveredMessageId case final value?) 'last_delivered_message_id': value, + 'extra_data': instance.extraData, +}; const _$AITypingStateEnumMap = { AITypingState.idle: 'AI_STATE_IDLE', diff --git a/packages/stream_chat/lib/src/core/models/filter.dart b/packages/stream_chat/lib/src/core/models/filter.dart index 2122519d0f..fc8dbadd0c 100644 --- a/packages/stream_chat/lib/src/core/models/filter.dart +++ b/packages/stream_chat/lib/src/core/models/filter.dart @@ -53,7 +53,8 @@ enum FilterOperator { nor, /// Matches any list that contains the specified value - contains; + contains + ; @override String toString() { @@ -117,29 +118,22 @@ class Filter extends Equatable { }) : operator = '$operator'; /// An empty filter - const Filter.empty() - : value = const {}, - operator = null, - key = null; + const Filter.empty() : value = const {}, operator = null, key = null; /// Combines the provided filters and matches the values /// matched by all filters. - factory Filter.and(List filters) => - Filter._(operator: FilterOperator.and, value: filters); + factory Filter.and(List filters) => Filter._(operator: FilterOperator.and, value: filters); /// Combines the provided filters and matches the values /// matched by at least one of the filters. - factory Filter.or(List filters) => - Filter._(operator: FilterOperator.or, value: filters); + factory Filter.or(List filters) => Filter._(operator: FilterOperator.or, value: filters); /// Combines the provided filters and matches the values /// not matched by all the filters. - factory Filter.nor(List filters) => - Filter._(operator: FilterOperator.nor, value: filters); + factory Filter.nor(List filters) => Filter._(operator: FilterOperator.nor, value: filters); /// Matches values that are equal to a specified value. - factory Filter.equal(String key, Object value) => - Filter._(operator: FilterOperator.equal, key: key, value: value); + factory Filter.equal(String key, Object value) => Filter._(operator: FilterOperator.equal, key: key, value: value); /// Matches all values that are not equal to a specified value. factory Filter.notEqual(String key, Object value) => @@ -154,8 +148,7 @@ class Filter extends Equatable { Filter._(operator: FilterOperator.greaterOrEqual, key: key, value: value); /// Matches values that are less than a specified value. - factory Filter.less(String key, Object value) => - Filter._(operator: FilterOperator.less, key: key, value: value); + factory Filter.less(String key, Object value) => Filter._(operator: FilterOperator.less, key: key, value: value); /// Matches values that are less than or equal to a specified value. factory Filter.lessOrEqual(String key, Object value) => @@ -170,8 +163,7 @@ class Filter extends Equatable { Filter._(operator: FilterOperator.notIn, key: key, value: values); /// Matches values by performing text search with the specified value. - factory Filter.query(String key, String text) => - Filter._(operator: FilterOperator.query, key: key, value: text); + factory Filter.query(String key, String text) => Filter._(operator: FilterOperator.query, key: key, value: text); /// Matches values with the specified prefix. factory Filter.autoComplete(String key, String text) => 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..9b66746f24 --- /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, + DateTime? endAt, + DateTime? createdAt, + DateTime? updatedAt, + }) : endAt = endAt?.toUtc(), + 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..662fc500d5 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/location.g.dart @@ -0,0 +1,28 @@ +// 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/member.dart b/packages/stream_chat/lib/src/core/models/member.dart index 66b51a287a..06c7185f1d 100644 --- a/packages/stream_chat/lib/src/core/models/member.dart +++ b/packages/stream_chat/lib/src/core/models/member.dart @@ -26,15 +26,16 @@ class Member extends Equatable implements ComparableFieldProvider { this.shadowBanned = false, this.pinnedAt, this.archivedAt, + this.deletedMessages = const [], this.extraData = const {}, - }) : userId = userId ?? user?.id, - createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(); + }) : userId = userId ?? user?.id, + createdAt = createdAt ?? DateTime.now(), + updatedAt = updatedAt ?? DateTime.now(); /// Create a new instance from a json factory Member.fromJson(Map json) => _$MemberFromJson( - Serializer.moveToExtraDataFromRoot(json, _topLevelFields), - ); + Serializer.moveToExtraDataFromRoot(json, _topLevelFields), + ); /// Known top level fields. /// @@ -53,7 +54,8 @@ class Member extends Equatable implements ComparableFieldProvider { 'created_at', 'updated_at', 'pinned_at', - 'archived_at' + 'archived_at', + 'deleted_messages', ]; /// The interested user @@ -98,6 +100,12 @@ class Member extends Equatable implements ComparableFieldProvider { /// The last date of update final DateTime updatedAt; + /// List of message ids deleted by this member only for himself. + /// + /// These messages are not visible to this member anymore, but are still + /// visible to other channel members. + final List deletedMessages; + /// Map of custom member extraData. final Map extraData; @@ -118,49 +126,51 @@ class Member extends Equatable implements ComparableFieldProvider { bool? banned, DateTime? banExpires, bool? shadowBanned, + List? deletedMessages, Map? extraData, - }) => - Member( - user: user ?? this.user, - inviteAcceptedAt: inviteAcceptedAt ?? this.inviteAcceptedAt, - inviteRejectedAt: inviteRejectedAt ?? this.inviteRejectedAt, - invited: invited ?? this.invited, - banned: banned ?? this.banned, - banExpires: banExpires ?? this.banExpires, - shadowBanned: shadowBanned ?? this.shadowBanned, - channelRole: channelRole ?? this.channelRole, - userId: userId ?? this.userId, - isModerator: isModerator ?? this.isModerator, - pinnedAt: pinnedAt ?? this.pinnedAt, - archivedAt: archivedAt ?? this.archivedAt, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - extraData: extraData ?? this.extraData, - ); + }) => Member( + user: user ?? this.user, + inviteAcceptedAt: inviteAcceptedAt ?? this.inviteAcceptedAt, + inviteRejectedAt: inviteRejectedAt ?? this.inviteRejectedAt, + invited: invited ?? this.invited, + banned: banned ?? this.banned, + banExpires: banExpires ?? this.banExpires, + shadowBanned: shadowBanned ?? this.shadowBanned, + channelRole: channelRole ?? this.channelRole, + userId: userId ?? this.userId, + isModerator: isModerator ?? this.isModerator, + pinnedAt: pinnedAt ?? this.pinnedAt, + archivedAt: archivedAt ?? this.archivedAt, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedMessages: deletedMessages ?? this.deletedMessages, + extraData: extraData ?? this.extraData, + ); /// Serialize to json Map toJson() => Serializer.moveFromExtraDataToRoot( - _$MemberToJson(this), - ); + _$MemberToJson(this), + ); @override List get props => [ - user, - inviteAcceptedAt, - inviteRejectedAt, - invited, - channelRole, - userId, - isModerator, - banned, - banExpires, - shadowBanned, - pinnedAt, - archivedAt, - createdAt, - updatedAt, - extraData, - ]; + user, + inviteAcceptedAt, + inviteRejectedAt, + invited, + channelRole, + userId, + isModerator, + banned, + banExpires, + shadowBanned, + pinnedAt, + archivedAt, + createdAt, + updatedAt, + deletedMessages, + extraData, + ]; @override ComparableField? getComparableField(String sortKey) { diff --git a/packages/stream_chat/lib/src/core/models/member.g.dart b/packages/stream_chat/lib/src/core/models/member.g.dart index 0cd677e72a..730051a2db 100644 --- a/packages/stream_chat/lib/src/core/models/member.g.dart +++ b/packages/stream_chat/lib/src/core/models/member.g.dart @@ -7,53 +7,39 @@ part of 'member.dart'; // ************************************************************************** Member _$MemberFromJson(Map json) => Member( - user: json['user'] == null - ? null - : User.fromJson(json['user'] as Map), - inviteAcceptedAt: json['invite_accepted_at'] == null - ? null - : DateTime.parse(json['invite_accepted_at'] as String), - inviteRejectedAt: json['invite_rejected_at'] == null - ? null - : DateTime.parse(json['invite_rejected_at'] as String), - invited: json['invited'] as bool? ?? false, - channelRole: json['channel_role'] as String?, - userId: json['user_id'] as String?, - isModerator: json['is_moderator'] as bool? ?? false, - 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), - banned: json['banned'] as bool? ?? false, - banExpires: json['ban_expires'] == null - ? null - : DateTime.parse(json['ban_expires'] as String), - shadowBanned: json['shadow_banned'] as bool? ?? false, - pinnedAt: json['pinned_at'] == null - ? null - : DateTime.parse(json['pinned_at'] as String), - archivedAt: json['archived_at'] == null - ? null - : DateTime.parse(json['archived_at'] as String), - extraData: json['extra_data'] as Map? ?? const {}, - ); + user: json['user'] == null ? null : User.fromJson(json['user'] as Map), + inviteAcceptedAt: json['invite_accepted_at'] == null ? null : DateTime.parse(json['invite_accepted_at'] as String), + inviteRejectedAt: json['invite_rejected_at'] == null ? null : DateTime.parse(json['invite_rejected_at'] as String), + invited: json['invited'] as bool? ?? false, + channelRole: json['channel_role'] as String?, + userId: json['user_id'] as String?, + isModerator: json['is_moderator'] as bool? ?? false, + 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), + banned: json['banned'] as bool? ?? false, + banExpires: json['ban_expires'] == null ? null : DateTime.parse(json['ban_expires'] as String), + shadowBanned: json['shadow_banned'] as bool? ?? false, + pinnedAt: json['pinned_at'] == null ? null : DateTime.parse(json['pinned_at'] as String), + archivedAt: json['archived_at'] == null ? null : DateTime.parse(json['archived_at'] as String), + deletedMessages: (json['deleted_messages'] as List?)?.map((e) => e as String).toList() ?? const [], + extraData: json['extra_data'] as Map? ?? const {}, +); Map _$MemberToJson(Member instance) => { - 'user': instance.user?.toJson(), - 'invite_accepted_at': instance.inviteAcceptedAt?.toIso8601String(), - 'invite_rejected_at': instance.inviteRejectedAt?.toIso8601String(), - 'invited': instance.invited, - 'channel_role': instance.channelRole, - 'user_id': instance.userId, - 'is_moderator': instance.isModerator, - 'banned': instance.banned, - 'ban_expires': instance.banExpires?.toIso8601String(), - 'shadow_banned': instance.shadowBanned, - 'pinned_at': instance.pinnedAt?.toIso8601String(), - 'archived_at': instance.archivedAt?.toIso8601String(), - 'created_at': instance.createdAt.toIso8601String(), - 'updated_at': instance.updatedAt.toIso8601String(), - 'extra_data': instance.extraData, - }; + 'user': instance.user?.toJson(), + 'invite_accepted_at': instance.inviteAcceptedAt?.toIso8601String(), + 'invite_rejected_at': instance.inviteRejectedAt?.toIso8601String(), + 'invited': instance.invited, + 'channel_role': instance.channelRole, + 'user_id': instance.userId, + 'is_moderator': instance.isModerator, + 'banned': instance.banned, + 'ban_expires': instance.banExpires?.toIso8601String(), + 'shadow_banned': instance.shadowBanned, + 'pinned_at': instance.pinnedAt?.toIso8601String(), + 'archived_at': instance.archivedAt?.toIso8601String(), + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_messages': instance.deletedMessages, + 'extra_data': instance.extraData, +}; diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index d36f3c7af6..d65c691974 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'; @@ -35,11 +36,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, @@ -55,6 +52,7 @@ class Message extends Equatable implements ComparableFieldProvider { this.localUpdatedAt, DateTime? deletedAt, this.localDeletedAt, + this.deletedForMe, this.messageTextUpdatedAt, this.user, this.pinned = false, @@ -71,19 +69,15 @@ class Message extends Equatable implements ComparableFieldProvider { this.draft, this.reminder, this.channelRole, - }) : 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; + this.sharedLocation, + }) : id = id ?? const Uuid().v4(), + type = MessageType(type), + pinExpires = pinExpires?.toUtc(), + remoteCreatedAt = createdAt, + remoteUpdatedAt = updatedAt, + remoteDeletedAt = deletedAt, + _quotedMessageId = quotedMessageId, + _pollId = pollId; /// Create a new instance from JSON. factory Message.fromJson(Map json) { @@ -91,14 +85,22 @@ class Message extends Equatable implements ComparableFieldProvider { Serializer.moveToExtraDataFromRoot(json, topLevelFields), ); + // TODO: Remove this once type are properly enriched on the backend. + var type = message.type; + if (message.deletedForMe ?? false) { + type = MessageType.deleted; + } + var state = MessageState.sent; - if (message.deletedAt != null) { + if (message.deletedForMe ?? false) { + state = MessageState.deletedForMe; + } else if (message.deletedAt != null) { state = MessageState.softDeleted; } else if (message.updatedAt.isAfter(message.createdAt)) { state = MessageState.updated; } - return message.copyWith(state: state); + return message.copyWith(type: type, state: state); } /// The message ID. This is either created by Stream or set client side when @@ -129,45 +131,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. @@ -309,11 +306,13 @@ 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; static Object? _channelRoleReadValue(Map json, String key) { @@ -329,6 +328,17 @@ class Message extends Equatable implements ComparableFieldProvider { @JsonKey(includeToJson: false, readValue: _channelRoleReadValue) final String? channelRole; + /// 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; + + /// Whether the message was deleted only for the current user. + @JsonKey(includeToJson: false) + final bool? deletedForMe; + /// Message custom extraData. final Map extraData; @@ -378,6 +388,8 @@ class Message extends Equatable implements ComparableFieldProvider { 'draft', 'reminder', 'member', + 'shared_location', + 'deleted_for_me', ]; /// Serialize to json. @@ -405,10 +417,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, @@ -441,20 +449,18 @@ class Message extends Equatable implements ComparableFieldProvider { Object? draft = _nullConst, Object? reminder = _nullConst, String? channelRole, + Location? sharedLocation, + bool? deletedForMe, }) { assert(() { - if (pinExpires is! DateTime && - pinExpires != null && - pinExpires is! _NullConst) { + if (pinExpires is! DateTime && pinExpires != null && pinExpires is! _NullConst) { throw ArgumentError('`pinExpires` can only be set as DateTime or null'); } return true; }(), 'Validate type for pinExpires'); assert(() { - if (quotedMessage is! Message && - quotedMessage != null && - quotedMessage is! _NullConst) { + if (quotedMessage is! Message && quotedMessage != null && quotedMessage is! _NullConst) { throw ArgumentError( '`quotedMessage` can only be set as Message or null', ); @@ -463,9 +469,7 @@ class Message extends Equatable implements ComparableFieldProvider { }(), 'Validate type for quotedMessage'); assert(() { - if (quotedMessageId is! String && - quotedMessageId != null && - quotedMessageId is! _NullConst) { + if (quotedMessageId is! String && quotedMessageId != null && quotedMessageId is! _NullConst) { throw ArgumentError( '`quotedMessage` can only be set as String or null', ); @@ -481,21 +485,12 @@ 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, - quotedMessage: quotedMessage == _nullConst - ? this.quotedMessage - : quotedMessage as Message?, - quotedMessageId: quotedMessageId == _nullConst - ? _quotedMessageId - : quotedMessageId as String?, + quotedMessage: quotedMessage == _nullConst ? this.quotedMessage : quotedMessage as Message?, + quotedMessageId: quotedMessageId == _nullConst ? _quotedMessageId : quotedMessageId as String?, replyCount: replyCount ?? this.replyCount, threadParticipants: threadParticipants ?? this.threadParticipants, showInChannel: showInChannel ?? this.showInChannel, @@ -510,8 +505,7 @@ class Message extends Equatable implements ComparableFieldProvider { user: user ?? this.user, pinned: pinned ?? this.pinned, pinnedAt: pinnedAt ?? this.pinnedAt, - pinExpires: - pinExpires == _nullConst ? this.pinExpires : pinExpires as DateTime?, + pinExpires: pinExpires == _nullConst ? this.pinExpires : pinExpires as DateTime?, pinnedBy: pinnedBy ?? this.pinnedBy, poll: poll ?? this.poll, pollId: pollId ?? _pollId, @@ -521,9 +515,10 @@ class Message extends Equatable implements ComparableFieldProvider { restrictedVisibility: restrictedVisibility ?? this.restrictedVisibility, moderation: moderation ?? this.moderation, draft: draft == _nullConst ? this.draft : draft as Draft?, - reminder: - reminder == _nullConst ? this.reminder : reminder as MessageReminder?, + reminder: reminder == _nullConst ? this.reminder : reminder as MessageReminder?, channelRole: channelRole ?? this.channelRole, + sharedLocation: sharedLocation ?? this.sharedLocation, + deletedForMe: deletedForMe ?? this.deletedForMe, ); } @@ -570,6 +565,8 @@ class Message extends Equatable implements ComparableFieldProvider { draft: other.draft, reminder: other.reminder, channelRole: other.channelRole, + sharedLocation: other.sharedLocation, + deletedForMe: other.deletedForMe, ); } @@ -588,55 +585,68 @@ class Message extends Equatable implements ComparableFieldProvider { Message syncWith(Message? other) { if (other == null) return this; - return copyWith( + var synced = copyWith( localCreatedAt: other.localCreatedAt, localUpdatedAt: other.localUpdatedAt, localDeletedAt: other.localDeletedAt, ); + + // The backend does not always enrich this deletedForMe field + if (other.deletedForMe == true && synced.deletedForMe != true) { + synced = synced.copyWith( + deletedForMe: true, + type: MessageType.deleted, + state: MessageState.deletedForMe, + ); + } + + return synced; } @override List get props => [ - id, - text, - type, - attachments, - mentionedUsers, - reactionGroups, - latestReactions, - ownReactions, - parentId, - quotedMessage, - quotedMessageId, - replyCount, - threadParticipants, - showInChannel, - shadowed, - silent, - command, - localCreatedAt, - remoteCreatedAt, - localUpdatedAt, - remoteUpdatedAt, - localDeletedAt, - remoteDeletedAt, - messageTextUpdatedAt, - user, - pinned, - pinnedAt, - pinExpires, - pinnedBy, - poll, - pollId, - extraData, - state, - i18n, - restrictedVisibility, - moderation, - draft, - reminder, - channelRole, - ]; + id, + text, + type, + attachments, + mentionedUsers, + reactionGroups, + latestReactions, + ownReactions, + parentId, + quotedMessage, + quotedMessageId, + replyCount, + threadParticipants, + showInChannel, + shadowed, + silent, + command, + localCreatedAt, + remoteCreatedAt, + localUpdatedAt, + remoteUpdatedAt, + localDeletedAt, + remoteDeletedAt, + messageTextUpdatedAt, + user, + pinned, + pinnedAt, + pinExpires, + pinnedBy, + poll, + pollId, + extraData, + state, + i18n, + restrictedVisibility, + moderation, + draft, + reminder, + channelRole, + sharedLocation, + deletedForMe, + ]; @override ComparableField? getComparableField(String sortKey) { 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 60e27988ca..fe9d4721fd 100644 --- a/packages/stream_chat/lib/src/core/models/message.g.dart +++ b/packages/stream_chat/lib/src/core/models/message.g.dart @@ -7,114 +7,81 @@ part of 'message.dart'; // ************************************************************************** Message _$MessageFromJson(Map json) => Message( - id: json['id'] as String?, - text: json['text'] as String?, - type: json['type'] == null - ? MessageType.regular - : MessageType.fromJson(json['type'] as String), - attachments: (json['attachments'] as List?) - ?.map((e) => Attachment.fromJson(e as Map)) - .toList() ?? - const [], - mentionedUsers: (json['mentioned_users'] as List?) - ?.map((e) => User.fromJson(e as Map)) - .toList() ?? - 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( - (k, e) => - MapEntry(k, ReactionGroup.fromJson(e as Map)), - ), - latestReactions: (json['latest_reactions'] as List?) - ?.map((e) => Reaction.fromJson(e as Map)) - .toList(), - ownReactions: (json['own_reactions'] as List?) - ?.map((e) => Reaction.fromJson(e as Map)) - .toList(), - parentId: json['parent_id'] as String?, - quotedMessage: json['quoted_message'] == null - ? null - : Message.fromJson(json['quoted_message'] as Map), - quotedMessageId: json['quoted_message_id'] as String?, - replyCount: (json['reply_count'] as num?)?.toInt() ?? 0, - threadParticipants: (json['thread_participants'] as List?) - ?.map((e) => User.fromJson(e as Map)) - .toList(), - showInChannel: json['show_in_channel'] as bool?, - command: json['command'] 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), - deletedAt: json['deleted_at'] == null - ? null - : DateTime.parse(json['deleted_at'] as String), - messageTextUpdatedAt: json['message_text_updated_at'] == null - ? null - : DateTime.parse(json['message_text_updated_at'] as String), - user: json['user'] == null - ? null - : User.fromJson(json['user'] as Map), - pinned: json['pinned'] as bool? ?? false, - pinnedAt: json['pinned_at'] == null - ? null - : DateTime.parse(json['pinned_at'] as String), - pinExpires: json['pin_expires'] == null - ? null - : DateTime.parse(json['pin_expires'] as String), - pinnedBy: json['pinned_by'] == null - ? null - : User.fromJson(json['pinned_by'] as Map), - poll: json['poll'] == null - ? null - : Poll.fromJson(json['poll'] as Map), - pollId: json['poll_id'] as String?, - extraData: json['extra_data'] as Map? ?? const {}, - i18n: (json['i18n'] as Map?)?.map( - (k, e) => MapEntry(k, e as String), - ), - restrictedVisibility: (json['restricted_visibility'] as List?) - ?.map((e) => e as String) - .toList(), - moderation: Message._moderationReadValue(json, 'moderation') == null - ? null - : Moderation.fromJson(Message._moderationReadValue(json, 'moderation') - as Map), - draft: json['draft'] == null - ? null - : Draft.fromJson(json['draft'] as Map), - reminder: json['reminder'] == null - ? null - : MessageReminder.fromJson(json['reminder'] as Map), - channelRole: - Message._channelRoleReadValue(json, 'channel_role') as String?, - ); + id: json['id'] as String?, + text: json['text'] as String?, + type: json['type'] == null ? MessageType.regular : MessageType.fromJson(json['type'] as String), + attachments: + (json['attachments'] as List?)?.map((e) => Attachment.fromJson(e as Map)).toList() ?? + const [], + mentionedUsers: + (json['mentioned_users'] as List?)?.map((e) => User.fromJson(e as Map)).toList() ?? + const [], + silent: json['silent'] as bool? ?? false, + shadowed: json['shadowed'] as bool? ?? false, + reactionGroups: (Message._reactionGroupsReadValue(json, 'reaction_groups') as Map?)?.map( + (k, e) => MapEntry(k, ReactionGroup.fromJson(e as Map)), + ), + latestReactions: (json['latest_reactions'] as List?) + ?.map((e) => Reaction.fromJson(e as Map)) + .toList(), + ownReactions: (json['own_reactions'] as List?) + ?.map((e) => Reaction.fromJson(e as Map)) + .toList(), + parentId: json['parent_id'] as String?, + quotedMessage: json['quoted_message'] == null + ? null + : Message.fromJson(json['quoted_message'] as Map), + quotedMessageId: json['quoted_message_id'] as String?, + replyCount: (json['reply_count'] as num?)?.toInt() ?? 0, + threadParticipants: (json['thread_participants'] as List?) + ?.map((e) => User.fromJson(e as Map)) + .toList(), + showInChannel: json['show_in_channel'] as bool?, + command: json['command'] 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), + deletedAt: json['deleted_at'] == null ? null : DateTime.parse(json['deleted_at'] as String), + deletedForMe: json['deleted_for_me'] as bool?, + messageTextUpdatedAt: json['message_text_updated_at'] == null + ? null + : DateTime.parse(json['message_text_updated_at'] as String), + user: json['user'] == null ? null : User.fromJson(json['user'] as Map), + pinned: json['pinned'] as bool? ?? false, + pinnedAt: json['pinned_at'] == null ? null : DateTime.parse(json['pinned_at'] as String), + pinExpires: json['pin_expires'] == null ? null : DateTime.parse(json['pin_expires'] as String), + pinnedBy: json['pinned_by'] == null ? null : User.fromJson(json['pinned_by'] as Map), + poll: json['poll'] == null ? null : Poll.fromJson(json['poll'] as Map), + pollId: json['poll_id'] as String?, + extraData: json['extra_data'] as Map? ?? const {}, + i18n: (json['i18n'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ), + restrictedVisibility: (json['restricted_visibility'] as List?)?.map((e) => e as String).toList(), + moderation: Message._moderationReadValue(json, 'moderation') == null + ? null + : Moderation.fromJson(Message._moderationReadValue(json, 'moderation') as Map), + draft: json['draft'] == null ? null : Draft.fromJson(json['draft'] as Map), + reminder: json['reminder'] == null ? null : MessageReminder.fromJson(json['reminder'] as Map), + channelRole: Message._channelRoleReadValue(json, 'channel_role') as String?, + sharedLocation: json['shared_location'] == null + ? null + : Location.fromJson(json['shared_location'] as Map), +); Map _$MessageToJson(Message instance) => { - 'id': instance.id, - 'text': instance.text, - if (MessageType.toJson(instance.type) case final value?) 'type': value, - 'attachments': instance.attachments.map((e) => e.toJson()).toList(), - 'mentioned_users': User.toIds(instance.mentionedUsers), - 'parent_id': instance.parentId, - 'quoted_message_id': instance.quotedMessageId, - 'show_in_channel': instance.showInChannel, - 'silent': instance.silent, - 'pinned': instance.pinned, - 'pin_expires': instance.pinExpires?.toIso8601String(), - 'poll_id': instance.pollId, - if (instance.restrictedVisibility case final value?) - 'restricted_visibility': value, - 'draft': instance.draft?.toJson(), - 'reminder': instance.reminder?.toJson(), - 'extra_data': instance.extraData, - }; + 'id': instance.id, + 'text': instance.text, + if (MessageType.toJson(instance.type) case final value?) 'type': value, + 'attachments': instance.attachments.map((e) => e.toJson()).toList(), + 'mentioned_users': User.toIds(instance.mentionedUsers), + 'parent_id': instance.parentId, + 'quoted_message_id': instance.quotedMessageId, + 'show_in_channel': instance.showInChannel, + 'silent': instance.silent, + 'pinned': instance.pinned, + 'pin_expires': instance.pinExpires?.toIso8601String(), + 'poll_id': instance.pollId, + if (instance.restrictedVisibility case final value?) 'restricted_visibility': value, + 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_delete_scope.dart b/packages/stream_chat/lib/src/core/models/message_delete_scope.dart new file mode 100644 index 0000000000..9a53a777bf --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/message_delete_scope.dart @@ -0,0 +1,60 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'message_delete_scope.freezed.dart'; +part 'message_delete_scope.g.dart'; + +/// Represents the scope of deletion for a message. +/// +/// - [deleteForMe]: The message is deleted only for the current user. +/// - [deleteForAll]: The message is deleted for all users. The [hard] +/// parameter indicates whether the deletion is permanent (hard) or soft. +@freezed +sealed class MessageDeleteScope with _$MessageDeleteScope { + /// The message is deleted only for the current user. + /// + /// Note: This does not permanently delete the message, it will remain + /// visible to other channel members. + const factory MessageDeleteScope.deleteForMe() = DeleteForMe; + + /// The message is deleted for all users. + /// + /// If [hard] is true, the message is permanently deleted and cannot be + /// recovered. If false, the message is soft deleted and may be recoverable + /// by channel members with the appropriate permissions. + /// + /// Defaults to soft deletion (hard = false). + const factory MessageDeleteScope.deleteForAll({ + @Default(false) bool hard, + }) = DeleteForAll; + + /// Creates a instance of [MessageDeleteScope] from a JSON map. + factory MessageDeleteScope.fromJson(Map json) => _$MessageDeleteScopeFromJson(json); + + // region Predefined Scopes + + /// The message is soft deleted for all users. + /// + /// This is equivalent to `MessageDeleteScope.deleteForAll(hard: false)`. + static const softDeleteForAll = MessageDeleteScope.deleteForAll(); + + /// The message is permanently (hard) deleted for all users. + /// + /// This is equivalent to `MessageDeleteScope.deleteForAll(hard: true)`. + static const hardDeleteForAll = MessageDeleteScope.deleteForAll(hard: true); + + // endregion +} + +/// Extension methods for [MessageDeleteScope] to provide additional +/// functionality. +extension MessageDeleteScopeX on MessageDeleteScope { + /// Indicates whether the deletion is permanent (hard) or soft. + /// + /// For [DeleteForMe], this is always false. + bool get hard { + return switch (this) { + DeleteForMe() => false, + DeleteForAll(hard: final hard) => hard, + }; + } +} diff --git a/packages/stream_chat/lib/src/core/models/message_delete_scope.freezed.dart b/packages/stream_chat/lib/src/core/models/message_delete_scope.freezed.dart new file mode 100644 index 0000000000..84da7fb10b --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/message_delete_scope.freezed.dart @@ -0,0 +1,166 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'message_delete_scope.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +MessageDeleteScope _$MessageDeleteScopeFromJson(Map json) { + switch (json['runtimeType']) { + case 'deleteForMe': + return DeleteForMe.fromJson(json); + case 'deleteForAll': + return DeleteForAll.fromJson(json); + + default: + throw CheckedFromJsonException(json, 'runtimeType', 'MessageDeleteScope', + 'Invalid union type "${json['runtimeType']}"!'); + } +} + +/// @nodoc +mixin _$MessageDeleteScope { + /// Serializes this MessageDeleteScope to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is MessageDeleteScope); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'MessageDeleteScope()'; + } +} + +/// @nodoc +class $MessageDeleteScopeCopyWith<$Res> { + $MessageDeleteScopeCopyWith( + MessageDeleteScope _, $Res Function(MessageDeleteScope) __); +} + +/// @nodoc +@JsonSerializable() +class DeleteForMe implements MessageDeleteScope { + const DeleteForMe({final String? $type}) : $type = $type ?? 'deleteForMe'; + factory DeleteForMe.fromJson(Map json) => + _$DeleteForMeFromJson(json); + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + Map toJson() { + return _$DeleteForMeToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is DeleteForMe); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'MessageDeleteScope.deleteForMe()'; + } +} + +/// @nodoc +@JsonSerializable() +class DeleteForAll implements MessageDeleteScope { + const DeleteForAll({this.hard = false, final String? $type}) + : $type = $type ?? 'deleteForAll'; + factory DeleteForAll.fromJson(Map json) => + _$DeleteForAllFromJson(json); + + @JsonKey() + final bool hard; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of MessageDeleteScope + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $DeleteForAllCopyWith get copyWith => + _$DeleteForAllCopyWithImpl(this, _$identity); + + @override + Map toJson() { + return _$DeleteForAllToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is DeleteForAll && + (identical(other.hard, hard) || other.hard == hard)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, hard); + + @override + String toString() { + return 'MessageDeleteScope.deleteForAll(hard: $hard)'; + } +} + +/// @nodoc +abstract mixin class $DeleteForAllCopyWith<$Res> + implements $MessageDeleteScopeCopyWith<$Res> { + factory $DeleteForAllCopyWith( + DeleteForAll value, $Res Function(DeleteForAll) _then) = + _$DeleteForAllCopyWithImpl; + @useResult + $Res call({bool hard}); +} + +/// @nodoc +class _$DeleteForAllCopyWithImpl<$Res> implements $DeleteForAllCopyWith<$Res> { + _$DeleteForAllCopyWithImpl(this._self, this._then); + + final DeleteForAll _self; + final $Res Function(DeleteForAll) _then; + + /// Create a copy of MessageDeleteScope + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? hard = null, + }) { + return _then(DeleteForAll( + hard: null == hard + ? _self.hard + : hard // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +// dart format on diff --git a/packages/stream_chat/lib/src/core/models/message_delete_scope.g.dart b/packages/stream_chat/lib/src/core/models/message_delete_scope.g.dart new file mode 100644 index 0000000000..75f4373b42 --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/message_delete_scope.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'message_delete_scope.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DeleteForMe _$DeleteForMeFromJson(Map json) => DeleteForMe( + $type: json['runtimeType'] as String?, +); + +Map _$DeleteForMeToJson(DeleteForMe instance) => { + 'runtimeType': instance.$type, +}; + +DeleteForAll _$DeleteForAllFromJson(Map json) => DeleteForAll( + hard: json['hard'] as bool? ?? false, + $type: json['runtimeType'] as String?, +); + +Map _$DeleteForAllToJson(DeleteForAll instance) => { + 'hard': instance.hard, + 'runtimeType': instance.$type, +}; diff --git a/packages/stream_chat/lib/src/core/models/message_delivery.g.dart b/packages/stream_chat/lib/src/core/models/message_delivery.g.dart index 7a4f1431b9..96a3c10157 100644 --- a/packages/stream_chat/lib/src/core/models/message_delivery.g.dart +++ b/packages/stream_chat/lib/src/core/models/message_delivery.g.dart @@ -6,8 +6,7 @@ part of 'message_delivery.dart'; // JsonSerializableGenerator // ************************************************************************** -Map _$MessageDeliveryToJson(MessageDelivery instance) => - { - 'cid': instance.channelCid, - 'id': instance.messageId, - }; +Map _$MessageDeliveryToJson(MessageDelivery instance) => { + 'cid': instance.channelCid, + 'id': instance.messageId, +}; diff --git a/packages/stream_chat/lib/src/core/models/message_reminder.dart b/packages/stream_chat/lib/src/core/models/message_reminder.dart index d816e06148..23128f53aa 100644 --- a/packages/stream_chat/lib/src/core/models/message_reminder.dart +++ b/packages/stream_chat/lib/src/core/models/message_reminder.dart @@ -38,12 +38,11 @@ class MessageReminder extends Equatable implements ComparableFieldProvider { this.remindAt, DateTime? createdAt, DateTime? updatedAt, - }) : createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(); + }) : createdAt = createdAt ?? DateTime.now(), + updatedAt = updatedAt ?? DateTime.now(); /// Create a new instance from a json - factory MessageReminder.fromJson(Map json) => - _$MessageReminderFromJson(json); + factory MessageReminder.fromJson(Map json) => _$MessageReminderFromJson(json); /// The channel CID where the message exists. final String channelCid; @@ -124,16 +123,16 @@ class MessageReminder extends Equatable implements ComparableFieldProvider { @override List get props => [ - channelCid, - channel, - messageId, - message, - userId, - user, - remindAt, - createdAt, - updatedAt, - ]; + channelCid, + channel, + messageId, + message, + userId, + user, + remindAt, + createdAt, + updatedAt, + ]; @override ComparableField? getComparableField(String sortKey) { diff --git a/packages/stream_chat/lib/src/core/models/message_reminder.g.dart b/packages/stream_chat/lib/src/core/models/message_reminder.g.dart index 1562ace757..2df37be652 100644 --- a/packages/stream_chat/lib/src/core/models/message_reminder.g.dart +++ b/packages/stream_chat/lib/src/core/models/message_reminder.g.dart @@ -6,37 +6,23 @@ part of 'message_reminder.dart'; // JsonSerializableGenerator // ************************************************************************** -MessageReminder _$MessageReminderFromJson(Map json) => - MessageReminder( - 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, - user: json['user'] == null - ? null - : User.fromJson(json['user'] as Map), - remindAt: json['remind_at'] == null - ? null - : DateTime.parse(json['remind_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), - ); +MessageReminder _$MessageReminderFromJson(Map json) => MessageReminder( + 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, + user: json['user'] == null ? null : User.fromJson(json['user'] as Map), + remindAt: json['remind_at'] == null ? null : DateTime.parse(json['remind_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 _$MessageReminderToJson(MessageReminder instance) => - { - 'channel_cid': instance.channelCid, - 'message_id': instance.messageId, - 'user_id': instance.userId, - 'remind_at': instance.remindAt?.toIso8601String(), - 'created_at': instance.createdAt.toIso8601String(), - 'updated_at': instance.updatedAt.toIso8601String(), - }; +Map _$MessageReminderToJson(MessageReminder instance) => { + 'channel_cid': instance.channelCid, + 'message_id': instance.messageId, + 'user_id': instance.userId, + 'remind_at': instance.remindAt?.toIso8601String(), + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), +}; 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..bcd7f20550 100644 --- a/packages/stream_chat/lib/src/core/models/message_state.dart +++ b/packages/stream_chat/lib/src/core/models/message_state.dart @@ -1,9 +1,9 @@ // ignore_for_file: avoid_positional_boolean_parameters import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:stream_chat/src/core/models/message_delete_scope.dart'; part 'message_state.freezed.dart'; - part 'message_state.g.dart'; /// Helper extension for [MessageState]. @@ -33,7 +33,7 @@ extension MessageStateX on MessageState { } /// Returns true if the message is in outgoing deleting state. - bool get isDeleting => isSoftDeleting || isHardDeleting; + bool get isDeleting => isSoftDeleting || isHardDeleting || isDeletingForMe; /// Returns true if the message is in outgoing soft deleting state. bool get isSoftDeleting { @@ -43,7 +43,10 @@ extension MessageStateX on MessageState { final outgoingState = messageState.state; if (outgoingState is! Deleting) return false; - return !outgoingState.hard; + final deletingScope = outgoingState.scope; + if (deletingScope is! DeleteForAll) return false; + + return !deletingScope.hard; } /// Returns true if the message is in outgoing hard deleting state. @@ -54,7 +57,22 @@ extension MessageStateX on MessageState { final outgoingState = messageState.state; if (outgoingState is! Deleting) return false; - return outgoingState.hard; + final deletingScope = outgoingState.scope; + if (deletingScope is! DeleteForAll) return false; + + return deletingScope.hard; + } + + /// Returns true if the message is in outgoing deleting for me state. + bool get isDeletingForMe { + final messageState = this; + if (messageState is! MessageOutgoing) return false; + + final outgoingState = messageState.state; + if (outgoingState is! Deleting) return false; + + final deletingScope = outgoingState.scope; + return deletingScope is DeleteForMe; } /// Returns true if the message is in completed sent state. @@ -70,7 +88,7 @@ extension MessageStateX on MessageState { } /// Returns true if the message is in completed deleted state. - bool get isDeleted => isSoftDeleted || isHardDeleted; + bool get isDeleted => isSoftDeleted || isHardDeleted || isDeletedForMe; /// Returns true if the message is in completed soft deleted state. bool get isSoftDeleted { @@ -80,7 +98,10 @@ extension MessageStateX on MessageState { final completedState = messageState.state; if (completedState is! Deleted) return false; - return !completedState.hard; + final deletingScope = completedState.scope; + if (deletingScope is! DeleteForAll) return false; + + return !deletingScope.hard; } /// Returns true if the message is in completed hard deleted state. @@ -91,7 +112,22 @@ extension MessageStateX on MessageState { final completedState = messageState.state; if (completedState is! Deleted) return false; - return completedState.hard; + final deletingScope = completedState.scope; + if (deletingScope is! DeleteForAll) return false; + + return deletingScope.hard; + } + + /// Returns true if the message is in completed deleted for me state. + bool get isDeletedForMe { + final messageState = this; + if (messageState is! MessageCompleted) return false; + + final completedState = messageState.state; + if (completedState is! Deleted) return false; + + final deletingScope = completedState.scope; + return deletingScope is DeleteForMe; } /// Returns true if the message is in failed sending state. @@ -103,13 +139,15 @@ 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. - bool get isDeletingFailed => isSoftDeletingFailed || isHardDeletingFailed; + bool get isDeletingFailed => isSoftDeletingFailed || isHardDeletingFailed || isDeletingForMeFailed; /// Returns true if the message is in failed soft deleting state. bool get isSoftDeletingFailed { @@ -119,7 +157,10 @@ extension MessageStateX on MessageState { final failedState = messageState.state; if (failedState is! DeletingFailed) return false; - return !failedState.hard; + final deletingScope = failedState.scope; + if (deletingScope is! DeleteForAll) return false; + + return !deletingScope.hard; } /// Returns true if the message is in failed hard deleting state. @@ -130,7 +171,22 @@ extension MessageStateX on MessageState { final failedState = messageState.state; if (failedState is! DeletingFailed) return false; - return failedState.hard; + final deletingScope = failedState.scope; + if (deletingScope is! DeleteForAll) return false; + + return deletingScope.hard; + } + + /// Returns true if the message is in failed deleting for me state. + bool get isDeletingForMeFailed { + final messageState = this; + if (messageState is! MessageFailed) return false; + + final failedState = messageState.state; + if (failedState is! DeletingFailed) return false; + + final deletingScope = failedState.scope; + return deletingScope is DeleteForMe; } } @@ -158,30 +214,81 @@ sealed class MessageState with _$MessageState { }) = MessageFailed; /// Creates a new instance from a json - factory MessageState.fromJson(Map json) => - _$MessageStateFromJson(json); + factory MessageState.fromJson(Map json) => _$MessageStateFromJson(json); + + // region Factory Constructors for Common States /// Deleting state when the message is being deleted. - factory MessageState.deleting({required bool hard}) { + factory MessageState.deleting({ + required MessageDeleteScope scope, + }) { return MessageState.outgoing( - state: OutgoingState.deleting(hard: hard), + state: OutgoingState.deleting(scope: scope), ); } /// Deleting state when the message has been successfully deleted. - factory MessageState.deleted({required bool hard}) { + factory MessageState.deleted({ + required MessageDeleteScope scope, + }) { return MessageState.completed( - state: CompletedState.deleted(hard: hard), + state: CompletedState.deleted(scope: scope), ); } /// Deleting failed state when the message fails to be deleted. - factory MessageState.deletingFailed({required bool hard}) { + factory MessageState.deletingFailed({ + required MessageDeleteScope scope, + }) { return MessageState.failed( - state: FailedState.deletingFailed(hard: hard), + state: FailedState.deletingFailed(scope: scope), ); } + /// 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, + ), + ); + } + + // endregion + + // region Common Static Instances + /// Sending state when the message is being sent. static const sending = MessageState.outgoing( state: OutgoingState.sending(), @@ -199,7 +306,16 @@ sealed class MessageState with _$MessageState { /// Hard deleting state when the message is being hard deleted. static const hardDeleting = MessageState.outgoing( - state: OutgoingState.deleting(hard: true), + state: OutgoingState.deleting( + scope: MessageDeleteScope.hardDeleteForAll, + ), + ); + + /// Deleting for me state when the message is being deleted only for me. + static const deletingForMe = MessageState.outgoing( + state: OutgoingState.deleting( + scope: MessageDeleteScope.deleteForMe(), + ), ); /// Sent state when the message has been successfully sent. @@ -219,17 +335,17 @@ sealed class MessageState with _$MessageState { /// Hard deleted state when the message has been successfully hard deleted. static const hardDeleted = MessageState.completed( - state: CompletedState.deleted(hard: true), + state: CompletedState.deleted( + scope: MessageDeleteScope.hardDeleteForAll, + ), ); - /// 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(), + /// Deleted for me state when the message has been successfully deleted only + /// for me. + static const deletedForMe = MessageState.completed( + state: CompletedState.deleted( + scope: MessageDeleteScope.deleteForMe(), + ), ); /// Deleting failed state when the message fails to be soft deleted. @@ -239,8 +355,19 @@ sealed class MessageState with _$MessageState { /// Hard deleting failed state when the message fails to be hard deleted. static const hardDeletingFailed = MessageState.failed( - state: FailedState.deletingFailed(hard: true), + state: FailedState.deletingFailed( + scope: MessageDeleteScope.hardDeleteForAll, + ), ); + + /// Deleting for me failed state when the message fails to be deleted only + static const deletingForMeFailed = MessageState.failed( + state: FailedState.deletingFailed( + scope: MessageDeleteScope.deleteForMe(), + ), + ); + + // endregion } /// Represents the state of an outgoing message. @@ -254,12 +381,11 @@ sealed class OutgoingState with _$OutgoingState { /// Deleting state when the message is being deleted. const factory OutgoingState.deleting({ - @Default(false) bool hard, + @Default(MessageDeleteScope.softDeleteForAll) MessageDeleteScope scope, }) = Deleting; /// Creates a new instance from a json - factory OutgoingState.fromJson(Map json) => - _$OutgoingStateFromJson(json); + factory OutgoingState.fromJson(Map json) => _$OutgoingStateFromJson(json); } /// Represents the completed state of a message. @@ -273,31 +399,41 @@ sealed class CompletedState with _$CompletedState { /// Deleted state when the message has been successfully deleted. const factory CompletedState.deleted({ - @Default(false) bool hard, + @Default(MessageDeleteScope.softDeleteForAll) MessageDeleteScope scope, }) = Deleted; /// Creates a new instance from a json - factory CompletedState.fromJson(Map json) => - _$CompletedStateFromJson(json); + factory CompletedState.fromJson(Map json) => _$CompletedStateFromJson(json); } /// Represents the failed state of a message. @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({ - @Default(false) bool hard, + @Default(MessageDeleteScope.softDeleteForAll) MessageDeleteScope scope, }) = DeletingFailed; /// Creates a new instance from a json - factory FailedState.fromJson(Map json) => - _$FailedStateFromJson(json); + factory FailedState.fromJson(Map json) => _$FailedStateFromJson(json); } // coverage:ignore-start @@ -420,13 +556,13 @@ extension OutgoingStatePatternMatching on OutgoingState { TResult when({ required TResult Function() sending, required TResult Function() updating, - required TResult Function(bool hard) deleting, + required TResult Function(MessageDeleteScope scope) deleting, }) { final outgoingState = this; return switch (outgoingState) { Sending() => sending(), Updating() => updating(), - Deleting() => deleting(outgoingState.hard), + Deleting() => deleting(outgoingState.scope), }; } @@ -435,13 +571,13 @@ extension OutgoingStatePatternMatching on OutgoingState { TResult? whenOrNull({ TResult? Function()? sending, TResult? Function()? updating, - TResult? Function(bool hard)? deleting, + TResult? Function(MessageDeleteScope scope)? deleting, }) { final outgoingState = this; return switch (outgoingState) { Sending() => sending?.call(), Updating() => updating?.call(), - Deleting() => deleting?.call(outgoingState.hard), + Deleting() => deleting?.call(outgoingState.scope), }; } @@ -450,14 +586,14 @@ extension OutgoingStatePatternMatching on OutgoingState { TResult maybeWhen({ TResult Function()? sending, TResult Function()? updating, - TResult Function(bool hard)? deleting, + TResult Function(MessageDeleteScope scope)? deleting, required TResult orElse(), }) { final outgoingState = this; final result = switch (outgoingState) { Sending() => sending?.call(), Updating() => updating?.call(), - Deleting() => deleting?.call(outgoingState.hard), + Deleting() => deleting?.call(outgoingState.scope), }; return result ?? orElse(); @@ -519,13 +655,13 @@ extension CompletedStatePatternMatching on CompletedState { TResult when({ required TResult Function() sent, required TResult Function() updated, - required TResult Function(bool hard) deleted, + required TResult Function(MessageDeleteScope scope) deleted, }) { final completedState = this; return switch (completedState) { Sent() => sent(), Updated() => updated(), - Deleted() => deleted(completedState.hard), + Deleted() => deleted(completedState.scope), }; } @@ -534,13 +670,13 @@ extension CompletedStatePatternMatching on CompletedState { TResult? whenOrNull({ TResult? Function()? sent, TResult? Function()? updated, - TResult? Function(bool hard)? deleted, + TResult? Function(MessageDeleteScope scope)? deleted, }) { final completedState = this; return switch (completedState) { Sent() => sent?.call(), Updated() => updated?.call(), - Deleted() => deleted?.call(completedState.hard), + Deleted() => deleted?.call(completedState.scope), }; } @@ -549,14 +685,14 @@ extension CompletedStatePatternMatching on CompletedState { TResult maybeWhen({ TResult Function()? sent, TResult Function()? updated, - TResult Function(bool hard)? deleted, + TResult Function(MessageDeleteScope scope)? deleted, required TResult orElse(), }) { final completedState = this; final result = switch (completedState) { Sent() => sent?.call(), Updated() => updated?.call(), - Deleted() => deleted?.call(completedState.hard), + Deleted() => deleted?.call(completedState.scope), }; return result ?? orElse(); @@ -616,46 +752,52 @@ extension FailedStatePatternMatching on FailedState { /// @nodoc @optionalTypeArgs TResult when({ - required TResult Function() sendingFailed, - required TResult Function() updatingFailed, - required TResult Function(bool hard) deletingFailed, + 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(MessageDeleteScope scope) deletingFailed, }) { final failedState = this; return switch (failedState) { - SendingFailed() => sendingFailed(), - UpdatingFailed() => updatingFailed(), - DeletingFailed() => deletingFailed(failedState.hard), + SendingFailed() => sendingFailed(failedState.skipPush, failedState.skipEnrichUrl), + UpdatingFailed() => updatingFailed(failedState.skipPush, failedState.skipEnrichUrl), + PartialUpdatingFailed() => partialUpdatingFailed(failedState.set, failedState.unset, failedState.skipEnrichUrl), + DeletingFailed() => deletingFailed(failedState.scope), }; } /// @nodoc @optionalTypeArgs TResult? whenOrNull({ - TResult? Function()? sendingFailed, - TResult? Function()? updatingFailed, - TResult? Function(bool hard)? deletingFailed, + 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(MessageDeleteScope scope)? deletingFailed, }) { final failedState = this; return switch (failedState) { - SendingFailed() => sendingFailed?.call(), - UpdatingFailed() => updatingFailed?.call(), - DeletingFailed() => deletingFailed?.call(failedState.hard), + 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.scope), }; } /// @nodoc @optionalTypeArgs TResult maybeWhen({ - TResult Function()? sendingFailed, - TResult Function()? updatingFailed, - TResult Function(bool hard)? deletingFailed, + 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(MessageDeleteScope scope)? deletingFailed, required TResult orElse(), }) { final failedState = this; final result = switch (failedState) { - SendingFailed() => sendingFailed?.call(), - UpdatingFailed() => updatingFailed?.call(), - DeletingFailed() => deletingFailed?.call(failedState.hard), + 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.scope), }; return result ?? orElse(); @@ -666,12 +808,14 @@ 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 +825,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 +842,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 +850,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..f896483f61 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 @@ -473,13 +473,14 @@ class Updating implements OutgoingState { /// @nodoc @JsonSerializable() class Deleting implements OutgoingState { - const Deleting({this.hard = false, final String? $type}) + const Deleting( + {this.scope = MessageDeleteScope.softDeleteForAll, final String? $type}) : $type = $type ?? 'deleting'; factory Deleting.fromJson(Map json) => _$DeletingFromJson(json); @JsonKey() - final bool hard; + final MessageDeleteScope scope; @JsonKey(name: 'runtimeType') final String $type; @@ -503,16 +504,16 @@ class Deleting implements OutgoingState { return identical(this, other) || (other.runtimeType == runtimeType && other is Deleting && - (identical(other.hard, hard) || other.hard == hard)); + (identical(other.scope, scope) || other.scope == scope)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, hard); + int get hashCode => Object.hash(runtimeType, scope); @override String toString() { - return 'OutgoingState.deleting(hard: $hard)'; + return 'OutgoingState.deleting(scope: $scope)'; } } @@ -522,7 +523,9 @@ abstract mixin class $DeletingCopyWith<$Res> factory $DeletingCopyWith(Deleting value, $Res Function(Deleting) _then) = _$DeletingCopyWithImpl; @useResult - $Res call({bool hard}); + $Res call({MessageDeleteScope scope}); + + $MessageDeleteScopeCopyWith<$Res> get scope; } /// @nodoc @@ -536,15 +539,25 @@ class _$DeletingCopyWithImpl<$Res> implements $DeletingCopyWith<$Res> { /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') $Res call({ - Object? hard = null, + Object? scope = null, }) { return _then(Deleting( - hard: null == hard - ? _self.hard - : hard // ignore: cast_nullable_to_non_nullable - as bool, + scope: null == scope + ? _self.scope + : scope // ignore: cast_nullable_to_non_nullable + as MessageDeleteScope, )); } + + /// Create a copy of OutgoingState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $MessageDeleteScopeCopyWith<$Res> get scope { + return $MessageDeleteScopeCopyWith<$Res>(_self.scope, (value) { + return _then(_self.copyWith(scope: value)); + }); + } } CompletedState _$CompletedStateFromJson(Map json) { @@ -656,13 +669,14 @@ class Updated implements CompletedState { /// @nodoc @JsonSerializable() class Deleted implements CompletedState { - const Deleted({this.hard = false, final String? $type}) + const Deleted( + {this.scope = MessageDeleteScope.softDeleteForAll, final String? $type}) : $type = $type ?? 'deleted'; factory Deleted.fromJson(Map json) => _$DeletedFromJson(json); @JsonKey() - final bool hard; + final MessageDeleteScope scope; @JsonKey(name: 'runtimeType') final String $type; @@ -686,16 +700,16 @@ class Deleted implements CompletedState { return identical(this, other) || (other.runtimeType == runtimeType && other is Deleted && - (identical(other.hard, hard) || other.hard == hard)); + (identical(other.scope, scope) || other.scope == scope)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, hard); + int get hashCode => Object.hash(runtimeType, scope); @override String toString() { - return 'CompletedState.deleted(hard: $hard)'; + return 'CompletedState.deleted(scope: $scope)'; } } @@ -705,7 +719,9 @@ abstract mixin class $DeletedCopyWith<$Res> factory $DeletedCopyWith(Deleted value, $Res Function(Deleted) _then) = _$DeletedCopyWithImpl; @useResult - $Res call({bool hard}); + $Res call({MessageDeleteScope scope}); + + $MessageDeleteScopeCopyWith<$Res> get scope; } /// @nodoc @@ -719,15 +735,25 @@ class _$DeletedCopyWithImpl<$Res> implements $DeletedCopyWith<$Res> { /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') $Res call({ - Object? hard = null, + Object? scope = null, }) { return _then(Deleted( - hard: null == hard - ? _self.hard - : hard // ignore: cast_nullable_to_non_nullable - as bool, + scope: null == scope + ? _self.scope + : scope // ignore: cast_nullable_to_non_nullable + as MessageDeleteScope, )); } + + /// Create a copy of CompletedState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $MessageDeleteScopeCopyWith<$Res> get scope { + return $MessageDeleteScopeCopyWith<$Res>(_self.scope, (value) { + return _then(_self.copyWith(scope: value)); + }); + } } FailedState _$FailedStateFromJson(Map json) { @@ -736,6 +762,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 +802,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 +833,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,29 +923,195 @@ 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(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.updatingFailed()'; + 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, + )); } } /// @nodoc @JsonSerializable() class DeletingFailed implements FailedState { - const DeletingFailed({this.hard = false, final String? $type}) + const DeletingFailed( + {this.scope = MessageDeleteScope.softDeleteForAll, final String? $type}) : $type = $type ?? 'deletingFailed'; factory DeletingFailed.fromJson(Map json) => _$DeletingFailedFromJson(json); @JsonKey() - final bool hard; + final MessageDeleteScope scope; @JsonKey(name: 'runtimeType') final String $type; @@ -871,16 +1135,16 @@ class DeletingFailed implements FailedState { return identical(this, other) || (other.runtimeType == runtimeType && other is DeletingFailed && - (identical(other.hard, hard) || other.hard == hard)); + (identical(other.scope, scope) || other.scope == scope)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, hard); + int get hashCode => Object.hash(runtimeType, scope); @override String toString() { - return 'FailedState.deletingFailed(hard: $hard)'; + return 'FailedState.deletingFailed(scope: $scope)'; } } @@ -891,7 +1155,9 @@ abstract mixin class $DeletingFailedCopyWith<$Res> DeletingFailed value, $Res Function(DeletingFailed) _then) = _$DeletingFailedCopyWithImpl; @useResult - $Res call({bool hard}); + $Res call({MessageDeleteScope scope}); + + $MessageDeleteScopeCopyWith<$Res> get scope; } /// @nodoc @@ -906,15 +1172,25 @@ class _$DeletingFailedCopyWithImpl<$Res> /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') $Res call({ - Object? hard = null, + Object? scope = null, }) { return _then(DeletingFailed( - hard: null == hard - ? _self.hard - : hard // ignore: cast_nullable_to_non_nullable - as bool, + scope: null == scope + ? _self.scope + : scope // ignore: cast_nullable_to_non_nullable + as MessageDeleteScope, )); } + + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $MessageDeleteScopeCopyWith<$Res> get scope { + return $MessageDeleteScopeCopyWith<$Res>(_self.scope, (value) { + return _then(_self.copyWith(scope: value)); + }); + } } // dart format on 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..6336fe611a 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 @@ -6,134 +6,148 @@ part of 'message_state.dart'; // JsonSerializableGenerator // ************************************************************************** -MessageInitial _$MessageInitialFromJson(Map json) => - MessageInitial( - $type: json['runtimeType'] as String?, - ); - -Map _$MessageInitialToJson(MessageInitial instance) => - { - 'runtimeType': instance.$type, - }; - -MessageOutgoing _$MessageOutgoingFromJson(Map json) => - MessageOutgoing( - state: OutgoingState.fromJson(json['state'] as Map), - $type: json['runtimeType'] as String?, - ); - -Map _$MessageOutgoingToJson(MessageOutgoing instance) => - { - 'state': instance.state.toJson(), - 'runtimeType': instance.$type, - }; - -MessageCompleted _$MessageCompletedFromJson(Map json) => - MessageCompleted( - state: CompletedState.fromJson(json['state'] as Map), - $type: json['runtimeType'] as String?, - ); - -Map _$MessageCompletedToJson(MessageCompleted instance) => - { - 'state': instance.state.toJson(), - 'runtimeType': instance.$type, - }; - -MessageFailed _$MessageFailedFromJson(Map json) => - MessageFailed( - state: FailedState.fromJson(json['state'] as Map), - reason: json['reason'], - $type: json['runtimeType'] as String?, - ); - -Map _$MessageFailedToJson(MessageFailed instance) => - { - 'state': instance.state.toJson(), - 'reason': instance.reason, - 'runtimeType': instance.$type, - }; +MessageInitial _$MessageInitialFromJson(Map json) => MessageInitial( + $type: json['runtimeType'] as String?, +); + +Map _$MessageInitialToJson(MessageInitial instance) => { + 'runtimeType': instance.$type, +}; + +MessageOutgoing _$MessageOutgoingFromJson(Map json) => MessageOutgoing( + state: OutgoingState.fromJson(json['state'] as Map), + $type: json['runtimeType'] as String?, +); + +Map _$MessageOutgoingToJson(MessageOutgoing instance) => { + 'state': instance.state.toJson(), + 'runtimeType': instance.$type, +}; + +MessageCompleted _$MessageCompletedFromJson(Map json) => MessageCompleted( + state: CompletedState.fromJson(json['state'] as Map), + $type: json['runtimeType'] as String?, +); + +Map _$MessageCompletedToJson(MessageCompleted instance) => { + 'state': instance.state.toJson(), + 'runtimeType': instance.$type, +}; + +MessageFailed _$MessageFailedFromJson(Map json) => MessageFailed( + state: FailedState.fromJson(json['state'] as Map), + reason: json['reason'], + $type: json['runtimeType'] as String?, +); + +Map _$MessageFailedToJson(MessageFailed instance) => { + 'state': instance.state.toJson(), + 'reason': instance.reason, + 'runtimeType': instance.$type, +}; Sending _$SendingFromJson(Map json) => Sending( - $type: json['runtimeType'] as String?, - ); + $type: json['runtimeType'] as String?, +); Map _$SendingToJson(Sending instance) => { - 'runtimeType': instance.$type, - }; + 'runtimeType': instance.$type, +}; Updating _$UpdatingFromJson(Map json) => Updating( - $type: json['runtimeType'] as String?, - ); + $type: json['runtimeType'] as String?, +); Map _$UpdatingToJson(Updating instance) => { - 'runtimeType': instance.$type, - }; + 'runtimeType': instance.$type, +}; Deleting _$DeletingFromJson(Map json) => Deleting( - hard: json['hard'] as bool? ?? false, - $type: json['runtimeType'] as String?, - ); + scope: json['scope'] == null + ? MessageDeleteScope.softDeleteForAll + : MessageDeleteScope.fromJson(json['scope'] as Map), + $type: json['runtimeType'] as String?, +); Map _$DeletingToJson(Deleting instance) => { - 'hard': instance.hard, - 'runtimeType': instance.$type, - }; + 'scope': instance.scope.toJson(), + 'runtimeType': instance.$type, +}; Sent _$SentFromJson(Map json) => Sent( - $type: json['runtimeType'] as String?, - ); + $type: json['runtimeType'] as String?, +); Map _$SentToJson(Sent instance) => { - 'runtimeType': instance.$type, - }; + 'runtimeType': instance.$type, +}; Updated _$UpdatedFromJson(Map json) => Updated( - $type: json['runtimeType'] as String?, - ); + $type: json['runtimeType'] as String?, +); Map _$UpdatedToJson(Updated instance) => { - 'runtimeType': instance.$type, - }; + 'runtimeType': instance.$type, +}; Deleted _$DeletedFromJson(Map json) => Deleted( - hard: json['hard'] as bool? ?? false, - $type: json['runtimeType'] as String?, - ); + scope: json['scope'] == null + ? MessageDeleteScope.softDeleteForAll + : MessageDeleteScope.fromJson(json['scope'] as Map), + $type: json['runtimeType'] as String?, +); Map _$DeletedToJson(Deleted instance) => { - 'hard': instance.hard, - 'runtimeType': instance.$type, - }; - -SendingFailed _$SendingFailedFromJson(Map json) => - SendingFailed( - $type: json['runtimeType'] as String?, - ); - -Map _$SendingFailedToJson(SendingFailed instance) => - { - 'runtimeType': instance.$type, - }; - -UpdatingFailed _$UpdatingFailedFromJson(Map json) => - UpdatingFailed( - $type: json['runtimeType'] as String?, - ); - -Map _$UpdatingFailedToJson(UpdatingFailed instance) => - { - 'runtimeType': instance.$type, - }; - -DeletingFailed _$DeletingFailedFromJson(Map json) => - DeletingFailed( - hard: json['hard'] as bool? ?? false, - $type: json['runtimeType'] as String?, - ); - -Map _$DeletingFailedToJson(DeletingFailed instance) => - { - 'hard': instance.hard, - 'runtimeType': instance.$type, - }; + 'scope': instance.scope.toJson(), + 'runtimeType': instance.$type, +}; + +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, +}; + +DeletingFailed _$DeletingFailedFromJson(Map json) => DeletingFailed( + scope: json['scope'] == null + ? MessageDeleteScope.softDeleteForAll + : MessageDeleteScope.fromJson(json['scope'] as Map), + $type: json['runtimeType'] as String?, +); + +Map _$DeletingFailedToJson(DeletingFailed instance) => { + 'scope': instance.scope.toJson(), + 'runtimeType': instance.$type, +}; diff --git a/packages/stream_chat/lib/src/core/models/moderation.dart b/packages/stream_chat/lib/src/core/models/moderation.dart index e267afc7de..f3133a39a6 100644 --- a/packages/stream_chat/lib/src/core/models/moderation.dart +++ b/packages/stream_chat/lib/src/core/models/moderation.dart @@ -20,8 +20,7 @@ class Moderation extends Equatable { }); /// Create a new instance from a json - factory Moderation.fromJson(Map json) => - _$ModerationFromJson(json); + factory Moderation.fromJson(Map json) => _$ModerationFromJson(json); /// The action taken by the moderation system. @JsonKey( @@ -53,14 +52,14 @@ class Moderation extends Equatable { @override List get props => [ - action, - originalText, - textHarms, - imageHarms, - blocklistMatched, - semanticFilterMatched, - platformCircumvented, - ]; + action, + originalText, + textHarms, + imageHarms, + blocklistMatched, + semanticFilterMatched, + platformCircumvented, + ]; } /// The moderation action performed over the message. diff --git a/packages/stream_chat/lib/src/core/models/moderation.g.dart b/packages/stream_chat/lib/src/core/models/moderation.g.dart index 6f8ef841c1..f287d23d84 100644 --- a/packages/stream_chat/lib/src/core/models/moderation.g.dart +++ b/packages/stream_chat/lib/src/core/models/moderation.g.dart @@ -7,26 +7,21 @@ part of 'moderation.dart'; // ************************************************************************** Moderation _$ModerationFromJson(Map json) => Moderation( - action: ModerationAction.fromJson(json['action'] as String), - originalText: json['original_text'] as String, - textHarms: (json['text_harms'] as List?) - ?.map((e) => e as String) - .toList(), - imageHarms: (json['image_harms'] as List?) - ?.map((e) => e as String) - .toList(), - blocklistMatched: json['blocklist_matched'] as String?, - semanticFilterMatched: json['semantic_filter_matched'] as String?, - platformCircumvented: json['platform_circumvented'] as bool? ?? false, - ); + action: ModerationAction.fromJson(json['action'] as String), + originalText: json['original_text'] as String, + textHarms: (json['text_harms'] as List?)?.map((e) => e as String).toList(), + imageHarms: (json['image_harms'] as List?)?.map((e) => e as String).toList(), + blocklistMatched: json['blocklist_matched'] as String?, + semanticFilterMatched: json['semantic_filter_matched'] as String?, + platformCircumvented: json['platform_circumvented'] as bool? ?? false, +); -Map _$ModerationToJson(Moderation instance) => - { - 'action': ModerationAction.toJson(instance.action), - 'original_text': instance.originalText, - 'text_harms': instance.textHarms, - 'image_harms': instance.imageHarms, - 'blocklist_matched': instance.blocklistMatched, - 'semantic_filter_matched': instance.semanticFilterMatched, - 'platform_circumvented': instance.platformCircumvented, - }; +Map _$ModerationToJson(Moderation instance) => { + 'action': ModerationAction.toJson(instance.action), + 'original_text': instance.originalText, + 'text_harms': instance.textHarms, + 'image_harms': instance.imageHarms, + 'blocklist_matched': instance.blocklistMatched, + 'semantic_filter_matched': instance.semanticFilterMatched, + 'platform_circumvented': instance.platformCircumvented, +}; diff --git a/packages/stream_chat/lib/src/core/models/mute.g.dart b/packages/stream_chat/lib/src/core/models/mute.g.dart index 624df9cb70..b1e3dc7aac 100644 --- a/packages/stream_chat/lib/src/core/models/mute.g.dart +++ b/packages/stream_chat/lib/src/core/models/mute.g.dart @@ -7,19 +7,17 @@ part of 'mute.dart'; // ************************************************************************** Mute _$MuteFromJson(Map json) => Mute( - user: User.fromJson(json['user'] as Map), - target: User.fromJson(json['target'] as Map), - createdAt: DateTime.parse(json['created_at'] as String), - updatedAt: DateTime.parse(json['updated_at'] as String), - expires: json['expires'] == null - ? null - : DateTime.parse(json['expires'] as String), - ); + user: User.fromJson(json['user'] as Map), + target: User.fromJson(json['target'] as Map), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + expires: json['expires'] == null ? null : DateTime.parse(json['expires'] as String), +); Map _$MuteToJson(Mute instance) => { - 'user': instance.user.toJson(), - 'target': instance.target.toJson(), - 'created_at': instance.createdAt.toIso8601String(), - 'updated_at': instance.updatedAt.toIso8601String(), - 'expires': instance.expires?.toIso8601String(), - }; + 'user': instance.user.toJson(), + 'target': instance.target.toJson(), + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'expires': instance.expires?.toIso8601String(), +}; diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index c685761937..e3e8b83b9b 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -40,8 +40,8 @@ class OwnUser extends User { /// Create a new instance from json. factory OwnUser.fromJson(Map json) => _$OwnUserFromJson( - Serializer.moveToExtraDataFromRoot(json, topLevelFields), - ); + Serializer.moveToExtraDataFromRoot(json, topLevelFields), + ); /// Create a new instance from [User] object. factory OwnUser.fromUser(User user) => OwnUser.fromJson(user.toJson()); @@ -74,37 +74,37 @@ class OwnUser extends User { int? avgResponseTime, PushPreference? pushPreferences, PrivacySettings? privacySettings, - }) => - OwnUser( - id: id ?? this.id, - role: role ?? this.role, - name: name ?? - extraData?['name'] as String? ?? - // Using extraData value in order to not use id as name. - this.extraData['name'] as String?, - image: image ?? extraData?['image'] as String? ?? this.image, - banned: banned ?? this.banned, - banExpires: banExpires ?? this.banExpires, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - lastActive: lastActive ?? this.lastActive, - online: online ?? this.online, - extraData: extraData ?? this.extraData, - teams: teams ?? this.teams, - channelMutes: channelMutes ?? this.channelMutes, - devices: devices ?? this.devices, - mutes: mutes ?? this.mutes, - totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, - unreadChannels: unreadChannels ?? this.unreadChannels, - unreadThreads: unreadThreads ?? this.unreadThreads, - blockedUserIds: blockedUserIds ?? this.blockedUserIds, - language: language ?? this.language, - invisible: invisible ?? this.invisible, - teamsRole: teamsRole ?? this.teamsRole, - avgResponseTime: avgResponseTime ?? this.avgResponseTime, - pushPreferences: pushPreferences ?? this.pushPreferences, - privacySettings: privacySettings ?? this.privacySettings, - ); + }) => OwnUser( + id: id ?? this.id, + role: role ?? this.role, + name: + name ?? + extraData?['name'] as String? ?? + // Using extraData value in order to not use id as name. + this.extraData['name'] as String?, + image: image ?? extraData?['image'] as String? ?? this.image, + banned: banned ?? this.banned, + banExpires: banExpires ?? this.banExpires, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + lastActive: lastActive ?? this.lastActive, + online: online ?? this.online, + extraData: extraData ?? this.extraData, + teams: teams ?? this.teams, + channelMutes: channelMutes ?? this.channelMutes, + devices: devices ?? this.devices, + mutes: mutes ?? this.mutes, + totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, + unreadChannels: unreadChannels ?? this.unreadChannels, + unreadThreads: unreadThreads ?? this.unreadThreads, + blockedUserIds: blockedUserIds ?? this.blockedUserIds, + language: language ?? this.language, + invisible: invisible ?? this.invisible, + teamsRole: teamsRole ?? this.teamsRole, + avgResponseTime: avgResponseTime ?? this.avgResponseTime, + pushPreferences: pushPreferences ?? this.pushPreferences, + privacySettings: privacySettings ?? this.privacySettings, + ); /// Returns a new [OwnUser] that is a combination of this ownUser /// and the given [other] ownUser. diff --git a/packages/stream_chat/lib/src/core/models/own_user.g.dart b/packages/stream_chat/lib/src/core/models/own_user.g.dart index 21fc564e29..d8e69df5dd 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.g.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.g.dart @@ -7,90 +7,62 @@ part of 'own_user.dart'; // ************************************************************************** OwnUser _$OwnUserFromJson(Map json) => OwnUser( - devices: (json['devices'] as List?) - ?.map((e) => Device.fromJson(e as Map)) - .toList() ?? - const [], - mutes: (json['mutes'] as List?) - ?.map((e) => Mute.fromJson(e as Map)) - .toList() ?? - const [], - totalUnreadCount: (json['total_unread_count'] as num?)?.toInt() ?? 0, - unreadChannels: (json['unread_channels'] as num?)?.toInt() ?? 0, - channelMutes: (json['channel_mutes'] as List?) - ?.map((e) => ChannelMute.fromJson(e as Map)) - .toList() ?? - const [], - unreadThreads: (json['unread_threads'] as num?)?.toInt() ?? 0, - blockedUserIds: (json['blocked_user_ids'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], - pushPreferences: json['push_preferences'] == null - ? null - : PushPreference.fromJson( - json['push_preferences'] as Map), - privacySettings: json['privacy_settings'] == null - ? null - : PrivacySettings.fromJson( - json['privacy_settings'] as Map), - id: json['id'] as String, - role: json['role'] 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), - lastActive: json['last_active'] == null - ? null - : DateTime.parse(json['last_active'] as String), - online: json['online'] as bool? ?? false, - extraData: json['extra_data'] as Map? ?? const {}, - banned: json['banned'] as bool? ?? false, - banExpires: json['ban_expires'] == null - ? null - : DateTime.parse(json['ban_expires'] as String), - teams: - (json['teams'] as List?)?.map((e) => e as String).toList() ?? - const [], - language: json['language'] as String?, - invisible: json['invisible'] as bool?, - teamsRole: (json['teams_role'] as Map?)?.map( - (k, e) => MapEntry(k, e as String), - ), - avgResponseTime: (json['avg_response_time'] as num?)?.toInt(), - ); + devices: + (json['devices'] as List?)?.map((e) => Device.fromJson(e as Map)).toList() ?? const [], + mutes: (json['mutes'] as List?)?.map((e) => Mute.fromJson(e as Map)).toList() ?? const [], + totalUnreadCount: (json['total_unread_count'] as num?)?.toInt() ?? 0, + unreadChannels: (json['unread_channels'] as num?)?.toInt() ?? 0, + channelMutes: + (json['channel_mutes'] as List?)?.map((e) => ChannelMute.fromJson(e as Map)).toList() ?? + const [], + unreadThreads: (json['unread_threads'] as num?)?.toInt() ?? 0, + blockedUserIds: (json['blocked_user_ids'] as List?)?.map((e) => e as String).toList() ?? const [], + pushPreferences: json['push_preferences'] == null + ? null + : PushPreference.fromJson(json['push_preferences'] as Map), + privacySettings: json['privacy_settings'] == null + ? null + : PrivacySettings.fromJson(json['privacy_settings'] as Map), + id: json['id'] as String, + role: json['role'] 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), + lastActive: json['last_active'] == null ? null : DateTime.parse(json['last_active'] as String), + online: json['online'] as bool? ?? false, + extraData: json['extra_data'] as Map? ?? const {}, + banned: json['banned'] as bool? ?? false, + banExpires: json['ban_expires'] == null ? null : DateTime.parse(json['ban_expires'] as String), + teams: (json['teams'] as List?)?.map((e) => e as String).toList() ?? const [], + language: json['language'] as String?, + invisible: json['invisible'] as bool?, + teamsRole: (json['teams_role'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ), + avgResponseTime: (json['avg_response_time'] as num?)?.toInt(), +); Map _$OwnUserToJson(OwnUser instance) => { - 'id': instance.id, - if (instance.role case final value?) 'role': value, - 'teams': instance.teams, - if (instance.createdAt?.toIso8601String() case final value?) - 'created_at': value, - if (instance.updatedAt?.toIso8601String() case final value?) - 'updated_at': value, - if (instance.lastActive?.toIso8601String() case final value?) - 'last_active': value, - 'online': instance.online, - 'banned': instance.banned, - if (instance.banExpires?.toIso8601String() case final value?) - 'ban_expires': value, - if (instance.language case final value?) 'language': value, - if (instance.invisible case final value?) 'invisible': value, - if (instance.teamsRole case final value?) 'teams_role': value, - if (instance.avgResponseTime case final value?) - 'avg_response_time': value, - 'extra_data': instance.extraData, - 'devices': instance.devices.map((e) => e.toJson()).toList(), - 'mutes': instance.mutes.map((e) => e.toJson()).toList(), - 'channel_mutes': instance.channelMutes.map((e) => e.toJson()).toList(), - 'total_unread_count': instance.totalUnreadCount, - 'unread_channels': instance.unreadChannels, - 'unread_threads': instance.unreadThreads, - 'blocked_user_ids': instance.blockedUserIds, - if (instance.pushPreferences?.toJson() case final value?) - 'push_preferences': value, - if (instance.privacySettings?.toJson() case final value?) - 'privacy_settings': value, - }; + 'id': instance.id, + if (instance.role case final value?) 'role': value, + 'teams': instance.teams, + if (instance.createdAt?.toIso8601String() case final value?) 'created_at': value, + if (instance.updatedAt?.toIso8601String() case final value?) 'updated_at': value, + if (instance.lastActive?.toIso8601String() case final value?) 'last_active': value, + 'online': instance.online, + 'banned': instance.banned, + if (instance.banExpires?.toIso8601String() case final value?) 'ban_expires': value, + if (instance.language case final value?) 'language': value, + if (instance.invisible case final value?) 'invisible': value, + if (instance.teamsRole case final value?) 'teams_role': value, + if (instance.avgResponseTime case final value?) 'avg_response_time': value, + 'extra_data': instance.extraData, + 'devices': instance.devices.map((e) => e.toJson()).toList(), + 'mutes': instance.mutes.map((e) => e.toJson()).toList(), + 'channel_mutes': instance.channelMutes.map((e) => e.toJson()).toList(), + 'total_unread_count': instance.totalUnreadCount, + 'unread_channels': instance.unreadChannels, + 'unread_threads': instance.unreadThreads, + 'blocked_user_ids': instance.blockedUserIds, + if (instance.pushPreferences?.toJson() case final value?) 'push_preferences': value, + if (instance.privacySettings?.toJson() case final value?) 'privacy_settings': value, +}; diff --git a/packages/stream_chat/lib/src/core/models/poll.dart b/packages/stream_chat/lib/src/core/models/poll.dart index dd61feac2b..a0fb4bfc1b 100644 --- a/packages/stream_chat/lib/src/core/models/poll.dart +++ b/packages/stream_chat/lib/src/core/models/poll.dart @@ -57,9 +57,9 @@ class Poll extends Equatable implements ComparableFieldProvider { this.createdBy, this.ownVotesAndAnswers = const [], this.extraData = const {}, - }) : id = id ?? const Uuid().v4(), - createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(); + }) : id = id ?? const Uuid().v4(), + createdAt = createdAt ?? DateTime.now(), + updatedAt = updatedAt ?? DateTime.now(); /// Create a new instance from a json factory Poll.fromJson(Map json) => @@ -167,8 +167,7 @@ class Poll extends Equatable implements ComparableFieldProvider { final Map extraData; /// Serialize to json - Map toJson() => - Serializer.moveFromExtraDataToRoot(_$PollToJson(this)); + Map toJson() => Serializer.moveFromExtraDataToRoot(_$PollToJson(this)); /// Creates a copy of [Poll] with specified attributes overridden. Poll copyWith({ @@ -193,33 +192,29 @@ class Poll extends Equatable implements ComparableFieldProvider { DateTime? createdAt, DateTime? updatedAt, Map? extraData, - }) => - Poll( - 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 == _nullConst - ? this.maxVotesAllowed - : maxVotesAllowed as int?, - allowUserSuggestedOptions: - allowUserSuggestedOptions ?? this.allowUserSuggestedOptions, - allowAnswers: allowAnswers ?? this.allowAnswers, - isClosed: isClosed ?? this.isClosed, - voteCountsByOption: voteCountsByOption ?? this.voteCountsByOption, - ownVotesAndAnswers: ownVotesAndAnswers ?? this.ownVotesAndAnswers, - voteCount: voteCount ?? this.voteCount, - answersCount: answersCount ?? this.answersCount, - latestVotesByOption: latestVotesByOption ?? this.latestVotesByOption, - latestAnswers: latestAnswers ?? this.latestAnswers, - createdById: createdById ?? this.createdById, - createdBy: createdBy ?? this.createdBy, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - extraData: extraData ?? this.extraData, - ); + }) => Poll( + 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 == _nullConst ? this.maxVotesAllowed : maxVotesAllowed as int?, + allowUserSuggestedOptions: allowUserSuggestedOptions ?? this.allowUserSuggestedOptions, + allowAnswers: allowAnswers ?? this.allowAnswers, + isClosed: isClosed ?? this.isClosed, + voteCountsByOption: voteCountsByOption ?? this.voteCountsByOption, + ownVotesAndAnswers: ownVotesAndAnswers ?? this.ownVotesAndAnswers, + voteCount: voteCount ?? this.voteCount, + answersCount: answersCount ?? this.answersCount, + latestVotesByOption: latestVotesByOption ?? this.latestVotesByOption, + latestAnswers: latestAnswers ?? this.latestAnswers, + createdById: createdById ?? this.createdById, + createdBy: createdBy ?? this.createdBy, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + extraData: extraData ?? this.extraData, + ); /// Known top level fields. /// @@ -250,27 +245,27 @@ class Poll extends Equatable implements ComparableFieldProvider { @override List get props => [ - id, - name, - description, - options, - votingVisibility, - enforceUniqueVote, - maxVotesAllowed, - allowUserSuggestedOptions, - allowAnswers, - isClosed, - voteCountsByOption, - ownVotesAndAnswers, - voteCount, - answersCount, - latestVotesByOption, - latestAnswers, - createdById, - createdBy, - createdAt, - updatedAt, - ]; + id, + name, + description, + options, + votingVisibility, + enforceUniqueVote, + maxVotesAllowed, + allowUserSuggestedOptions, + allowAnswers, + isClosed, + voteCountsByOption, + ownVotesAndAnswers, + voteCount, + answersCount, + latestVotesByOption, + latestAnswers, + createdById, + createdBy, + createdAt, + updatedAt, + ]; @override ComparableField? getComparableField(String sortKey) { @@ -319,31 +314,28 @@ extension PollX on Poll { /// Whether the poll is already closed and the provided option is the one, /// and **the only one** with the most votes. - bool isOptionWinner(PollOption option) => - isClosed && isOptionWithMostVotes(option); + bool isOptionWinner(PollOption option) => isClosed && isOptionWithMostVotes(option); /// Whether the poll is already closed and the provided option is one of that /// has the most votes. - bool isOptionOneOfTheWinners(PollOption option) => - isClosed && isOptionWithMaximumVotes(option); + bool isOptionOneOfTheWinners(PollOption option) => isClosed && isOptionWithMaximumVotes(option); /// Whether the provided option is the one, and **the only one** with the most /// votes. bool isOptionWithMostVotes(PollOption option) { final optionsWithMostVotes = { for (final entry in voteCountsByOption.entries) - if (entry.value == currentMaximumVoteCount) entry.key: entry.value + if (entry.value == currentMaximumVoteCount) entry.key: entry.value, }; - return optionsWithMostVotes.length == 1 && - optionsWithMostVotes[option.id] != null; + return optionsWithMostVotes.length == 1 && optionsWithMostVotes[option.id] != null; } /// Whether the provided option is one of that has the most votes. bool isOptionWithMaximumVotes(PollOption option) { final optionsWithMostVotes = { for (final entry in voteCountsByOption.entries) - if (entry.value == currentMaximumVoteCount) entry.key: entry.value + if (entry.value == currentMaximumVoteCount) entry.key: entry.value, }; return optionsWithMostVotes[option.id] != null; @@ -368,6 +360,5 @@ extension PollX on Poll { /// Returns a Boolean value indicating whether the current user has voted the /// given option. - bool hasCurrentUserVotedFor(PollOption option) => - ownVotesAndAnswers.any((it) => it.optionId == option.id); + bool hasCurrentUserVotedFor(PollOption option) => ownVotesAndAnswers.any((it) => it.optionId == option.id); } diff --git a/packages/stream_chat/lib/src/core/models/poll.g.dart b/packages/stream_chat/lib/src/core/models/poll.g.dart index 475232c4cc..3a4432041e 100644 --- a/packages/stream_chat/lib/src/core/models/poll.g.dart +++ b/packages/stream_chat/lib/src/core/models/poll.g.dart @@ -7,73 +7,55 @@ part of 'poll.dart'; // ************************************************************************** Poll _$PollFromJson(Map json) => Poll( - id: json['id'] as String?, - name: json['name'] as String, - description: json['description'] as String?, - options: (json['options'] as List) - .map((e) => PollOption.fromJson(e as Map)) - .toList(), - votingVisibility: $enumDecodeNullable( - _$VotingVisibilityEnumMap, json['voting_visibility']) ?? - VotingVisibility.public, - enforceUniqueVote: json['enforce_unique_vote'] as bool? ?? true, - maxVotesAllowed: (json['max_votes_allowed'] as num?)?.toInt(), - allowAnswers: json['allow_answers'] as bool? ?? false, - latestAnswers: (json['latest_answers'] as List?) - ?.map((e) => PollVote.fromJson(e as Map)) - .toList() ?? - const [], - answersCount: (json['answers_count'] as num?)?.toInt() ?? 0, - allowUserSuggestedOptions: - json['allow_user_suggested_options'] as bool? ?? false, - isClosed: json['is_closed'] as bool? ?? false, - 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), - voteCountsByOption: - (json['vote_counts_by_option'] as Map?)?.map( - (k, e) => MapEntry(k, (e as num).toInt()), - ) ?? - const {}, - voteCount: (json['vote_count'] as num?)?.toInt() ?? 0, - latestVotesByOption: - (json['latest_votes_by_option'] as Map?)?.map( - (k, e) => MapEntry( - k, - (e as List) - .map( - (e) => PollVote.fromJson(e as Map)) - .toList()), - ) ?? - const {}, - createdById: json['created_by_id'] as String?, - createdBy: json['created_by'] == null - ? null - : User.fromJson(json['created_by'] as Map), - ownVotesAndAnswers: (json['own_votes'] as List?) - ?.map((e) => PollVote.fromJson(e as Map)) - .toList() ?? - const [], - extraData: json['extra_data'] as Map? ?? const {}, - ); + id: json['id'] as String?, + name: json['name'] as String, + description: json['description'] as String?, + options: (json['options'] as List).map((e) => PollOption.fromJson(e as Map)).toList(), + votingVisibility: + $enumDecodeNullable(_$VotingVisibilityEnumMap, json['voting_visibility']) ?? VotingVisibility.public, + enforceUniqueVote: json['enforce_unique_vote'] as bool? ?? true, + maxVotesAllowed: (json['max_votes_allowed'] as num?)?.toInt(), + allowAnswers: json['allow_answers'] as bool? ?? false, + latestAnswers: + (json['latest_answers'] as List?)?.map((e) => PollVote.fromJson(e as Map)).toList() ?? + const [], + answersCount: (json['answers_count'] as num?)?.toInt() ?? 0, + allowUserSuggestedOptions: json['allow_user_suggested_options'] as bool? ?? false, + isClosed: json['is_closed'] as bool? ?? false, + 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), + voteCountsByOption: + (json['vote_counts_by_option'] as Map?)?.map( + (k, e) => MapEntry(k, (e as num).toInt()), + ) ?? + const {}, + voteCount: (json['vote_count'] as num?)?.toInt() ?? 0, + latestVotesByOption: + (json['latest_votes_by_option'] as Map?)?.map( + (k, e) => MapEntry(k, (e as List).map((e) => PollVote.fromJson(e as Map)).toList()), + ) ?? + const {}, + createdById: json['created_by_id'] as String?, + createdBy: json['created_by'] == null ? null : User.fromJson(json['created_by'] as Map), + ownVotesAndAnswers: + (json['own_votes'] as List?)?.map((e) => PollVote.fromJson(e as Map)).toList() ?? + const [], + extraData: json['extra_data'] as Map? ?? const {}, +); Map _$PollToJson(Poll instance) => { - 'id': instance.id, - 'name': instance.name, - 'description': instance.description, - 'options': instance.options.map((e) => e.toJson()).toList(), - 'voting_visibility': - _$VotingVisibilityEnumMap[instance.votingVisibility]!, - 'enforce_unique_vote': instance.enforceUniqueVote, - 'max_votes_allowed': instance.maxVotesAllowed, - 'allow_user_suggested_options': instance.allowUserSuggestedOptions, - 'allow_answers': instance.allowAnswers, - 'is_closed': instance.isClosed, - 'extra_data': instance.extraData, - }; + 'id': instance.id, + 'name': instance.name, + 'description': instance.description, + 'options': instance.options.map((e) => e.toJson()).toList(), + 'voting_visibility': _$VotingVisibilityEnumMap[instance.votingVisibility]!, + 'enforce_unique_vote': instance.enforceUniqueVote, + 'max_votes_allowed': instance.maxVotesAllowed, + 'allow_user_suggested_options': instance.allowUserSuggestedOptions, + 'allow_answers': instance.allowAnswers, + 'is_closed': instance.isClosed, + 'extra_data': instance.extraData, +}; const _$VotingVisibilityEnumMap = { VotingVisibility.anonymous: 'anonymous', diff --git a/packages/stream_chat/lib/src/core/models/poll_option.dart b/packages/stream_chat/lib/src/core/models/poll_option.dart index 9ef99d07e3..fb8448af05 100644 --- a/packages/stream_chat/lib/src/core/models/poll_option.dart +++ b/packages/stream_chat/lib/src/core/models/poll_option.dart @@ -23,10 +23,9 @@ class PollOption extends Equatable { }); /// Create a new instance from a json - factory PollOption.fromJson(Map json) => - _$PollOptionFromJson( - Serializer.moveToExtraDataFromRoot(json, topLevelFields), - ); + factory PollOption.fromJson(Map json) => _$PollOptionFromJson( + Serializer.moveToExtraDataFromRoot(json, topLevelFields), + ); /// The unique identifier of the poll option. @JsonKey(includeIfNull: false) @@ -39,20 +38,18 @@ class PollOption extends Equatable { final Map extraData; /// Serialize to json - Map toJson() => - Serializer.moveFromExtraDataToRoot(_$PollOptionToJson(this)); + Map toJson() => Serializer.moveFromExtraDataToRoot(_$PollOptionToJson(this)); /// Creates a copy of [PollOption] with specified attributes overridden. PollOption copyWith({ Object? id = _nullConst, String? text, Map? extraData, - }) => - PollOption( - id: id == _nullConst ? this.id : id as String?, - text: text ?? this.text, - extraData: extraData ?? this.extraData, - ); + }) => PollOption( + id: id == _nullConst ? this.id : id as String?, + text: text ?? this.text, + extraData: extraData ?? this.extraData, + ); /// Known top level fields. /// diff --git a/packages/stream_chat/lib/src/core/models/poll_option.g.dart b/packages/stream_chat/lib/src/core/models/poll_option.g.dart index b7db997113..1f8ba16aa2 100644 --- a/packages/stream_chat/lib/src/core/models/poll_option.g.dart +++ b/packages/stream_chat/lib/src/core/models/poll_option.g.dart @@ -7,14 +7,13 @@ part of 'poll_option.dart'; // ************************************************************************** PollOption _$PollOptionFromJson(Map json) => PollOption( - id: json['id'] as String?, - text: json['text'] as String, - extraData: json['extra_data'] as Map? ?? const {}, - ); + id: json['id'] as String?, + text: json['text'] as String, + extraData: json['extra_data'] as Map? ?? const {}, +); -Map _$PollOptionToJson(PollOption instance) => - { - if (instance.id case final value?) 'id': value, - 'text': instance.text, - 'extra_data': instance.extraData, - }; +Map _$PollOptionToJson(PollOption instance) => { + if (instance.id case final value?) 'id': value, + 'text': instance.text, + 'extra_data': instance.extraData, +}; diff --git a/packages/stream_chat/lib/src/core/models/poll_vote.dart b/packages/stream_chat/lib/src/core/models/poll_vote.dart index a48bb03d9e..3f979cc13c 100644 --- a/packages/stream_chat/lib/src/core/models/poll_vote.dart +++ b/packages/stream_chat/lib/src/core/models/poll_vote.dart @@ -20,17 +20,16 @@ class PollVote extends Equatable implements ComparableFieldProvider { DateTime? updatedAt, this.userId, this.user, - }) : assert( - optionId != null || answerText != null, - 'Either optionId or answerText must be provided', - ), - isAnswer = answerText != null, - createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(); + }) : assert( + optionId != null || answerText != null, + 'Either optionId or answerText must be provided', + ), + isAnswer = answerText != null, + createdAt = createdAt ?? DateTime.now(), + updatedAt = updatedAt ?? DateTime.now(); /// Create a new instance from a json - factory PollVote.fromJson(Map json) => - _$PollVoteFromJson(json); + factory PollVote.fromJson(Map json) => _$PollVoteFromJson(json); /// The unique identifier of the poll vote. @JsonKey(includeIfNull: false) @@ -81,30 +80,29 @@ class PollVote extends Equatable implements ComparableFieldProvider { DateTime? updatedAt, String? userId, User? user, - }) => - PollVote( - id: id ?? this.id, - pollId: pollId ?? this.pollId, - optionId: optionId ?? this.optionId, - answerText: answerText ?? this.answerText, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - userId: userId ?? this.userId, - user: user ?? this.user, - ); + }) => PollVote( + id: id ?? this.id, + pollId: pollId ?? this.pollId, + optionId: optionId ?? this.optionId, + answerText: answerText ?? this.answerText, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + userId: userId ?? this.userId, + user: user ?? this.user, + ); @override List get props => [ - id, - pollId, - optionId, - isAnswer, - answerText, - createdAt, - updatedAt, - userId, - user, - ]; + id, + pollId, + optionId, + isAnswer, + answerText, + createdAt, + updatedAt, + userId, + user, + ]; @override ComparableField? getComparableField(String sortKey) { diff --git a/packages/stream_chat/lib/src/core/models/poll_vote.g.dart b/packages/stream_chat/lib/src/core/models/poll_vote.g.dart index 1ceb22c1d8..8eedbb3061 100644 --- a/packages/stream_chat/lib/src/core/models/poll_vote.g.dart +++ b/packages/stream_chat/lib/src/core/models/poll_vote.g.dart @@ -7,24 +7,18 @@ part of 'poll_vote.dart'; // ************************************************************************** PollVote _$PollVoteFromJson(Map json) => PollVote( - id: json['id'] as String?, - pollId: json['poll_id'] as String?, - optionId: json['option_id'] as String?, - answerText: json['answer_text'] 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), - userId: json['user_id'] as String?, - user: json['user'] == null - ? null - : User.fromJson(json['user'] as Map), - ); + id: json['id'] as String?, + pollId: json['poll_id'] as String?, + optionId: json['option_id'] as String?, + answerText: json['answer_text'] 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), + userId: json['user_id'] as String?, + user: json['user'] == null ? null : User.fromJson(json['user'] as Map), +); Map _$PollVoteToJson(PollVote instance) => { - if (instance.id case final value?) 'id': value, - if (instance.optionId case final value?) 'option_id': value, - if (instance.answerText case final value?) 'answer_text': value, - }; + if (instance.id case final value?) 'id': value, + if (instance.optionId case final value?) 'option_id': value, + if (instance.answerText case final value?) 'answer_text': value, +}; diff --git a/packages/stream_chat/lib/src/core/models/privacy_settings.g.dart b/packages/stream_chat/lib/src/core/models/privacy_settings.g.dart index e433675833..b888255ad6 100644 --- a/packages/stream_chat/lib/src/core/models/privacy_settings.g.dart +++ b/packages/stream_chat/lib/src/core/models/privacy_settings.g.dart @@ -6,57 +6,44 @@ part of 'privacy_settings.dart'; // JsonSerializableGenerator // ************************************************************************** -PrivacySettings _$PrivacySettingsFromJson(Map json) => - PrivacySettings( - typingIndicators: json['typing_indicators'] == null - ? null - : TypingIndicators.fromJson( - json['typing_indicators'] as Map), - readReceipts: json['read_receipts'] == null - ? null - : ReadReceipts.fromJson( - json['read_receipts'] as Map), - deliveryReceipts: json['delivery_receipts'] == null - ? null - : DeliveryReceipts.fromJson( - json['delivery_receipts'] as Map), - ); - -Map _$PrivacySettingsToJson(PrivacySettings instance) => - { - if (instance.typingIndicators?.toJson() case final value?) - 'typing_indicators': value, - if (instance.readReceipts?.toJson() case final value?) - 'read_receipts': value, - if (instance.deliveryReceipts?.toJson() case final value?) - 'delivery_receipts': value, - }; - -TypingIndicators _$TypingIndicatorsFromJson(Map json) => - TypingIndicators( - enabled: json['enabled'] as bool? ?? true, - ); - -Map _$TypingIndicatorsToJson(TypingIndicators instance) => - { - 'enabled': instance.enabled, - }; +PrivacySettings _$PrivacySettingsFromJson(Map json) => PrivacySettings( + typingIndicators: json['typing_indicators'] == null + ? null + : TypingIndicators.fromJson(json['typing_indicators'] as Map), + readReceipts: json['read_receipts'] == null + ? null + : ReadReceipts.fromJson(json['read_receipts'] as Map), + deliveryReceipts: json['delivery_receipts'] == null + ? null + : DeliveryReceipts.fromJson(json['delivery_receipts'] as Map), +); + +Map _$PrivacySettingsToJson(PrivacySettings instance) => { + if (instance.typingIndicators?.toJson() case final value?) 'typing_indicators': value, + if (instance.readReceipts?.toJson() case final value?) 'read_receipts': value, + if (instance.deliveryReceipts?.toJson() case final value?) 'delivery_receipts': value, +}; + +TypingIndicators _$TypingIndicatorsFromJson(Map json) => TypingIndicators( + enabled: json['enabled'] as bool? ?? true, +); + +Map _$TypingIndicatorsToJson(TypingIndicators instance) => { + 'enabled': instance.enabled, +}; ReadReceipts _$ReadReceiptsFromJson(Map json) => ReadReceipts( - enabled: json['enabled'] as bool? ?? true, - ); - -Map _$ReadReceiptsToJson(ReadReceipts instance) => - { - 'enabled': instance.enabled, - }; - -DeliveryReceipts _$DeliveryReceiptsFromJson(Map json) => - DeliveryReceipts( - enabled: json['enabled'] as bool? ?? true, - ); - -Map _$DeliveryReceiptsToJson(DeliveryReceipts instance) => - { - 'enabled': instance.enabled, - }; + enabled: json['enabled'] as bool? ?? true, +); + +Map _$ReadReceiptsToJson(ReadReceipts instance) => { + 'enabled': instance.enabled, +}; + +DeliveryReceipts _$DeliveryReceiptsFromJson(Map json) => DeliveryReceipts( + enabled: json['enabled'] as bool? ?? true, +); + +Map _$DeliveryReceiptsToJson(DeliveryReceipts instance) => { + 'enabled': instance.enabled, +}; diff --git a/packages/stream_chat/lib/src/core/models/push_preference.dart b/packages/stream_chat/lib/src/core/models/push_preference.dart index 00ba266a1d..a3de2f8b3f 100644 --- a/packages/stream_chat/lib/src/core/models/push_preference.dart +++ b/packages/stream_chat/lib/src/core/models/push_preference.dart @@ -82,8 +82,7 @@ class PushPreference extends Equatable { }); /// Create a new instance from a json - factory PushPreference.fromJson(Map json) => - _$PushPreferenceFromJson(json); + factory PushPreference.fromJson(Map json) => _$PushPreferenceFromJson(json); /// Push preference for calls final CallLevel? callLevel; @@ -111,8 +110,7 @@ class ChannelPushPreference extends Equatable { }); /// Create a new instance from a json - factory ChannelPushPreference.fromJson(Map json) => - _$ChannelPushPreferenceFromJson(json); + factory ChannelPushPreference.fromJson(Map json) => _$ChannelPushPreferenceFromJson(json); /// Push preference for chat messages final ChatLevel? chatLevel; diff --git a/packages/stream_chat/lib/src/core/models/push_preference.g.dart b/packages/stream_chat/lib/src/core/models/push_preference.g.dart index 971c0bd1f9..1cc2cc6cc1 100644 --- a/packages/stream_chat/lib/src/core/models/push_preference.g.dart +++ b/packages/stream_chat/lib/src/core/models/push_preference.g.dart @@ -6,47 +6,32 @@ part of 'push_preference.dart'; // JsonSerializableGenerator // ************************************************************************** -Map _$PushPreferenceInputToJson( - PushPreferenceInput instance) => - { - if (instance.channelCid case final value?) 'channel_cid': value, - if (instance.callLevel case final value?) 'call_level': value, - if (instance.chatLevel case final value?) 'chat_level': value, - if (instance.disabledUntil?.toIso8601String() case final value?) - 'disabled_until': value, - if (instance.removeDisable case final value?) 'remove_disable': value, - }; +Map _$PushPreferenceInputToJson(PushPreferenceInput instance) => { + if (instance.channelCid case final value?) 'channel_cid': value, + if (instance.callLevel case final value?) 'call_level': value, + if (instance.chatLevel case final value?) 'chat_level': value, + if (instance.disabledUntil?.toIso8601String() case final value?) 'disabled_until': value, + if (instance.removeDisable case final value?) 'remove_disable': value, +}; -PushPreference _$PushPreferenceFromJson(Map json) => - PushPreference( - callLevel: json['call_level'] as CallLevel?, - chatLevel: json['chat_level'] as ChatLevel?, - disabledUntil: json['disabled_until'] == null - ? null - : DateTime.parse(json['disabled_until'] as String), - ); +PushPreference _$PushPreferenceFromJson(Map json) => PushPreference( + callLevel: json['call_level'] as CallLevel?, + chatLevel: json['chat_level'] as ChatLevel?, + disabledUntil: json['disabled_until'] == null ? null : DateTime.parse(json['disabled_until'] as String), +); -Map _$PushPreferenceToJson(PushPreference instance) => - { - if (instance.callLevel case final value?) 'call_level': value, - if (instance.chatLevel case final value?) 'chat_level': value, - if (instance.disabledUntil?.toIso8601String() case final value?) - 'disabled_until': value, - }; +Map _$PushPreferenceToJson(PushPreference instance) => { + if (instance.callLevel case final value?) 'call_level': value, + if (instance.chatLevel case final value?) 'chat_level': value, + if (instance.disabledUntil?.toIso8601String() case final value?) 'disabled_until': value, +}; -ChannelPushPreference _$ChannelPushPreferenceFromJson( - Map json) => - ChannelPushPreference( - chatLevel: json['chat_level'] as ChatLevel?, - disabledUntil: json['disabled_until'] == null - ? null - : DateTime.parse(json['disabled_until'] as String), - ); +ChannelPushPreference _$ChannelPushPreferenceFromJson(Map json) => ChannelPushPreference( + chatLevel: json['chat_level'] as ChatLevel?, + disabledUntil: json['disabled_until'] == null ? null : DateTime.parse(json['disabled_until'] as String), +); -Map _$ChannelPushPreferenceToJson( - ChannelPushPreference instance) => - { - if (instance.chatLevel case final value?) 'chat_level': value, - if (instance.disabledUntil?.toIso8601String() case final value?) - 'disabled_until': value, - }; +Map _$ChannelPushPreferenceToJson(ChannelPushPreference instance) => { + if (instance.chatLevel case final value?) 'chat_level': value, + if (instance.disabledUntil?.toIso8601String() case final value?) 'disabled_until': value, +}; diff --git a/packages/stream_chat/lib/src/core/models/reaction.dart b/packages/stream_chat/lib/src/core/models/reaction.dart index 474b6dff6a..c60c7b85b8 100644 --- a/packages/stream_chat/lib/src/core/models/reaction.dart +++ b/packages/stream_chat/lib/src/core/models/reaction.dart @@ -1,4 +1,6 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:stream_chat/src/core/models/comparable_field.dart'; import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/core/util/serializer.dart'; @@ -6,94 +8,148 @@ part 'reaction.g.dart'; /// The class that defines a reaction @JsonSerializable() -class Reaction { +class Reaction extends Equatable implements ComparableFieldProvider { /// 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(); + }) : userId = userId ?? user?.id, + createdAt = createdAt ?? DateTime.timestamp(), + updatedAt = updatedAt ?? DateTime.timestamp(); /// Create a new instance from a json - factory Reaction.fromJson(Map json) => - _$ReactionFromJson(Serializer.moveToExtraDataFromRoot( - json, - topLevelFields, - )); + factory Reaction.fromJson(Map json) => _$ReactionFromJson( + Serializer.moveToExtraDataFromRoot( + json, + topLevelFields, + ), + ); /// 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 Map toJson() => Serializer.moveFromExtraDataToRoot( - _$ReactionToJson(this), - ); + _$ReactionToJson(this), + ); /// 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, - extraData: extraData ?? this.extraData, - ); + }) => Reaction( + messageId: messageId ?? this.messageId, + 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, + ); /// Returns a new [Reaction] that is a combination of this reaction and the /// 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, - extraData: other.extraData, - ); + messageId: other.messageId, + 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, + ]; + + @override + ComparableField? getComparableField(String sortKey) { + final value = switch (sortKey) { + ReactionSortKey.createdAt => createdAt, + _ => null, + }; + + return ComparableField.fromValue(value); + } +} + +/// Extension type representing sortable fields for [Reaction]. +/// +/// This type provides type-safe keys that can be used for sorting reactions +/// in queries. Each constant represents a field that can be sorted on. +extension type const ReactionSortKey(String key) implements String { + /// Sort reactions by their creation date. + /// + /// This is the default sort field (in ascending order). + static const createdAt = ReactionSortKey('created_at'); } 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..56436be596 100644 --- a/packages/stream_chat/lib/src/core/models/reaction.g.dart +++ b/packages/stream_chat/lib/src/core/models/reaction.g.dart @@ -7,22 +7,20 @@ 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, - extraData: json['extra_data'] as Map? ?? const {}, - ); + messageId: json['message_id'] 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() ?? 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, - 'extra_data': instance.extraData, - }; + '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..9be6a5150b 100644 --- a/packages/stream_chat/lib/src/core/models/reaction_group.dart +++ b/packages/stream_chat/lib/src/core/models/reaction_group.dart @@ -12,12 +12,11 @@ class ReactionGroup extends Equatable { this.sumScores = 0, DateTime? firstReactionAt, DateTime? lastReactionAt, - }) : firstReactionAt = firstReactionAt ?? DateTime.timestamp(), - lastReactionAt = lastReactionAt ?? DateTime.timestamp(); + }) : firstReactionAt = firstReactionAt ?? DateTime.timestamp(), + lastReactionAt = lastReactionAt ?? DateTime.timestamp(); /// Create a new instance from a json - factory ReactionGroup.fromJson(Map json) => - _$ReactionGroupFromJson(json); + factory ReactionGroup.fromJson(Map json) => _$ReactionGroupFromJson(json); /// The number of users that reacted with this reaction. final int count; @@ -51,9 +50,32 @@ class ReactionGroup extends Equatable { @override List get props => [ - count, - sumScores, - firstReactionAt, - lastReactionAt, - ]; + count, + sumScores, + firstReactionAt, + 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/models/reaction_group.g.dart b/packages/stream_chat/lib/src/core/models/reaction_group.g.dart index 65e1ceedd7..9c686e7f0a 100644 --- a/packages/stream_chat/lib/src/core/models/reaction_group.g.dart +++ b/packages/stream_chat/lib/src/core/models/reaction_group.g.dart @@ -6,22 +6,16 @@ part of 'reaction_group.dart'; // JsonSerializableGenerator // ************************************************************************** -ReactionGroup _$ReactionGroupFromJson(Map json) => - ReactionGroup( - count: (json['count'] as num?)?.toInt() ?? 0, - sumScores: (json['sum_scores'] as num?)?.toInt() ?? 0, - firstReactionAt: json['first_reaction_at'] == null - ? null - : DateTime.parse(json['first_reaction_at'] as String), - lastReactionAt: json['last_reaction_at'] == null - ? null - : DateTime.parse(json['last_reaction_at'] as String), - ); +ReactionGroup _$ReactionGroupFromJson(Map json) => ReactionGroup( + count: (json['count'] as num?)?.toInt() ?? 0, + sumScores: (json['sum_scores'] as num?)?.toInt() ?? 0, + firstReactionAt: json['first_reaction_at'] == null ? null : DateTime.parse(json['first_reaction_at'] as String), + lastReactionAt: json['last_reaction_at'] == null ? null : DateTime.parse(json['last_reaction_at'] as String), +); -Map _$ReactionGroupToJson(ReactionGroup instance) => - { - 'count': instance.count, - 'sum_scores': instance.sumScores, - 'first_reaction_at': instance.firstReactionAt.toIso8601String(), - 'last_reaction_at': instance.lastReactionAt.toIso8601String(), - }; +Map _$ReactionGroupToJson(ReactionGroup instance) => { + 'count': instance.count, + 'sum_scores': instance.sumScores, + 'first_reaction_at': instance.firstReactionAt.toIso8601String(), + 'last_reaction_at': instance.lastReactionAt.toIso8601String(), +}; diff --git a/packages/stream_chat/lib/src/core/models/read.dart b/packages/stream_chat/lib/src/core/models/read.dart index 165528946f..5952ad4b5c 100644 --- a/packages/stream_chat/lib/src/core/models/read.dart +++ b/packages/stream_chat/lib/src/core/models/read.dart @@ -58,8 +58,7 @@ class Read extends Equatable { user: user ?? this.user, unreadMessages: unreadMessages ?? this.unreadMessages, lastDeliveredAt: lastDeliveredAt ?? this.lastDeliveredAt, - lastDeliveredMessageId: - lastDeliveredMessageId ?? this.lastDeliveredMessageId, + lastDeliveredMessageId: lastDeliveredMessageId ?? this.lastDeliveredMessageId, ); } @@ -79,13 +78,13 @@ class Read extends Equatable { @override List get props => [ - lastRead, - lastReadMessageId, - user, - unreadMessages, - lastDeliveredAt, - lastDeliveredMessageId, - ]; + lastRead, + lastReadMessageId, + user, + unreadMessages, + lastDeliveredAt, + lastDeliveredMessageId, + ]; } /// Helper extension methods for [Iterable]<[Read]>. diff --git a/packages/stream_chat/lib/src/core/models/read.g.dart b/packages/stream_chat/lib/src/core/models/read.g.dart index 3e9d03fb0e..9f4358fcf5 100644 --- a/packages/stream_chat/lib/src/core/models/read.g.dart +++ b/packages/stream_chat/lib/src/core/models/read.g.dart @@ -7,21 +7,19 @@ part of 'read.dart'; // ************************************************************************** Read _$ReadFromJson(Map json) => Read( - lastRead: DateTime.parse(json['last_read'] as String), - user: User.fromJson(json['user'] as Map), - lastReadMessageId: json['last_read_message_id'] as String?, - unreadMessages: (json['unread_messages'] as num?)?.toInt(), - lastDeliveredAt: json['last_delivered_at'] == null - ? null - : DateTime.parse(json['last_delivered_at'] as String), - lastDeliveredMessageId: json['last_delivered_message_id'] as String?, - ); + lastRead: DateTime.parse(json['last_read'] as String), + user: User.fromJson(json['user'] as Map), + lastReadMessageId: json['last_read_message_id'] as String?, + unreadMessages: (json['unread_messages'] as num?)?.toInt(), + lastDeliveredAt: json['last_delivered_at'] == null ? null : DateTime.parse(json['last_delivered_at'] as String), + lastDeliveredMessageId: json['last_delivered_message_id'] as String?, +); Map _$ReadToJson(Read instance) => { - 'last_read': instance.lastRead.toIso8601String(), - 'user': instance.user.toJson(), - 'unread_messages': instance.unreadMessages, - 'last_read_message_id': instance.lastReadMessageId, - 'last_delivered_at': instance.lastDeliveredAt?.toIso8601String(), - 'last_delivered_message_id': instance.lastDeliveredMessageId, - }; + 'last_read': instance.lastRead.toIso8601String(), + 'user': instance.user.toJson(), + 'unread_messages': instance.unreadMessages, + 'last_read_message_id': instance.lastReadMessageId, + 'last_delivered_at': instance.lastDeliveredAt?.toIso8601String(), + 'last_delivered_message_id': instance.lastDeliveredMessageId, +}; diff --git a/packages/stream_chat/lib/src/core/models/thread.dart b/packages/stream_chat/lib/src/core/models/thread.dart index 3a26e15230..4074ca0158 100644 --- a/packages/stream_chat/lib/src/core/models/thread.dart +++ b/packages/stream_chat/lib/src/core/models/thread.dart @@ -44,12 +44,12 @@ class Thread extends Equatable implements ComparableFieldProvider { this.read = const [], this.draft, this.extraData = const {}, - }) : createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(); + }) : createdAt = createdAt ?? DateTime.now(), + updatedAt = updatedAt ?? DateTime.now(); /// Create a new instance from a json - factory Thread.fromJson(Map json) => _$ThreadFromJson( - Serializer.moveToExtraDataFromRoot(json, topLevelFields)); + factory Thread.fromJson(Map json) => + _$ThreadFromJson(Serializer.moveToExtraDataFromRoot(json, topLevelFields)); /// The active participant count in the thread. final int? activeParticipantCount; @@ -109,8 +109,7 @@ class Thread extends Equatable implements ComparableFieldProvider { final Map extraData; /// Serialize to json - Map toJson() => - Serializer.moveFromExtraDataToRoot(_$ThreadToJson(this)); + Map toJson() => Serializer.moveFromExtraDataToRoot(_$ThreadToJson(this)); /// Creates a copy of [Thread] with specified attributes overridden. Thread copyWith({ @@ -133,29 +132,27 @@ class Thread extends Equatable implements ComparableFieldProvider { List? read, Object? draft = _nullConst, Map? extraData, - }) => - Thread( - activeParticipantCount: - activeParticipantCount ?? this.activeParticipantCount, - channel: channel ?? this.channel, - channelCid: channelCid ?? this.channelCid, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - deletedAt: deletedAt ?? this.deletedAt, - createdByUserId: createdByUserId ?? this.createdByUserId, - createdBy: createdBy ?? this.createdBy, - title: title ?? this.title, - parentMessageId: parentMessageId ?? this.parentMessageId, - parentMessage: parentMessage ?? this.parentMessage, - replyCount: replyCount ?? this.replyCount, - participantCount: participantCount ?? this.participantCount, - threadParticipants: threadParticipants ?? this.threadParticipants, - lastMessageAt: lastMessageAt ?? this.lastMessageAt, - latestReplies: latestReplies ?? this.latestReplies, - read: read ?? this.read, - draft: draft == _nullConst ? this.draft : draft as Draft?, - extraData: extraData ?? this.extraData, - ); + }) => Thread( + activeParticipantCount: activeParticipantCount ?? this.activeParticipantCount, + channel: channel ?? this.channel, + channelCid: channelCid ?? this.channelCid, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + createdByUserId: createdByUserId ?? this.createdByUserId, + createdBy: createdBy ?? this.createdBy, + title: title ?? this.title, + parentMessageId: parentMessageId ?? this.parentMessageId, + parentMessage: parentMessage ?? this.parentMessage, + replyCount: replyCount ?? this.replyCount, + participantCount: participantCount ?? this.participantCount, + threadParticipants: threadParticipants ?? this.threadParticipants, + lastMessageAt: lastMessageAt ?? this.lastMessageAt, + latestReplies: latestReplies ?? this.latestReplies, + read: read ?? this.read, + draft: draft == _nullConst ? this.draft : draft as Draft?, + extraData: extraData ?? this.extraData, + ); /// Merge this thread with the [other] thread. Thread merge(Thread? other) { @@ -209,25 +206,25 @@ class Thread extends Equatable implements ComparableFieldProvider { @override List get props => [ - activeParticipantCount, - channelCid, - channel, - createdAt, - updatedAt, - deletedAt, - createdByUserId, - createdBy, - title, - parentMessageId, - parentMessage, - replyCount, - participantCount, - threadParticipants, - lastMessageAt, - latestReplies, - read, - draft, - ]; + activeParticipantCount, + channelCid, + channel, + createdAt, + updatedAt, + deletedAt, + createdByUserId, + createdBy, + title, + parentMessageId, + parentMessage, + replyCount, + participantCount, + threadParticipants, + lastMessageAt, + latestReplies, + read, + draft, + ]; @override ComparableField? getComparableField(String sortKey) { @@ -269,8 +266,7 @@ extension type const ThreadSortKey(String key) implements String { static const participantCount = ThreadSortKey('participant_count'); /// Sort threads by their active participant count. - static const activeParticipantCount = - ThreadSortKey('active_participant_count'); + static const activeParticipantCount = ThreadSortKey('active_participant_count'); /// Sort threads by their parent message id. static const parentMessageId = ThreadSortKey('parent_message_id'); diff --git a/packages/stream_chat/lib/src/core/models/thread.g.dart b/packages/stream_chat/lib/src/core/models/thread.g.dart index ed5b27b28b..7d5d46cd30 100644 --- a/packages/stream_chat/lib/src/core/models/thread.g.dart +++ b/packages/stream_chat/lib/src/core/models/thread.g.dart @@ -7,73 +7,53 @@ part of 'thread.dart'; // ************************************************************************** Thread _$ThreadFromJson(Map json) => Thread( - activeParticipantCount: - (json['active_participant_count'] as num?)?.toInt(), - channel: json['channel'] == null - ? null - : ChannelModel.fromJson(json['channel'] as Map), - channelCid: json['channel_cid'] as String, - parentMessageId: json['parent_message_id'] as String, - parentMessage: json['parent_message'] == null - ? null - : Message.fromJson(json['parent_message'] as Map), - createdByUserId: json['created_by_user_id'] as String, - createdBy: json['created_by'] == null - ? null - : User.fromJson(json['created_by'] as Map), - replyCount: (json['reply_count'] as num).toInt(), - participantCount: (json['participant_count'] as num).toInt(), - threadParticipants: (json['thread_participants'] as List?) - ?.map( - (e) => ThreadParticipant.fromJson(e as Map)) - .toList() ?? - const [], - lastMessageAt: json['last_message_at'] == null - ? null - : DateTime.parse(json['last_message_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), - deletedAt: json['deleted_at'] == null - ? null - : DateTime.parse(json['deleted_at'] as String), - title: json['title'] as String?, - latestReplies: (json['latest_replies'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList() ?? - const [], - read: (json['read'] as List?) - ?.map((e) => Read.fromJson(e as Map)) - .toList() ?? - const [], - draft: json['draft'] == null - ? null - : Draft.fromJson(json['draft'] as Map), - extraData: json['extra_data'] as Map? ?? const {}, - ); + activeParticipantCount: (json['active_participant_count'] as num?)?.toInt(), + channel: json['channel'] == null ? null : ChannelModel.fromJson(json['channel'] as Map), + channelCid: json['channel_cid'] as String, + parentMessageId: json['parent_message_id'] as String, + parentMessage: json['parent_message'] == null + ? null + : Message.fromJson(json['parent_message'] as Map), + createdByUserId: json['created_by_user_id'] as String, + createdBy: json['created_by'] == null ? null : User.fromJson(json['created_by'] as Map), + replyCount: (json['reply_count'] as num).toInt(), + participantCount: (json['participant_count'] as num).toInt(), + threadParticipants: + (json['thread_participants'] as List?) + ?.map((e) => ThreadParticipant.fromJson(e as Map)) + .toList() ?? + const [], + lastMessageAt: json['last_message_at'] == null ? null : DateTime.parse(json['last_message_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), + deletedAt: json['deleted_at'] == null ? null : DateTime.parse(json['deleted_at'] as String), + title: json['title'] as String?, + latestReplies: + (json['latest_replies'] as List?)?.map((e) => Message.fromJson(e as Map)).toList() ?? + const [], + read: (json['read'] as List?)?.map((e) => Read.fromJson(e as Map)).toList() ?? const [], + draft: json['draft'] == null ? null : Draft.fromJson(json['draft'] as Map), + extraData: json['extra_data'] as Map? ?? const {}, +); Map _$ThreadToJson(Thread instance) => { - 'active_participant_count': instance.activeParticipantCount, - 'channel_cid': instance.channelCid, - 'channel': instance.channel?.toJson(), - 'created_at': instance.createdAt.toIso8601String(), - 'updated_at': instance.updatedAt.toIso8601String(), - 'deleted_at': instance.deletedAt?.toIso8601String(), - 'created_by_user_id': instance.createdByUserId, - 'created_by': instance.createdBy?.toJson(), - 'title': instance.title, - 'parent_message_id': instance.parentMessageId, - 'parent_message': instance.parentMessage?.toJson(), - 'reply_count': instance.replyCount, - 'participant_count': instance.participantCount, - 'thread_participants': - instance.threadParticipants.map((e) => e.toJson()).toList(), - 'last_message_at': instance.lastMessageAt?.toIso8601String(), - 'latest_replies': instance.latestReplies.map((e) => e.toJson()).toList(), - 'read': instance.read?.map((e) => e.toJson()).toList(), - 'draft': instance.draft?.toJson(), - 'extra_data': instance.extraData, - }; + 'active_participant_count': instance.activeParticipantCount, + 'channel_cid': instance.channelCid, + 'channel': instance.channel?.toJson(), + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt?.toIso8601String(), + 'created_by_user_id': instance.createdByUserId, + 'created_by': instance.createdBy?.toJson(), + 'title': instance.title, + 'parent_message_id': instance.parentMessageId, + 'parent_message': instance.parentMessage?.toJson(), + 'reply_count': instance.replyCount, + 'participant_count': instance.participantCount, + 'thread_participants': instance.threadParticipants.map((e) => e.toJson()).toList(), + 'last_message_at': instance.lastMessageAt?.toIso8601String(), + 'latest_replies': instance.latestReplies.map((e) => e.toJson()).toList(), + 'read': instance.read?.map((e) => e.toJson()).toList(), + 'draft': instance.draft?.toJson(), + 'extra_data': instance.extraData, +}; diff --git a/packages/stream_chat/lib/src/core/models/thread_participant.dart b/packages/stream_chat/lib/src/core/models/thread_participant.dart index fece374619..96d5a7276c 100644 --- a/packages/stream_chat/lib/src/core/models/thread_participant.dart +++ b/packages/stream_chat/lib/src/core/models/thread_participant.dart @@ -22,8 +22,7 @@ class ThreadParticipant extends Equatable { }); /// Create a new instance from a json - factory ThreadParticipant.fromJson(Map json) => - _$ThreadParticipantFromJson(json); + factory ThreadParticipant.fromJson(Map json) => _$ThreadParticipantFromJson(json); /// The channel cid this thread participant belongs to. final String channelCid; @@ -63,27 +62,26 @@ class ThreadParticipant extends Equatable { String? threadId, String? userId, User? user, - }) => - ThreadParticipant( - channelCid: channelCid ?? this.channelCid, - createdAt: createdAt ?? this.createdAt, - lastReadAt: lastReadAt ?? this.lastReadAt, - lastThreadMessageAt: lastThreadMessageAt ?? this.lastThreadMessageAt, - leftThreadAt: leftThreadAt ?? this.leftThreadAt, - threadId: threadId ?? this.threadId, - userId: userId ?? this.userId, - user: user ?? this.user, - ); + }) => ThreadParticipant( + channelCid: channelCid ?? this.channelCid, + createdAt: createdAt ?? this.createdAt, + lastReadAt: lastReadAt ?? this.lastReadAt, + lastThreadMessageAt: lastThreadMessageAt ?? this.lastThreadMessageAt, + leftThreadAt: leftThreadAt ?? this.leftThreadAt, + threadId: threadId ?? this.threadId, + userId: userId ?? this.userId, + user: user ?? this.user, + ); @override List get props => [ - channelCid, - createdAt, - lastReadAt, - lastThreadMessageAt, - leftThreadAt, - threadId, - userId, - user, - ]; + channelCid, + createdAt, + lastReadAt, + lastThreadMessageAt, + leftThreadAt, + threadId, + userId, + user, + ]; } diff --git a/packages/stream_chat/lib/src/core/models/thread_participant.g.dart b/packages/stream_chat/lib/src/core/models/thread_participant.g.dart index 4a410c404b..ec1d860052 100644 --- a/packages/stream_chat/lib/src/core/models/thread_participant.g.dart +++ b/packages/stream_chat/lib/src/core/models/thread_participant.g.dart @@ -6,32 +6,26 @@ part of 'thread_participant.dart'; // JsonSerializableGenerator // ************************************************************************** -ThreadParticipant _$ThreadParticipantFromJson(Map json) => - ThreadParticipant( - channelCid: json['channel_cid'] as String, - createdAt: DateTime.parse(json['created_at'] as String), - lastReadAt: DateTime.parse(json['last_read_at'] as String), - lastThreadMessageAt: json['last_thread_message_at'] == null - ? null - : DateTime.parse(json['last_thread_message_at'] as String), - leftThreadAt: json['left_thread_at'] == null - ? null - : DateTime.parse(json['left_thread_at'] as String), - threadId: json['thread_id'] as String?, - userId: json['user_id'] as String?, - user: json['user'] == null - ? null - : User.fromJson(json['user'] as Map), - ); +ThreadParticipant _$ThreadParticipantFromJson(Map json) => ThreadParticipant( + channelCid: json['channel_cid'] as String, + createdAt: DateTime.parse(json['created_at'] as String), + lastReadAt: DateTime.parse(json['last_read_at'] as String), + lastThreadMessageAt: json['last_thread_message_at'] == null + ? null + : DateTime.parse(json['last_thread_message_at'] as String), + leftThreadAt: json['left_thread_at'] == null ? null : DateTime.parse(json['left_thread_at'] as String), + threadId: json['thread_id'] as String?, + userId: json['user_id'] as String?, + user: json['user'] == null ? null : User.fromJson(json['user'] as Map), +); -Map _$ThreadParticipantToJson(ThreadParticipant instance) => - { - 'channel_cid': instance.channelCid, - 'created_at': instance.createdAt.toIso8601String(), - 'last_read_at': instance.lastReadAt.toIso8601String(), - 'last_thread_message_at': instance.lastThreadMessageAt?.toIso8601String(), - 'left_thread_at': instance.leftThreadAt?.toIso8601String(), - 'thread_id': instance.threadId, - 'user_id': instance.userId, - 'user': instance.user?.toJson(), - }; +Map _$ThreadParticipantToJson(ThreadParticipant instance) => { + 'channel_cid': instance.channelCid, + 'created_at': instance.createdAt.toIso8601String(), + 'last_read_at': instance.lastReadAt.toIso8601String(), + 'last_thread_message_at': instance.lastThreadMessageAt?.toIso8601String(), + 'left_thread_at': instance.leftThreadAt?.toIso8601String(), + 'thread_id': instance.threadId, + 'user_id': instance.userId, + 'user': instance.user?.toJson(), +}; diff --git a/packages/stream_chat/lib/src/core/models/unread_counts.dart b/packages/stream_chat/lib/src/core/models/unread_counts.dart index d3e265fa76..91a0b7dfe5 100644 --- a/packages/stream_chat/lib/src/core/models/unread_counts.dart +++ b/packages/stream_chat/lib/src/core/models/unread_counts.dart @@ -15,8 +15,7 @@ class UnreadCountsChannel { }); /// Create a new instance from a json. - factory UnreadCountsChannel.fromJson(Map json) => - _$UnreadCountsChannelFromJson(json); + factory UnreadCountsChannel.fromJson(Map json) => _$UnreadCountsChannelFromJson(json); /// The unique identifier of the channel (format: "type:id"). final String channelId; @@ -45,8 +44,7 @@ class UnreadCountsThread { }); /// Create a new instance from a json. - factory UnreadCountsThread.fromJson(Map json) => - _$UnreadCountsThreadFromJson(json); + factory UnreadCountsThread.fromJson(Map json) => _$UnreadCountsThreadFromJson(json); /// Number of unread messages in this thread. final int unreadCount; @@ -78,8 +76,7 @@ class UnreadCountsChannelType { }); /// Create a new instance from a json. - factory UnreadCountsChannelType.fromJson(Map json) => - _$UnreadCountsChannelTypeFromJson(json); + factory UnreadCountsChannelType.fromJson(Map json) => _$UnreadCountsChannelTypeFromJson(json); /// The type of channel (e.g., "messaging", "livestream", "team"). final String channelType; diff --git a/packages/stream_chat/lib/src/core/models/unread_counts.g.dart b/packages/stream_chat/lib/src/core/models/unread_counts.g.dart index bd149b5a75..95ff1053b9 100644 --- a/packages/stream_chat/lib/src/core/models/unread_counts.g.dart +++ b/packages/stream_chat/lib/src/core/models/unread_counts.g.dart @@ -6,49 +6,40 @@ part of 'unread_counts.dart'; // JsonSerializableGenerator // ************************************************************************** -UnreadCountsChannel _$UnreadCountsChannelFromJson(Map json) => - UnreadCountsChannel( - channelId: json['channel_id'] as String, - unreadCount: (json['unread_count'] as num).toInt(), - lastRead: DateTime.parse(json['last_read'] as String), - ); - -Map _$UnreadCountsChannelToJson( - UnreadCountsChannel instance) => - { - 'channel_id': instance.channelId, - 'unread_count': instance.unreadCount, - 'last_read': instance.lastRead.toIso8601String(), - }; - -UnreadCountsThread _$UnreadCountsThreadFromJson(Map json) => - UnreadCountsThread( - unreadCount: (json['unread_count'] as num).toInt(), - lastRead: DateTime.parse(json['last_read'] as String), - lastReadMessageId: json['last_read_message_id'] as String, - parentMessageId: json['parent_message_id'] as String, - ); - -Map _$UnreadCountsThreadToJson(UnreadCountsThread instance) => - { - 'unread_count': instance.unreadCount, - 'last_read': instance.lastRead.toIso8601String(), - 'last_read_message_id': instance.lastReadMessageId, - 'parent_message_id': instance.parentMessageId, - }; - -UnreadCountsChannelType _$UnreadCountsChannelTypeFromJson( - Map json) => - UnreadCountsChannelType( - channelType: json['channel_type'] as String, - channelCount: (json['channel_count'] as num).toInt(), - unreadCount: (json['unread_count'] as num).toInt(), - ); - -Map _$UnreadCountsChannelTypeToJson( - UnreadCountsChannelType instance) => - { - 'channel_type': instance.channelType, - 'channel_count': instance.channelCount, - 'unread_count': instance.unreadCount, - }; +UnreadCountsChannel _$UnreadCountsChannelFromJson(Map json) => UnreadCountsChannel( + channelId: json['channel_id'] as String, + unreadCount: (json['unread_count'] as num).toInt(), + lastRead: DateTime.parse(json['last_read'] as String), +); + +Map _$UnreadCountsChannelToJson(UnreadCountsChannel instance) => { + 'channel_id': instance.channelId, + 'unread_count': instance.unreadCount, + 'last_read': instance.lastRead.toIso8601String(), +}; + +UnreadCountsThread _$UnreadCountsThreadFromJson(Map json) => UnreadCountsThread( + unreadCount: (json['unread_count'] as num).toInt(), + lastRead: DateTime.parse(json['last_read'] as String), + lastReadMessageId: json['last_read_message_id'] as String, + parentMessageId: json['parent_message_id'] as String, +); + +Map _$UnreadCountsThreadToJson(UnreadCountsThread instance) => { + 'unread_count': instance.unreadCount, + 'last_read': instance.lastRead.toIso8601String(), + 'last_read_message_id': instance.lastReadMessageId, + 'parent_message_id': instance.parentMessageId, +}; + +UnreadCountsChannelType _$UnreadCountsChannelTypeFromJson(Map json) => UnreadCountsChannelType( + channelType: json['channel_type'] as String, + channelCount: (json['channel_count'] as num).toInt(), + unreadCount: (json['unread_count'] as num).toInt(), +); + +Map _$UnreadCountsChannelTypeToJson(UnreadCountsChannelType instance) => { + 'channel_type': instance.channelType, + 'channel_count': instance.channelCount, + 'unread_count': instance.unreadCount, +}; diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index 3c45ee5a86..9d6e6e3207 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -49,13 +49,12 @@ class User extends Equatable implements ComparableFieldProvider { this.teamsRole, this.avgResponseTime, Map extraData = const {}, - }) : - // For backwards compatibility, set 'name', 'image' in [extraData]. - extraData = { - ...extraData, - if (name != null) 'name': name, - if (image != null) 'image': image, - }; + }) : // For backwards compatibility, set 'name', 'image' in [extraData]. + extraData = { + ...extraData, + if (name != null) 'name': name, + if (image != null) 'image': image, + }; /// Create a new instance from json. factory User.fromJson(Map json) => @@ -138,7 +137,7 @@ class User extends Equatable implements ComparableFieldProvider { /// The roles for the user in the teams. /// /// eg: `{'teamId': 'role', 'teamId2': 'role2'}` - final Map< /*Team*/ String, /*Role*/ String>? teamsRole; + final Map? teamsRole; /// The average response time of the user in seconds. final int? avgResponseTime; @@ -147,13 +146,12 @@ class User extends Equatable implements ComparableFieldProvider { final Map extraData; /// List of users to list of userIds. - static List? toIds(List? users) => - users?.map((u) => u.id).toList(); + static List? toIds(List? users) => users?.map((u) => u.id).toList(); /// Serialize to json. Map toJson() => Serializer.moveFromExtraDataToRoot( - _$UserToJson(this), - ); + _$UserToJson(this), + ); /// Creates a copy of [User] with specified attributes overridden. User copyWith({ @@ -173,44 +171,44 @@ class User extends Equatable implements ComparableFieldProvider { bool? invisible, Map? teamsRole, int? avgResponseTime, - }) => - User( - id: id ?? this.id, - role: role ?? this.role, - name: name ?? - extraData?['name'] as String? ?? - // Using extraData value in order to not use id as name. - this.extraData['name'] as String?, - image: image ?? extraData?['image'] as String? ?? this.image, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - lastActive: lastActive ?? this.lastActive, - online: online ?? this.online, - extraData: extraData ?? this.extraData, - banned: banned ?? this.banned, - banExpires: banExpires ?? this.banExpires, - teams: teams ?? this.teams, - language: language ?? this.language, - invisible: invisible ?? this.invisible, - teamsRole: teamsRole ?? this.teamsRole, - avgResponseTime: avgResponseTime ?? this.avgResponseTime, - ); + }) => User( + id: id ?? this.id, + role: role ?? this.role, + name: + name ?? + extraData?['name'] as String? ?? + // Using extraData value in order to not use id as name. + this.extraData['name'] as String?, + image: image ?? extraData?['image'] as String? ?? this.image, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + lastActive: lastActive ?? this.lastActive, + online: online ?? this.online, + extraData: extraData ?? this.extraData, + banned: banned ?? this.banned, + banExpires: banExpires ?? this.banExpires, + teams: teams ?? this.teams, + language: language ?? this.language, + invisible: invisible ?? this.invisible, + teamsRole: teamsRole ?? this.teamsRole, + avgResponseTime: avgResponseTime ?? this.avgResponseTime, + ); @override List get props => [ - id, - role, - lastActive, - online, - extraData, - banned, - banExpires, - teams, - language, - invisible, - teamsRole, - avgResponseTime, - ]; + id, + role, + lastActive, + online, + extraData, + banned, + banExpires, + teams, + language, + invisible, + teamsRole, + avgResponseTime, + ]; @override ComparableField? getComparableField(String sortKey) { diff --git a/packages/stream_chat/lib/src/core/models/user.g.dart b/packages/stream_chat/lib/src/core/models/user.g.dart index f3a8e5868d..03cb7553d0 100644 --- a/packages/stream_chat/lib/src/core/models/user.g.dart +++ b/packages/stream_chat/lib/src/core/models/user.g.dart @@ -7,52 +7,37 @@ part of 'user.dart'; // ************************************************************************** User _$UserFromJson(Map json) => User( - id: json['id'] as String, - role: json['role'] 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), - lastActive: json['last_active'] == null - ? null - : DateTime.parse(json['last_active'] as String), - online: json['online'] as bool? ?? false, - banned: json['banned'] as bool? ?? false, - banExpires: json['ban_expires'] == null - ? null - : DateTime.parse(json['ban_expires'] as String), - teams: - (json['teams'] as List?)?.map((e) => e as String).toList() ?? - const [], - language: json['language'] as String?, - invisible: json['invisible'] as bool?, - teamsRole: (json['teams_role'] as Map?)?.map( - (k, e) => MapEntry(k, e as String), - ), - avgResponseTime: (json['avg_response_time'] as num?)?.toInt(), - extraData: json['extra_data'] as Map? ?? const {}, - ); + id: json['id'] as String, + role: json['role'] 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), + lastActive: json['last_active'] == null ? null : DateTime.parse(json['last_active'] as String), + online: json['online'] as bool? ?? false, + banned: json['banned'] as bool? ?? false, + banExpires: json['ban_expires'] == null ? null : DateTime.parse(json['ban_expires'] as String), + teams: (json['teams'] as List?)?.map((e) => e as String).toList() ?? const [], + language: json['language'] as String?, + invisible: json['invisible'] as bool?, + teamsRole: (json['teams_role'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ), + avgResponseTime: (json['avg_response_time'] as num?)?.toInt(), + extraData: json['extra_data'] as Map? ?? const {}, +); Map _$UserToJson(User instance) => { - 'id': instance.id, - if (instance.role case final value?) 'role': value, - 'teams': instance.teams, - if (instance.createdAt?.toIso8601String() case final value?) - 'created_at': value, - if (instance.updatedAt?.toIso8601String() case final value?) - 'updated_at': value, - if (instance.lastActive?.toIso8601String() case final value?) - 'last_active': value, - 'online': instance.online, - 'banned': instance.banned, - if (instance.banExpires?.toIso8601String() case final value?) - 'ban_expires': value, - if (instance.language case final value?) 'language': value, - if (instance.invisible case final value?) 'invisible': value, - if (instance.teamsRole case final value?) 'teams_role': value, - if (instance.avgResponseTime case final value?) - 'avg_response_time': value, - 'extra_data': instance.extraData, - }; + 'id': instance.id, + if (instance.role case final value?) 'role': value, + 'teams': instance.teams, + if (instance.createdAt?.toIso8601String() case final value?) 'created_at': value, + if (instance.updatedAt?.toIso8601String() case final value?) 'updated_at': value, + if (instance.lastActive?.toIso8601String() case final value?) 'last_active': value, + 'online': instance.online, + 'banned': instance.banned, + if (instance.banExpires?.toIso8601String() case final value?) 'ban_expires': value, + if (instance.language case final value?) 'language': value, + if (instance.invisible case final value?) 'invisible': value, + if (instance.teamsRole case final value?) 'teams_role': value, + if (instance.avgResponseTime case final value?) 'avg_response_time': value, + 'extra_data': instance.extraData, +}; diff --git a/packages/stream_chat/lib/src/core/models/user_block.dart b/packages/stream_chat/lib/src/core/models/user_block.dart index 247de32bb8..77cbfe04f4 100644 --- a/packages/stream_chat/lib/src/core/models/user_block.dart +++ b/packages/stream_chat/lib/src/core/models/user_block.dart @@ -17,8 +17,7 @@ class UserBlock extends Equatable { }); /// Create a new instance from a json - factory UserBlock.fromJson(Map json) => - _$UserBlockFromJson(json); + factory UserBlock.fromJson(Map json) => _$UserBlockFromJson(json); /// User that blocked the [blockedUser]. final User user; @@ -45,21 +44,20 @@ class UserBlock extends Equatable { String? userId, String? blockedUserId, DateTime? createdAt, - }) => - UserBlock( - user: user ?? this.user, - blockedUser: blockedUser ?? this.blockedUser, - userId: userId ?? this.userId, - blockedUserId: blockedUserId ?? this.blockedUserId, - createdAt: createdAt ?? this.createdAt, - ); + }) => UserBlock( + user: user ?? this.user, + blockedUser: blockedUser ?? this.blockedUser, + userId: userId ?? this.userId, + blockedUserId: blockedUserId ?? this.blockedUserId, + createdAt: createdAt ?? this.createdAt, + ); @override List get props => [ - user, - blockedUser, - userId, - blockedUserId, - createdAt, - ]; + user, + blockedUser, + userId, + blockedUserId, + createdAt, + ]; } diff --git a/packages/stream_chat/lib/src/core/models/user_block.g.dart b/packages/stream_chat/lib/src/core/models/user_block.g.dart index 00e138211b..6e36cb806f 100644 --- a/packages/stream_chat/lib/src/core/models/user_block.g.dart +++ b/packages/stream_chat/lib/src/core/models/user_block.g.dart @@ -7,21 +7,17 @@ part of 'user_block.dart'; // ************************************************************************** UserBlock _$UserBlockFromJson(Map json) => UserBlock( - user: User.fromJson(json['user'] as Map), - blockedUser: json['blocked_user'] == null - ? null - : User.fromJson(json['blocked_user'] as Map), - userId: json['user_id'] as String?, - blockedUserId: json['blocked_user_id'] as String?, - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), - ); + user: User.fromJson(json['user'] as Map), + blockedUser: json['blocked_user'] == null ? null : User.fromJson(json['blocked_user'] as Map), + userId: json['user_id'] as String?, + blockedUserId: json['blocked_user_id'] as String?, + createdAt: json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), +); Map _$UserBlockToJson(UserBlock instance) => { - 'user': instance.user.toJson(), - 'blocked_user': instance.blockedUser?.toJson(), - 'user_id': instance.userId, - 'blocked_user_id': instance.blockedUserId, - 'created_at': instance.createdAt?.toIso8601String(), - }; + 'user': instance.user.toJson(), + 'blocked_user': instance.blockedUser?.toJson(), + 'user_id': instance.userId, + 'blocked_user_id': instance.blockedUserId, + 'created_at': instance.createdAt?.toIso8601String(), +}; 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/core/util/extension.dart b/packages/stream_chat/lib/src/core/util/extension.dart index 9d409dacc3..51bfe84639 100644 --- a/packages/stream_chat/lib/src/core/util/extension.dart +++ b/packages/stream_chat/lib/src/core/util/extension.dart @@ -14,8 +14,7 @@ extension IterableX on Iterable { extension MapX on Map { /// Returns a new map with null keys or values removed Map get nullProtected { - final nullProtected = {...this} - ..removeWhere((key, value) => key == null || value == null); + final nullProtected = {...this}..removeWhere((key, value) => key == null || value == null); return nullProtected.cast(); } } @@ -66,7 +65,7 @@ extension CompleterX on Completer { /// Extension providing merge functionality for any iterable. extension IterableMergeExtension on Iterable { - /// Merges this iterable with another iterable of the same type. + /// Merges this iterable with another iterable of the **same type**. /// /// This method allows merging two iterables by identifying items with the /// same key and using an update function to combine them. Items that exist @@ -93,12 +92,83 @@ extension IterableMergeExtension on Iterable { Iterable? other, { required K Function(T item) key, required T Function(T original, T updated) update, + }) { + return mergeFrom( + other, + key: key, + value: (item) => item, + update: update, + ); + } + + /// Merges this iterable with another iterable of **a different type**. + /// + /// This method generalizes [merge] to support merging items of type [V] + /// (for example, DTOs or partial updates) into an existing collection of + /// items of type [T]. + /// + /// The [value] function converts each [V] element into a corresponding [T] + /// instance (or returns `null` to skip the item). + /// + /// The [key] extractor identifies how to match existing and new elements. + /// When a matching key already exists, the [update] function determines how + /// to combine the original and new values. If no match exists, the new + /// element is added. + /// + /// Items that appear only in one iterable are preserved as-is. + /// + /// Example (merging DTOs into models): + /// ```dart + /// final users = [User(id: '1', name: 'John'), User(id: '2', name: 'Alice')]; + /// + /// final dtos = [ + /// UserDTO(id: '1', name: 'John Doe'), + /// UserDTO(id: '3', name: 'Bob'), + /// ]; + /// + /// final merged = users.mergeFrom( + /// dtos, + /// key: (user) => user.id, + /// value: (dto) => dto.toUser(), + /// update: (original, updated) => original.copyWith(name: updated.name), + /// ); + /// + /// // Result: + /// // [ + /// // User(id: '1', name: 'John Doe'), + /// // User(id: '2', name: 'Alice'), + /// // User(id: '3', name: 'Bob'), + /// // ] + /// ``` + /// + /// Example (skipping null conversions): + /// ```dart + /// final list = [Item(id: 1, name: 'A')]; + /// final updates = [ItemUpdate(id: 1, name: null)]; + /// + /// final merged = list.mergeFrom( + /// updates, + /// key: (item) => item.id, + /// value: (update) => update.toItemOrNull(), + /// update: (original, updated) => updated, + /// ); + /// + /// // The null return from `toItemOrNull()` causes the item to be skipped. + /// ``` + Iterable mergeFrom( + Iterable? other, { + required K Function(T item) key, + required T? Function(V item) value, + required T Function(T original, T updated) update, }) { if (other == null) return this; final itemMap = {for (final item in this) key(item): item}; - for (final item in other) { + for (final otherItem in other) { + final item = value.call(otherItem); + if (item == null) continue; + itemMap.update( key(item), (original) => update(original, item), diff --git a/packages/stream_chat/lib/src/core/util/message_rules.dart b/packages/stream_chat/lib/src/core/util/message_rules.dart index 6db2cfbf31..d3ac674eb0 100644 --- a/packages/stream_chat/lib/src/core/util/message_rules.dart +++ b/packages/stream_chat/lib/src/core/util/message_rules.dart @@ -19,11 +19,21 @@ class MessageRules { /// * A poll static bool canUpload(Message message) { final hasText = message.text?.trim().isNotEmpty == true; + if (hasText) return true; + final hasAttachments = message.attachments.isNotEmpty; + if (hasAttachments) return true; + final hasQuotedMessage = message.quotedMessageId != null; + if (hasQuotedMessage) return true; + + final hasSharedLocation = message.sharedLocation != null; + if (hasSharedLocation) return true; + final hasPoll = message.pollId != null; + if (hasPoll) return true; - return hasText || hasAttachments || hasQuotedMessage || hasPoll; + return false; } /// Whether the [message] can update the channel's last message timestamp. diff --git a/packages/stream_chat/lib/src/core/util/serializer.dart b/packages/stream_chat/lib/src/core/util/serializer.dart index 9c1cb71f67..5cd992bc11 100644 --- a/packages/stream_chat/lib/src/core/util/serializer.dart +++ b/packages/stream_chat/lib/src/core/util/serializer.dart @@ -11,12 +11,10 @@ class Serializer { ..removeWhere( (key, value) => topLevelFields.contains(key), ); - final rootFields = jsonClone - ..removeWhere((key, value) => extraDataMap.keys.contains(key)); - return rootFields - ..addAll({ - 'extra_data': extraDataMap, - }); + final rootFields = jsonClone..removeWhere((key, value) => extraDataMap.keys.contains(key)); + return rootFields..addAll({ + 'extra_data': extraDataMap, + }); } /// Takes values in `extra_data` key and puts them on the root level of diff --git a/packages/stream_chat/lib/src/core/util/utils.dart b/packages/stream_chat/lib/src/core/util/utils.dart index c220499182..23c8bc8923 100644 --- a/packages/stream_chat/lib/src/core/util/utils.dart +++ b/packages/stream_chat/lib/src/core/util/utils.dart @@ -3,8 +3,7 @@ import 'dart:math' as math; // This alphabet uses `A-Za-z0-9_-` symbols. The genetic algorithm helped // optimize the gzip compression for this alphabet. -const _alphabet = - 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW'; +const _alphabet = 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW'; /// Generates a random String id /// Adopted from: https://github.com/ai/nanoid/blob/main/non-secure/index.js 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 80fe638758..4daf3f87f6 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'; @@ -86,6 +87,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, { @@ -117,13 +124,15 @@ abstract class ChatPersistenceClient { ); } - /// Get all the stored [ChannelState]s + /// Returns all stored channel states. /// - /// Optionally, pass [filter], [sort], [paginationParams] - /// for filtering out states. + /// Optionally provide [filter] to filter channels, [channelStateSort] to + /// sort results, [messageLimit] to limit messages per channel, and + /// [paginationParams] to paginate results. Future> getChannelStates({ Filter? filter, SortOrder? channelStateSort, + int? messageLimit, PaginationParams? paginationParams, }); @@ -138,12 +147,10 @@ abstract class ChatPersistenceClient { }); /// Remove a message by [messageId] - Future deleteMessageById(String messageId) => - deleteMessageByIds([messageId]); + Future deleteMessageById(String messageId) => deleteMessageByIds([messageId]); /// Remove a pinned message by [messageId] - Future deletePinnedMessageById(String messageId) => - deletePinnedMessageByIds([messageId]); + Future deletePinnedMessageById(String messageId) => deletePinnedMessageByIds([messageId]); /// Remove a message by [messageIds] Future deleteMessageByIds(List messageIds); @@ -155,8 +162,7 @@ abstract class ChatPersistenceClient { Future deleteMessageByCid(String cid) => deleteMessageByCids([cid]); /// Remove a pinned message by channel [cid] - Future deletePinnedMessageByCid(String cid) async => - deletePinnedMessageByCids([cid]); + Future deletePinnedMessageByCid(String cid) async => deletePinnedMessageByCids([cid]); /// Remove a message by message [cids] Future deleteMessageByCids(List cids); @@ -164,6 +170,24 @@ abstract class ChatPersistenceClient { /// Remove a pinned message by message [cids] Future deletePinnedMessageByCids(List cids); + /// Deletes all stored messages sent by a user with the given [userId]. + /// + /// If [hardDelete] is `true`, permanently removes messages from storage. + /// Otherwise, soft-deletes them by updating their type, deletion timestamp, + /// and state. + /// + /// If [cid] is provided, only deletes messages in that channel. Otherwise, + /// deletes messages across all channels. + /// + /// The [deletedAt] timestamp is used for soft deletes. Defaults to the + /// current time if not provided. + Future deleteMessagesFromUser({ + String? cid, + required String userId, + bool hardDelete = false, + DateTime? deletedAt, + }); + /// Remove a channel by [channelId] Future deleteChannels(List cids); @@ -171,18 +195,22 @@ 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) => - bulkUpdateMessages({cid: messages}); + Future updateMessages(String cid, List messages) => bulkUpdateMessages({cid: messages}); /// Bulk updates the message data of multiple channels. Future bulkUpdateMessages(Map?> messages); /// Updates the pinned message data of a particular channel [cid] with /// the new [messages] data - Future updatePinnedMessages(String cid, List messages) => - bulkUpdatePinnedMessages({cid: messages}); + Future updatePinnedMessages(String cid, List messages) => bulkUpdatePinnedMessages({cid: messages}); /// Bulk updates the message data of multiple channels. Future bulkUpdatePinnedMessages(Map?> messages); @@ -202,16 +230,14 @@ abstract class ChatPersistenceClient { /// Updates all the members of a particular channle [cid] /// with the new [members] data - Future updateMembers(String cid, List members) => - bulkUpdateMembers({cid: members}); + Future updateMembers(String cid, List members) => bulkUpdateMembers({cid: members}); /// Bulk updates the members data of multiple channels. Future bulkUpdateMembers(Map?> members); /// Updates the read data of a particular channel [cid] with /// the new [reads] data - Future updateReads(String cid, List reads) => - bulkUpdateReads({cid: reads}); + Future updateReads(String cid, List reads) => bulkUpdateReads({cid: reads}); /// Bulk updates the read data of multiple channels. Future bulkUpdateReads(Map?> reads); @@ -231,6 +257,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); @@ -278,8 +307,7 @@ abstract class ChatPersistenceClient { } /// Update the channel state data using [channelState] - Future updateChannelState(ChannelState channelState) => - updateChannelStates([channelState]); + Future updateChannelState(ChannelState channelState) => updateChannelStates([channelState]); /// Update list of channel states Future updateChannelStates(List channelStates) async { @@ -306,6 +334,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. @@ -317,10 +347,10 @@ abstract class ChatPersistenceClient { final members = state.members; final messages = switch (CurrentPlatform.isWeb) { true => state.messages?.where( - (it) => !it.attachments.any( - (it) => it.uploadState != const UploadState.success(), - ), + (it) => !it.attachments.any( + (it) => it.uploadState != const UploadState.success(), ), + ), _ => state.messages, }; @@ -341,32 +371,45 @@ abstract class ChatPersistenceClient { reactions.addAll(messages?.expand(_expandReactions) ?? []); pinnedReactions.addAll(pinnedMessages?.expand(_expandReactions) ?? []); - polls.addAll([ - ...?messages?.map((it) => it.poll), - ...?pinnedMessages?.map((it) => it.poll), - ].withNullifyer); + polls.addAll( + [ + ...?messages?.map((it) => it.poll), + ...?pinnedMessages?.map((it) => it.poll), + ].withNullifyer, + ); pollVotesToDelete.addAll(polls.map((it) => it.id)); pollVotes.addAll(polls.expand(_expandPollVotes)); - drafts.addAll([ - state.draft, - ...?messages?.map((it) => it.draft), - ...?pinnedMessages?.map((it) => it.draft), - ].nonNulls); - - users.addAll([ - channel.createdBy, - ...?messages?.map((it) => it.user), - ...?pinnedMessages?.map((it) => it.user), - ...?reads?.map((it) => it.user), - ...?members?.map((it) => it.user), - ...reactions.map((it) => it.user), - ...pinnedReactions.map((it) => it.user), - ...polls.map((it) => it.createdBy), - ...pollVotes.map((it) => it.user), - ].withNullifyer); + drafts.addAll( + [ + state.draft, + ...?messages?.map((it) => it.draft), + ...?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), + ...?pinnedMessages?.map((it) => it.user), + ...?reads?.map((it) => it.user), + ...?members?.map((it) => it.user), + ...reactions.map((it) => it.user), + ...pinnedReactions.map((it) => it.user), + ...polls.map((it) => it.createdBy), + ...pollVotes.map((it) => it.user), + ].withNullifyer, + ); } // Removing old members and reactions data as they may have @@ -400,6 +443,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 12f0eb8f6f..7941395501 100644 --- a/packages/stream_chat/lib/src/event_type.dart +++ b/packages/stream_chat/lib/src/event_type.dart @@ -52,23 +52,19 @@ class EventType { static const String channelDeleted = 'channel.deleted'; /// Event sent when a channel is deleted - static const String notificationChannelDeleted = - 'notification.channel_deleted'; + static const String notificationChannelDeleted = 'notification.channel_deleted'; /// Event sent when a channel is truncated static const String channelTruncated = 'channel.truncated'; /// Event sent when a channel is truncated - static const String notificationChannelTruncated = - 'notification.channel_truncated'; + static const String notificationChannelTruncated = 'notification.channel_truncated'; /// Event sent when the user is added to a channel - static const String notificationAddedToChannel = - 'notification.added_to_channel'; + static const String notificationAddedToChannel = 'notification.added_to_channel'; /// Event sent when the user is removed from a channel - static const String notificationRemovedFromChannel = - 'notification.removed_from_channel'; + static const String notificationRemovedFromChannel = 'notification.removed_from_channel'; /// Event sent when a channel is updated static const String channelUpdated = 'channel.updated'; @@ -104,8 +100,7 @@ class EventType { static const String connectionRecovered = 'connection.recovered'; /// Event sent when the user is accepts an invite - static const String notificationInviteAccepted = - 'notification.invite_accepted'; + static const String notificationInviteAccepted = 'notification.invite_accepted'; /// Event sent when the user is invited static const String notificationInvited = 'notification.invited'; @@ -122,6 +117,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'; @@ -150,8 +148,7 @@ class EventType { static const String threadUpdated = 'thread.updated'; /// Event sent when a new message is added to a thread. - static const String notificationThreadMessageNew = - 'notification.thread_message_new'; + static const String notificationThreadMessageNew = 'notification.thread_message_new'; /// Event sent when a draft message is either created or updated. static const String draftUpdated = 'draft.updated'; @@ -171,13 +168,24 @@ 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'; + /// Local event sent when push notification preference is updated. static const String pushPreferenceUpdated = 'push_preference.updated'; /// Local event sent when channel push notification preference is updated. - static const String channelPushPreferenceUpdated = - 'channel.push_preference.updated'; + static const String channelPushPreferenceUpdated = 'channel.push_preference.updated'; /// Event sent when a message is marked as delivered. static const String messageDelivered = 'message.delivered'; + + /// Event sent when all messages of a user are deleted. + static const String userMessagesDeleted = 'user.messages.deleted'; } 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/src/ws/connect_user_details.g.dart b/packages/stream_chat/lib/src/ws/connect_user_details.g.dart index 435c4febfc..2a77459610 100644 --- a/packages/stream_chat/lib/src/ws/connect_user_details.g.dart +++ b/packages/stream_chat/lib/src/ws/connect_user_details.g.dart @@ -6,14 +6,12 @@ part of 'connect_user_details.dart'; // JsonSerializableGenerator // ************************************************************************** -Map _$ConnectUserDetailsToJson(ConnectUserDetails instance) => - { - 'id': instance.id, - if (instance.name case final value?) 'name': value, - if (instance.image case final value?) 'image': value, - if (instance.language case final value?) 'language': value, - if (instance.invisible case final value?) 'invisible': value, - if (instance.privacySettings?.toJson() case final value?) - 'privacy_settings': value, - if (instance.extraData case final value?) 'extra_data': value, - }; +Map _$ConnectUserDetailsToJson(ConnectUserDetails instance) => { + 'id': instance.id, + if (instance.name case final value?) 'name': value, + if (instance.image case final value?) 'image': value, + if (instance.language case final value?) 'language': value, + if (instance.invisible case final value?) 'invisible': value, + if (instance.privacySettings?.toJson() case final value?) 'privacy_settings': value, + if (instance.extraData case final value?) 'extra_data': value, +}; diff --git a/packages/stream_chat/lib/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart index d6cb82a716..40f18756e4 100644 --- a/packages/stream_chat/lib/src/ws/websocket.dart +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -24,10 +24,11 @@ typedef EventHandler = void Function(Event); /// Typedef used for connecting to a websocket. Method returns a /// [WebSocketChannel] and accepts a connection [url] and an optional /// [Iterable] of `protocols`. -typedef WebSocketChannelProvider = WebSocketChannel Function( - Uri uri, { - Iterable? protocols, -}); +typedef WebSocketChannelProvider = + WebSocketChannel Function( + Uri uri, { + Iterable? protocols, + }); /// A WebSocket connection that reconnects upon failure. class WebSocket with TimerHelper { @@ -132,8 +133,7 @@ class WebSocket with TimerHelper { if (_webSocketChannel != null) { _closeWebSocketChannel(); } - _webSocketChannel = - webSocketChannelProvider?.call(uri) ?? WebSocketChannel.connect(uri); + _webSocketChannel = webSocketChannelProvider?.call(uri) ?? WebSocketChannel.connect(uri); _subscribeToWebSocketChannel(); } @@ -409,8 +409,7 @@ class WebSocket with TimerHelper { } void _onConnectionError(error, [stacktrace]) { - _logger?.warning( - '[onConnectionError] #ws; error occurred', error, stacktrace); + _logger?.warning('[onConnectionError] #ws; error occurred', error, stacktrace); StreamWebSocketError wsError; if (error is WebSocketChannelException) { diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 457d0c48a5..b3f358c4c9 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -44,8 +44,11 @@ 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_delete_scope.dart'; export 'src/core/models/message_delivery.dart'; export 'src/core/models/message_reminder.dart'; export 'src/core/models/message_state.dart'; @@ -71,6 +74,5 @@ export 'src/core/util/extension.dart'; export 'src/core/util/message_rules.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 1adff547b0..14d653d358 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.23.0'; +const PACKAGE_VERSION = '10.0.0-beta.13'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index 2ffdba917f..9ae75c72c5 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.23.0 +version: 10.0.0-beta.13 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -18,7 +18,7 @@ issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues # 2. Add it to the melos.yaml file for future updates. environment: - sdk: ^3.6.2 + sdk: ^3.10.0 dependencies: async: ^2.11.0 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 f497da7c95..95259b7eff 100644 --- a/packages/stream_chat/test/fixtures/channel_state_to_json.json +++ b/packages/stream_chat/test/fixtures/channel_state_to_json.json @@ -34,9 +34,7 @@ "poll_id": null, "restricted_visibility": [ "user-id-3" - ], - "draft": null, - "reminder": null + ] }, { "id": "dry-meadow-0-e8e74482-b4cd-48db-9d1e-30e6c191786f", @@ -51,9 +49,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", @@ -68,9 +64,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", @@ -85,9 +79,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", @@ -102,9 +94,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", @@ -119,9 +109,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", @@ -136,9 +124,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", @@ -153,9 +139,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", @@ -170,9 +154,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", @@ -187,9 +169,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", @@ -204,9 +184,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", @@ -221,9 +199,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", @@ -238,9 +214,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", @@ -255,9 +229,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", @@ -272,9 +244,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", @@ -289,9 +259,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", @@ -306,9 +274,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", @@ -323,9 +289,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", @@ -340,9 +304,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", @@ -357,9 +319,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", @@ -374,9 +334,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", @@ -391,9 +349,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", @@ -408,9 +364,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", @@ -425,9 +379,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", @@ -442,12 +394,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/member.json b/packages/stream_chat/test/fixtures/member.json index 6d95d7ad02..63b7ddd175 100644 --- a/packages/stream_chat/test/fixtures/member.json +++ b/packages/stream_chat/test/fixtures/member.json @@ -12,5 +12,10 @@ "channel_role": "channel_member", "created_at": "2020-01-28T22:17:30.95443Z", "updated_at": "2020-01-28T22:17:30.95443Z", + "deleted_messages": [ + "msg-1", + "msg-2", + "msg-3" + ], "some_custom_field": "with_custom_data" } \ No newline at end of file diff --git a/packages/stream_chat/test/fixtures/message.json b/packages/stream_chat/test/fixtures/message.json index 30cb6f0426..ee08a54991 100644 --- a/packages/stream_chat/test/fixtures/message.json +++ b/packages/stream_chat/test/fixtures/message.json @@ -54,11 +54,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_delivery_reporter_test.dart b/packages/stream_chat/test/src/client/channel_delivery_reporter_test.dart index 424e00cec9..7f0a529f3e 100644 --- a/packages/stream_chat/test/src/client/channel_delivery_reporter_test.dart +++ b/packages/stream_chat/test/src/client/channel_delivery_reporter_test.dart @@ -59,15 +59,13 @@ void main() { expect(capturedDeliveries, hasLength(2)); expect( capturedDeliveries.any( - (d) => - d.channelCid == 'test:channel-1' && d.messageId == 'message-1', + (d) => d.channelCid == 'test:channel-1' && d.messageId == 'message-1', ), isTrue, ); expect( capturedDeliveries.any( - (d) => - d.channelCid == 'test:channel-2' && d.messageId == 'message-2', + (d) => d.channelCid == 'test:channel-2' && d.messageId == 'message-2', ), isTrue, ); diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 9610356b00..2ea1f896ed 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -91,8 +91,7 @@ void main() { expect(channel.extraData['image'], imageUrl); const newImage = 'https://getstream.io/new-image'; - final newChannelInstance = - Channel(client, channelType, channelId, image: newImage); + final newChannelInstance = Channel(client, channelType, channelId, image: newImage); expect(newChannelInstance.image, newImage); expect(newChannelInstance.extraData['image'], newImage); @@ -108,8 +107,7 @@ void main() { expect(channel.extraData['name'], name); const newName = 'New channel name'; - final newChannelInstance = - Channel(client, channelType, channelId, name: newName); + final newChannelInstance = Channel(client, channelType, channelId, name: newName); expect(newChannelInstance.name, newName); expect(newChannelInstance.extraData['name'], newName); @@ -147,13 +145,10 @@ void main() { // mock persistence client final channelThreads = >{}; - when(() => client.chatPersistenceClient.getChannelThreads(channelCid)) - .thenAnswer((_) async => channelThreads); + when(() => client.chatPersistenceClient.getChannelThreads(channelCid)).thenAnswer((_) async => channelThreads); final channelState = _generateChannelState(channelId, channelType); - when(() => client.chatPersistenceClient.getChannelStateByCid(channelCid)) - .thenAnswer((_) async => channelState); - when(() => client.chatPersistenceClient.updateMessages(channelCid, any())) - .thenAnswer((_) => Future.value()); + when(() => client.chatPersistenceClient.getChannelStateByCid(channelCid)).thenAnswer((_) async => channelState); + when(() => client.chatPersistenceClient.updateMessages(channelCid, any())).thenAnswer((_) => Future.value()); // client logger when(() => client.logger).thenReturn(_createLogger('mock-client-logger')); @@ -221,6 +216,7 @@ void main() { tearDown(() { channel.dispose(); + clearInteractions(client); }); test('should throw if trying to set `extraData`', () { @@ -255,14 +251,15 @@ void main() { user: client.state.currentUser, ); - final sendMessageResponse = SendMessageResponse() - ..message = message.copyWith(state: MessageState.sent); + final sendMessageResponse = SendMessageResponse()..message = message.copyWith(state: MessageState.sent); - when(() => client.sendMessage( - any(that: isSameMessageAs(message)), - channelId, - channelType, - )).thenAnswer((_) async => sendMessageResponse); + when( + () => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + ), + ).thenAnswer((_) async => sendMessageResponse); expectLater( // skipping first seed message list -> [] messages @@ -288,11 +285,292 @@ void main() { expect(res, isNotNull); expect(res.message.id, message.id); - verify(() => client.sendMessage( + verify( + () => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + ), + ).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', + text: 'Hello world!', + 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', + text: 'Hello world!', + 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', + text: 'Hello world!', + 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', + text: 'Hello world!', + user: client.state.currentUser, + ); + + when( + () => client.sendMessage( any(that: isSameMessageAs(message)), channelId, channelType, - )).called(1); + ), + ).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', + text: 'Hello world!', + 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 { @@ -313,36 +591,43 @@ void main() { final sendImageResponse = SendImageResponse()..file = 'test-image-url'; final sendFileResponse = SendFileResponse()..file = 'test-file-url'; - when(() => client.sendImage( - any(), - channelId, - channelType, - onSendProgress: any(named: 'onSendProgress'), - cancelToken: any(named: 'cancelToken'), - extraData: any(named: 'extraData'), - )).thenAnswer((_) async => sendImageResponse); + when( + () => client.sendImage( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + extraData: any(named: 'extraData'), + ), + ).thenAnswer((_) async => sendImageResponse); - when(() => client.sendFile( - any(), - channelId, - channelType, - onSendProgress: any(named: 'onSendProgress'), - cancelToken: any(named: 'cancelToken'), - extraData: any(named: 'extraData'), - )).thenAnswer((_) async => sendFileResponse); + when( + () => client.sendFile( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + extraData: any(named: 'extraData'), + ), + ).thenAnswer((_) async => sendFileResponse); - when(() => client.sendMessage( - any(that: isSameMessageAs(message)), - channelId, - channelType, - )).thenAnswer((_) async => SendMessageResponse() - ..message = message.copyWith( - attachments: attachments - .map((it) => - it.copyWith(uploadState: const UploadState.success())) - .toList(growable: false), - state: MessageState.sent, - )); + when( + () => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + ), + ).thenAnswer( + (_) async => SendMessageResponse() + ..message = message.copyWith( + attachments: attachments + .map((it) => it.copyWith(uploadState: const UploadState.success())) + .toList(growable: false), + state: MessageState.sent, + ), + ); expectLater( // skipping first seed message list -> [] messages @@ -354,10 +639,7 @@ void main() { isSameMessageAs( message.copyWith( state: MessageState.sending, - attachments: [ - ...attachments.map((it) => it.copyWith( - uploadState: const UploadState.preparing())) - ], + attachments: [...attachments.map((it) => it.copyWith(uploadState: const UploadState.preparing()))], ), matchMessageState: true, matchAttachments: true, @@ -369,8 +651,8 @@ void main() { isSameMessageAs( message.copyWith( state: MessageState.sending, - attachments: [...attachments]..[0] = - attachments[0].copyWith( + attachments: [...attachments] + ..[0] = attachments[0].copyWith( uploadState: const UploadState.success(), ), ), @@ -402,10 +684,7 @@ void main() { isSameMessageAs( message.copyWith( state: MessageState.sending, - attachments: [ - ...attachments.map((it) => - it.copyWith(uploadState: const UploadState.success())) - ], + attachments: [...attachments.map((it) => it.copyWith(uploadState: const UploadState.success()))], ), matchMessageState: true, matchAttachments: true, @@ -416,10 +695,7 @@ void main() { isSameMessageAs( message.copyWith( state: MessageState.sent, - attachments: [ - ...attachments.map((it) => - it.copyWith(uploadState: const UploadState.success())) - ], + attachments: [...attachments.map((it) => it.copyWith(uploadState: const UploadState.success()))], ), matchMessageState: true, matchAttachments: true, @@ -442,29 +718,35 @@ void main() { isTrue, ); - verify(() => client.sendImage( - any(), - channelId, - channelType, - onSendProgress: any(named: 'onSendProgress'), - cancelToken: any(named: 'cancelToken'), - extraData: any(named: 'extraData'), - )).called(2); + verify( + () => client.sendImage( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + extraData: any(named: 'extraData'), + ), + ).called(2); - verify(() => client.sendFile( - any(), - channelId, - channelType, - onSendProgress: any(named: 'onSendProgress'), - cancelToken: any(named: 'cancelToken'), - extraData: any(named: 'extraData'), - )).called(1); + verify( + () => client.sendFile( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + extraData: any(named: 'extraData'), + ), + ).called(1); - verify(() => client.sendMessage( - any(that: isSameMessageAs(message)), - channelId, - channelType, - )).called(1); + verify( + () => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + ), + ).called(1); }); test('should not send if the message is invalid', () async { @@ -755,15 +1037,120 @@ 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.timestamp().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'); setUp(() { - when(() => client.createDraft( - draftMessage, - channelId, - channelType, - )).thenAnswer( + when( + () => client.createDraft( + draftMessage, + channelId, + channelType, + ), + ).thenAnswer( (_) async => CreateDraftResponse() ..draft = Draft( channelCid: channelCid, @@ -779,11 +1166,13 @@ void main() { expect(res, isNotNull); expect(res.draft.message, draftMessage); - verify(() => channel.client.createDraft( - draftMessage, - channelId, - channelType, - )).called(1); + verify( + () => channel.client.createDraft( + draftMessage, + channelId, + channelType, + ), + ).called(1); }); }); @@ -791,11 +1180,13 @@ void main() { final draftMessage = DraftMessage(text: 'Draft message text'); setUp(() { - when(() => client.getDraft( - channelId, - channelType, - parentId: any(named: 'parentId'), - )).thenAnswer( + when( + () => client.getDraft( + channelId, + channelType, + parentId: any(named: 'parentId'), + ), + ).thenAnswer( (_) async => GetDraftResponse() ..draft = Draft( channelCid: channelCid, @@ -811,10 +1202,12 @@ void main() { expect(res, isNotNull); expect(res.draft.message, draftMessage); - verify(() => channel.client.getDraft( - channelId, - channelType, - )).called(1); + verify( + () => channel.client.getDraft( + channelId, + channelType, + ), + ).called(1); }); test('with parentId should pass parentId to client', () async { @@ -824,21 +1217,25 @@ void main() { expect(res, isNotNull); expect(res.draft.message, draftMessage); - verify(() => channel.client.getDraft( - channelId, - channelType, - parentId: parentId, - )).called(1); + verify( + () => channel.client.getDraft( + channelId, + channelType, + parentId: parentId, + ), + ).called(1); }); }); group('`.deleteDraft`', () { setUp(() { - when(() => client.deleteDraft( - channelId, - channelType, - parentId: any(named: 'parentId'), - )).thenAnswer((_) async => EmptyResponse()); + when( + () => client.deleteDraft( + channelId, + channelType, + parentId: any(named: 'parentId'), + ), + ).thenAnswer((_) async => EmptyResponse()); }); test('should call client.deleteDraft', () async { @@ -846,10 +1243,12 @@ void main() { expect(res, isNotNull); - verify(() => channel.client.deleteDraft( - channelId, - channelType, - )).called(1); + verify( + () => channel.client.deleteDraft( + channelId, + channelType, + ), + ).called(1); }); test('with parentId should pass parentId to client', () async { @@ -858,11 +1257,13 @@ void main() { expect(res, isNotNull); - verify(() => channel.client.deleteDraft( - channelId, - channelType, - parentId: parentId, - )).called(1); + verify( + () => channel.client.deleteDraft( + channelId, + channelType, + parentId: parentId, + ), + ).called(1); }); }); @@ -870,10 +1271,12 @@ void main() { const messageId = 'test-message-id'; setUp(() { - when(() => client.createReminder( - messageId, - remindAt: any(named: 'remindAt'), - )).thenAnswer( + when( + () => client.createReminder( + messageId, + remindAt: any(named: 'remindAt'), + ), + ).thenAnswer( (_) async => CreateReminderResponse() ..reminder = MessageReminder( messageId: messageId, @@ -903,10 +1306,12 @@ void main() { expect(res.reminder.messageId, messageId); expect(res.reminder.remindAt, remindAt); - verify(() => channel.client.createReminder( - messageId, - remindAt: remindAt, - )).called(1); + verify( + () => channel.client.createReminder( + messageId, + remindAt: remindAt, + ), + ).called(1); }); }); @@ -914,10 +1319,12 @@ void main() { const messageId = 'test-message-id'; setUp(() { - when(() => client.updateReminder( - messageId, - remindAt: any(named: 'remindAt'), - )).thenAnswer( + when( + () => client.updateReminder( + messageId, + remindAt: any(named: 'remindAt'), + ), + ).thenAnswer( (_) async => UpdateReminderResponse() ..reminder = MessageReminder( messageId: messageId, @@ -947,10 +1354,12 @@ void main() { expect(res.reminder.messageId, messageId); expect(res.reminder.remindAt, remindAt); - verify(() => channel.client.updateReminder( - messageId, - remindAt: remindAt, - )).called(1); + verify( + () => channel.client.updateReminder( + messageId, + remindAt: remindAt, + ), + ).called(1); }); }); @@ -979,11 +1388,11 @@ void main() { state: MessageState.sent, ); - final updateMessageResponse = UpdateMessageResponse() - ..message = message; + final updateMessageResponse = UpdateMessageResponse()..message = message; - when(() => client.updateMessage(any(that: isSameMessageAs(message)))) - .thenAnswer((_) async => updateMessageResponse); + when( + () => client.updateMessage(any(that: isSameMessageAs(message))), + ).thenAnswer((_) async => updateMessageResponse); expectLater( // skipping first seed message list -> [] messages @@ -1009,9 +1418,11 @@ void main() { expect(res, isNotNull); expect(res.message.id, message.id); - verify(() => client.updateMessage( - any(that: isSameMessageAs(message)), - )).called(1); + verify( + () => client.updateMessage( + any(that: isSameMessageAs(message)), + ), + ).called(1); }); test('with attachments should work just fine', () async { @@ -1032,34 +1443,41 @@ void main() { final sendImageResponse = SendImageResponse()..file = 'test-image-url'; final sendFileResponse = SendFileResponse()..file = 'test-file-url'; - when(() => client.sendImage( - any(), - channelId, - channelType, - onSendProgress: any(named: 'onSendProgress'), - cancelToken: any(named: 'cancelToken'), - extraData: any(named: 'extraData'), - )).thenAnswer((_) async => sendImageResponse); + when( + () => client.sendImage( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + extraData: any(named: 'extraData'), + ), + ).thenAnswer((_) async => sendImageResponse); - when(() => client.sendFile( - any(), - channelId, - channelType, - onSendProgress: any(named: 'onSendProgress'), - cancelToken: any(named: 'cancelToken'), - extraData: any(named: 'extraData'), - )).thenAnswer((_) async => sendFileResponse); + when( + () => client.sendFile( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + extraData: any(named: 'extraData'), + ), + ).thenAnswer((_) async => sendFileResponse); - when(() => client.updateMessage( - any(that: isSameMessageAs(message)), - )).thenAnswer((_) async => UpdateMessageResponse() - ..message = message.copyWith( - state: MessageState.sent, - attachments: attachments - .map((it) => - it.copyWith(uploadState: const UploadState.success())) - .toList(growable: false), - )); + when( + () => client.updateMessage( + any(that: isSameMessageAs(message)), + ), + ).thenAnswer( + (_) async => UpdateMessageResponse() + ..message = message.copyWith( + state: MessageState.sent, + attachments: attachments + .map((it) => it.copyWith(uploadState: const UploadState.success())) + .toList(growable: false), + ), + ); expectLater( // skipping first seed message list -> [] messages @@ -1071,10 +1489,7 @@ void main() { isSameMessageAs( message.copyWith( state: MessageState.updating, - attachments: [ - ...attachments.map((it) => it.copyWith( - uploadState: const UploadState.preparing())) - ], + attachments: [...attachments.map((it) => it.copyWith(uploadState: const UploadState.preparing()))], ), matchMessageState: true, matchAttachments: true, @@ -1086,8 +1501,8 @@ void main() { isSameMessageAs( message.copyWith( state: MessageState.updating, - attachments: [...attachments]..[0] = - attachments[0].copyWith( + attachments: [...attachments] + ..[0] = attachments[0].copyWith( uploadState: const UploadState.success(), ), ), @@ -1119,10 +1534,7 @@ void main() { isSameMessageAs( message.copyWith( state: MessageState.updating, - attachments: [ - ...attachments.map((it) => - it.copyWith(uploadState: const UploadState.success())) - ], + attachments: [...attachments.map((it) => it.copyWith(uploadState: const UploadState.success()))], ), matchMessageState: true, matchAttachments: true, @@ -1133,10 +1545,7 @@ void main() { isSameMessageAs( message.copyWith( state: MessageState.updated, - attachments: [ - ...attachments.map((it) => - it.copyWith(uploadState: const UploadState.success())) - ], + attachments: [...attachments.map((it) => it.copyWith(uploadState: const UploadState.success()))], ), matchMessageState: true, matchAttachments: true, @@ -1159,89 +1568,804 @@ void main() { isTrue, ); - verify(() => client.sendImage( - any(), - channelId, - channelType, - onSendProgress: any(named: 'onSendProgress'), - cancelToken: any(named: 'cancelToken'), - extraData: any(named: 'extraData'), - )).called(2); + verify( + () => client.sendImage( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + extraData: any(named: 'extraData'), + ), + ).called(2); - verify(() => client.sendFile( - any(), - channelId, - channelType, - onSendProgress: any(named: 'onSendProgress'), - cancelToken: any(named: 'cancelToken'), - extraData: any(named: 'extraData'), - )).called(1); + verify( + () => client.sendFile( + any(), + channelId, + channelType, + onSendProgress: any(named: 'onSendProgress'), + cancelToken: any(named: 'cancelToken'), + extraData: any(named: 'extraData'), + ), + ).called(1); - verify(() => client.updateMessage( - any(that: isSameMessageAs(message)), - )).called(1); + verify( + () => client.updateMessage( + 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 - channel.state?.messagesStream.skip(1), - emitsInOrder([ - [ - isSameMessageAs( - message.copyWith( - state: MessageState.updating, + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.updating), + matchMessageState: true, ), - matchText: true, - matchMessageState: true, - ), - ], - [ - isSameMessageAs( - updateMessageResponse.message.copyWith( - state: MessageState.updated, + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updatingFailed( + skipPush: false, + skipEnrichUrl: true, + ), + ), + matchMessageState: true, ), - matchText: true, - matchMessageState: true, - ), - ], - ]), - ); - - final res = await channel.partialUpdateMessage( + ], + ]), + ); + + 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([ + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updating, + ), + 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); + 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, + ), + ], + ]), + ); + + try { + await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + ); + } 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 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`', () { + test('should work fine', () async { + const messageId = 'test-message-id'; + final message = Message( + id: messageId, + createdAt: DateTime.now(), + state: MessageState.sent, + ); + + when(() => client.deleteMessage(messageId)).thenAnswer((_) async => EmptyResponse()); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.softDeleting), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith(state: MessageState.softDeleted), + matchMessageState: true, + ), + ], + ]), + ); + + final res = await channel.deleteMessage(message); + + expect(res, isNotNull); + + verify(() => client.deleteMessage(messageId)).called(1); + }); + + test('should delete attachments for hard delete', () async { + final attachments = List.generate( + 3, + (index) => Attachment( + id: 'test-attachment-id-$index', + type: index.isEven ? 'image' : 'file', + file: AttachmentFile(size: index * 33, path: 'test-file-path'), + imageUrl: index.isEven ? 'test-image-url-$index' : null, + assetUrl: index.isOdd ? 'test-asset-url-$index' : null, + uploadState: const UploadState.success(), + ), + ); + + const messageId = 'test-message-id'; + final message = Message( + id: messageId, + attachments: attachments, + createdAt: DateTime.now(), + state: MessageState.sent, + ); + + when( + () => client.deleteMessage(messageId, hard: true), + ).thenAnswer((_) async => EmptyResponse()); + + when( + () => client.deleteImage(any(), channelId, channelType), + ).thenAnswer((_) async => EmptyResponse()); + + when( + () => client.deleteFile(any(), channelId, channelType), + ).thenAnswer((_) async => EmptyResponse()); + + final res = await channel.deleteMessage(message, hard: true); + expect(res, isNotNull); + + verify(() => client.deleteMessage(messageId, hard: true)).called(1); + + verify(() => client.deleteImage(any(), channelId, channelType)).called(2); + + verify(() => client.deleteFile(any(), channelId, channelType)).called(1); + }); + + test( + 'should hard delete the message if the state is sending or failed', + () async { + const messageId = 'test-message-id'; + final message = Message( + id: messageId, + text: 'Hello World!', + state: MessageState.sending, + ); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.sending), + matchMessageState: true, + ), + ], + const [], // message is hard deleted from state + ]), + ); + + // Add message to channel state first + channel.state?.addNewMessage(message); - verify( - () => client.partialUpdateMessage(message.id, set: set, unset: unset), - ).called(1); + final res = await channel.deleteMessage(message); + + expect(res, isNotNull); + verifyNever(() => client.deleteMessage(messageId)); + }, + ); }); - group('`.deleteMessage`', () { + group('`.deleteMessageForMe`', () { test('should work fine', () async { const messageId = 'test-message-id'; final message = Message( @@ -1250,8 +2374,7 @@ void main() { state: MessageState.sent, ); - when(() => client.deleteMessage(messageId)) - .thenAnswer((_) async => EmptyResponse()); + when(() => client.deleteMessageForMe(messageId)).thenAnswer((_) async => EmptyResponse()); expectLater( // skipping first seed message list -> [] messages @@ -1259,73 +2382,34 @@ void main() { emitsInOrder([ [ isSameMessageAs( - message.copyWith(state: MessageState.softDeleting), + message.copyWith(state: MessageState.deletingForMe), matchMessageState: true, ), ], [ isSameMessageAs( - message.copyWith(state: MessageState.softDeleted), + message.copyWith(state: MessageState.deletedForMe), matchMessageState: true, ), ], ]), ); - final res = await channel.deleteMessage(message); - - expect(res, isNotNull); - - verify(() => client.deleteMessage(messageId)).called(1); - }); - - test('should delete attachments for hard delete', () async { - final attachments = List.generate( - 3, - (index) => Attachment( - id: 'test-attachment-id-$index', - type: index.isEven ? 'image' : 'file', - file: AttachmentFile(size: index * 33, path: 'test-file-path'), - imageUrl: index.isEven ? 'test-image-url-$index' : null, - assetUrl: index.isOdd ? 'test-asset-url-$index' : null, - uploadState: const UploadState.success(), - ), - ); - const messageId = 'test-message-id'; - final message = Message( - attachments: attachments, - id: messageId, - createdAt: DateTime.now(), - state: MessageState.sent, - ); - - when(() => client.deleteMessage(messageId, hard: true)) - .thenAnswer((_) async => EmptyResponse()); - - when(() => client.deleteImage(any(), channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); - - when(() => client.deleteFile(any(), channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); - - final res = await channel.deleteMessage(message, hard: true); + final res = await channel.deleteMessageForMe(message); expect(res, isNotNull); - verify(() => client.deleteMessage(messageId, hard: true)).called(1); - verify(() => client.deleteImage( - any(), - channelId, - channelType, - )).called(2); + verify(() => client.deleteMessageForMe(messageId)).called(1); }); test( - '''should directly update the state with message as deleted if the state is sending or failed''', + 'should hard delete the message if the state is sending or failed', () async { const messageId = 'test-message-id'; final message = Message( id: messageId, + text: 'Hello World!', + state: MessageState.sending, ); expectLater( @@ -1334,35 +2418,42 @@ void main() { emitsInOrder([ [ isSameMessageAs( - message.copyWith(state: MessageState.softDeleted), + message.copyWith(state: MessageState.sending), matchMessageState: true, ), ], + const [], // message is hard deleted from state ]), ); - final res = await channel.deleteMessage(message); + // Add message to channel state first + channel.state?.addNewMessage(message); + + final res = await channel.deleteMessageForMe(message); expect(res, isNotNull); - verifyNever(() => client.deleteMessage(messageId)); + verifyNever(() => client.deleteMessageForMe(messageId)); }, ); }); group('`.pinMessage`', () { - test('should work fine without passing timeoutOrExpirationDate', - () async { + test('should work fine without passing timeoutOrExpirationDate', () async { final message = Message(id: 'test-message-id'); - when(() => client.partialUpdateMessage( - message.id, - set: any(named: 'set'), - unset: any(named: 'unset'), - )).thenAnswer((_) async => UpdateMessageResponse() - ..message = message.copyWith( - pinned: true, - pinExpires: null, - )); + when( + () => client.partialUpdateMessage( + message.id, + set: any(named: 'set'), + unset: any(named: 'unset'), + ), + ).thenAnswer( + (_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: true, + pinExpires: null, + ), + ); expectLater( // skipping first seed message list -> [] messages @@ -1389,11 +2480,13 @@ void main() { expect(res.message.pinned, isTrue); expect(res.message.pinExpires, isNull); - verify(() => client.partialUpdateMessage( - message.id, - set: any(named: 'set'), - unset: any(named: 'unset'), - )).called(1); + verify( + () => client.partialUpdateMessage( + message.id, + set: any(named: 'set'), + unset: any(named: 'unset'), + ), + ).called(1); }); test( @@ -1402,17 +2495,21 @@ void main() { final message = Message(id: 'test-message-id'); const timeoutOrExpirationDate = 300; // 300 seconds - when(() => client.partialUpdateMessage( - message.id, - set: any(named: 'set'), - unset: any(named: 'unset'), - )).thenAnswer((_) async => UpdateMessageResponse() - ..message = message.copyWith( - pinned: true, - pinExpires: DateTime.now().add( - const Duration(seconds: timeoutOrExpirationDate), + when( + () => client.partialUpdateMessage( + message.id, + set: any(named: 'set'), + unset: any(named: 'unset'), + ), + ).thenAnswer( + (_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: true, + pinExpires: DateTime.now().add( + const Duration(seconds: timeoutOrExpirationDate), + ), ), - )); + ); expectLater( // skipping first seed message list -> [] messages @@ -1442,11 +2539,13 @@ void main() { expect(res.message.pinned, isTrue); expect(res.message.pinExpires, isNotNull); - verify(() => client.partialUpdateMessage( - message.id, - set: any(named: 'set'), - unset: any(named: 'unset'), - )).called(1); + verify( + () => client.partialUpdateMessage( + message.id, + set: any(named: 'set'), + unset: any(named: 'unset'), + ), + ).called(1); }, ); @@ -1454,18 +2553,21 @@ void main() { 'should work fine if passed timeoutOrExpirationDate as DateTime', () async { final message = Message(id: 'test-message-id'); - final timeoutOrExpirationDate = - DateTime.now().add(const Duration(days: 3)); // 3 days - - when(() => client.partialUpdateMessage( - message.id, - set: any(named: 'set'), - unset: any(named: 'unset'), - )).thenAnswer((_) async => UpdateMessageResponse() - ..message = message.copyWith( - pinned: true, - pinExpires: timeoutOrExpirationDate, - )); + final timeoutOrExpirationDate = DateTime.now().add(const Duration(days: 3)); // 3 days + + when( + () => client.partialUpdateMessage( + message.id, + set: any(named: 'set'), + unset: any(named: 'unset'), + ), + ).thenAnswer( + (_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: true, + pinExpires: timeoutOrExpirationDate, + ), + ); expectLater( // skipping first seed message list -> [] messages @@ -1496,11 +2598,13 @@ void main() { expect(res.message.pinExpires, isNotNull); expect(res.message.pinExpires, timeoutOrExpirationDate.toUtc()); - verify(() => client.partialUpdateMessage( - message.id, - set: any(named: 'set'), - unset: any(named: 'unset'), - )).called(1); + verify( + () => client.partialUpdateMessage( + message.id, + set: any(named: 'set'), + unset: any(named: 'unset'), + ), + ).called(1); }, ); @@ -1525,11 +2629,12 @@ void main() { test('`.unpinMessage`', () async { final message = Message(id: 'test-message-id', pinned: true); - when(() => client.partialUpdateMessage( - message.id, - set: {'pinned': false}, - )).thenAnswer((_) async => UpdateMessageResponse() - ..message = message.copyWith(pinned: false)); + when( + () => client.partialUpdateMessage( + message.id, + set: {'pinned': false}, + ), + ).thenAnswer((_) async => UpdateMessageResponse()..message = message.copyWith(pinned: false)); expectLater( // skipping first seed message list -> [] messages @@ -1555,10 +2660,12 @@ void main() { expect(res, isNotNull); expect(res.message.pinned, isFalse); - verify(() => client.partialUpdateMessage( - message.id, - set: {'pinned': false}, - )).called(1); + verify( + () => client.partialUpdateMessage( + message.id, + set: {'pinned': false}, + ), + ).called(1); }); group('`.search`', () { @@ -1571,12 +2678,14 @@ void main() { final results = List.generate(3, (index) => GetMessageResponse()); - when(() => client.search( - filter, - query: query, - sort: any(named: 'sort'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( + when( + () => client.search( + filter, + query: query, + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + ), + ).thenAnswer( (_) async => SearchMessagesResponse()..results = results, ); @@ -1589,12 +2698,14 @@ void main() { expect(res, isNotNull); expect(res.results.length, results.length); - verify(() => client.search( - filter, - query: query, - sort: any(named: 'sort'), - paginationParams: any(named: 'paginationParams'), - )).called(1); + verify( + () => client.search( + filter, + query: query, + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + ), + ).called(1); }); test('should work fine with `messageFilters`', () async { @@ -1604,12 +2715,14 @@ void main() { final results = List.generate(3, (index) => GetMessageResponse()); - when(() => client.search( - filter, - messageFilters: messageFilters, - sort: any(named: 'sort'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( + when( + () => client.search( + filter, + messageFilters: messageFilters, + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + ), + ).thenAnswer( (_) async => SearchMessagesResponse()..results = results, ); @@ -1622,70 +2735,73 @@ void main() { expect(res, isNotNull); expect(res.results.length, results.length); - verify(() => client.search( - filter, - messageFilters: messageFilters, - sort: any(named: 'sort'), - paginationParams: any(named: 'paginationParams'), - )).called(1); + verify( + () => client.search( + filter, + messageFilters: messageFilters, + sort: any(named: 'sort'), + paginationParams: any(named: 'paginationParams'), + ), + ).called(1); }); }); test('`.deleteFile`', () async { const url = 'test-file-url'; - when(() => client.deleteFile(url, channelId, channelType, - cancelToken: any(named: 'cancelToken'))) - .thenAnswer((_) async => EmptyResponse()); + when( + () => client.deleteFile(url, channelId, channelType, cancelToken: any(named: 'cancelToken')), + ).thenAnswer((_) async => EmptyResponse()); final res = await channel.deleteFile(url); expect(res, isNotNull); - verify(() => client.deleteFile(url, channelId, channelType, - cancelToken: any(named: 'cancelToken'))).called(1); + verify(() => client.deleteFile(url, channelId, channelType, cancelToken: any(named: 'cancelToken'))).called(1); }); test('`.deleteImage`', () async { const url = 'test-image-url'; - when(() => client.deleteImage(url, channelId, channelType, - cancelToken: any(named: 'cancelToken'))) - .thenAnswer((_) async => EmptyResponse()); + when( + () => client.deleteImage(url, channelId, channelType, cancelToken: any(named: 'cancelToken')), + ).thenAnswer((_) async => EmptyResponse()); final res = await channel.deleteImage(url); expect(res, isNotNull); - verify(() => client.deleteImage(url, channelId, channelType, - cancelToken: any(named: 'cancelToken'))).called(1); + verify(() => client.deleteImage(url, channelId, channelType, cancelToken: any(named: 'cancelToken'))).called(1); }); test('`.stopAIResponse`', () async { final stopAIEvent = Event(type: EventType.aiIndicatorStop); - when(() => client.sendEvent( - channelId, - channelType, - any(that: isSameEventAs(stopAIEvent)), - )).thenAnswer((_) async => EmptyResponse()); + when( + () => client.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(stopAIEvent)), + ), + ).thenAnswer((_) async => EmptyResponse()); final res = await channel.stopAIResponse(); expect(res, isNotNull); - verify(() => client.sendEvent( - channelId, - channelType, - any(that: isSameEventAs(stopAIEvent)), - )).called(1); + verify( + () => client.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(stopAIEvent)), + ), + ).called(1); }); test('`.sendEvent`', () async { final event = Event(type: 'event.local'); - when(() => client.sendEvent(channelId, channelType, event)) - .thenAnswer((_) async => EmptyResponse()); + when(() => client.sendEvent(channelId, channelType, event)).thenAnswer((_) async => EmptyResponse()); final res = await channel.sendEvent(event); @@ -1696,139 +2812,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, @@ -1840,14 +2841,9 @@ void main() { emitsInOrder([ [ isSameMessageAs( - message.copyWith( - state: MessageState.sent, - reactionGroups: { - type: ReactionGroup( - count: 1, - sumScores: extraDataScore, - ) - }, + message.copyWith( + state: MessageState.sent, + reactionGroups: {type: ReactionGroup(count: 1, sumScores: 1)}, latestReactions: [reaction], ownReactions: [reaction], ), @@ -1858,27 +2854,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( @@ -1890,10 +2874,15 @@ 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)) - .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + when( + () => client.sendReaction(message.id, reaction), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); expectLater( // skipping first seed message list -> [] messages @@ -1907,7 +2896,7 @@ void main() { type: ReactionGroup( count: 1, sumScores: 1, - ) + ), }, latestReactions: [reaction], ownReactions: [reaction], @@ -1927,25 +2916,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, @@ -1955,7 +2943,7 @@ void main() { prevType: ReactionGroup( count: 1, sumScores: 1, - ) + ), }, state: MessageState.sent, ); @@ -1964,7 +2952,7 @@ void main() { final newReaction = Reaction( type: type, messageId: messageId, - userId: userId, + user: client.state.currentUser, ); final newMessage = message.copyWith( ownReactions: [newReaction], @@ -1973,11 +2961,13 @@ void main() { const enforceUnique = true; - when(() => client.sendReaction( - messageId, - type, - enforceUnique: enforceUnique, - )).thenAnswer( + when( + () => client.sendReaction( + messageId, + newReaction, + enforceUnique: enforceUnique, + ), + ).thenAnswer( (_) async => SendReactionResponse() ..message = newMessage ..reaction = newReaction, @@ -1999,7 +2989,7 @@ void main() { final res = await channel.sendReaction( message, - type, + newReaction, enforceUnique: enforceUnique, ); @@ -2007,11 +2997,13 @@ void main() { expect(res.reaction.type, type); expect(res.reaction.messageId, messageId); - verify(() => client.sendReaction( - messageId, - type, - enforceUnique: enforceUnique, - )).called(1); + verify( + () => client.sendReaction( + messageId, + newReaction, + enforceUnique: enforceUnique, + ), + ).called(1); }, ); }); @@ -2025,9 +3017,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, @@ -2047,7 +3043,7 @@ void main() { type: ReactionGroup( count: 1, sumScores: 1, - ) + ), }, latestReactions: [reaction], ownReactions: [reaction], @@ -2060,13 +3056,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( @@ -2079,16 +3075,19 @@ 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)) - .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + when( + () => client.sendReaction(message.id, reaction), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); expectLater( // skipping first seed message list -> [] messages - channel.state?.threadsStream - .skip(1) - .map((event) => event['test-parent-id']), + channel.state?.threadsStream.skip(1).map((event) => event['test-parent-id']), emitsInOrder([ [ isSameMessageAs( @@ -2098,7 +3097,7 @@ void main() { type: ReactionGroup( count: 1, sumScores: 1, - ) + ), }, latestReactions: [reaction], ownReactions: [reaction], @@ -2120,26 +3119,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, @@ -2150,7 +3148,7 @@ void main() { prevType: ReactionGroup( count: 1, sumScores: 1, - ) + ), }, state: MessageState.sent, ); @@ -2159,7 +3157,7 @@ void main() { final newReaction = Reaction( type: type, messageId: messageId, - userId: userId, + user: client.state.currentUser, ); final newMessage = message.copyWith( ownReactions: [newReaction], @@ -2168,11 +3166,13 @@ void main() { const enforceUnique = true; - when(() => client.sendReaction( - messageId, - type, - enforceUnique: enforceUnique, - )).thenAnswer( + when( + () => client.sendReaction( + messageId, + newReaction, + enforceUnique: enforceUnique, + ), + ).thenAnswer( (_) async => SendReactionResponse() ..message = newMessage ..reaction = newReaction, @@ -2180,9 +3180,7 @@ void main() { expectLater( // skipping first seed message list -> [] messages - channel.state?.threadsStream - .skip(1) - .map((event) => event['test-parent-id']), + channel.state?.threadsStream.skip(1).map((event) => event['test-parent-id']), emitsInOrder([ [ isSameMessageAs( @@ -2197,7 +3195,7 @@ void main() { final res = await channel.sendReaction( message, - type, + newReaction, enforceUnique: enforceUnique, ); @@ -2205,11 +3203,13 @@ void main() { expect(res.reaction.type, type); expect(res.reaction.messageId, messageId); - verify(() => client.sendReaction( - messageId, - type, - enforceUnique: enforceUnique, - )).called(1); + verify( + () => client.sendReaction( + messageId, + newReaction, + enforceUnique: enforceUnique, + ), + ).called(1); }, ); }); @@ -2232,13 +3232,12 @@ void main() { type: ReactionGroup( count: 1, sumScores: 1, - ) + ), }, state: MessageState.sent, ); - when(() => client.deleteReaction(messageId, type)) - .thenAnswer((_) async => EmptyResponse()); + when(() => client.deleteReaction(messageId, type)).thenAnswer((_) async => EmptyResponse()); expectLater( // skipping first seed message list -> [] messages @@ -2284,13 +3283,14 @@ void main() { type: ReactionGroup( count: 1, sumScores: 1, - ) + ), }, state: MessageState.sent, ); - when(() => client.deleteReaction(messageId, type)) - .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + when( + () => client.deleteReaction(messageId, type), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); expectLater( // skipping first seed message list -> [] messages @@ -2349,19 +3349,16 @@ void main() { type: ReactionGroup( count: 1, sumScores: 1, - ) + ), }, state: MessageState.sent, ); - when(() => client.deleteReaction(messageId, type)) - .thenAnswer((_) async => EmptyResponse()); + when(() => client.deleteReaction(messageId, type)).thenAnswer((_) async => EmptyResponse()); expectLater( // skipping first seed message list -> [] messages - channel.state?.threadsStream - .skip(1) - .map((event) => event['test-parent-id']), + channel.state?.threadsStream.skip(1).map((event) => event['test-parent-id']), emitsInOrder([ [ isSameMessageAs( @@ -2406,19 +3403,18 @@ void main() { type: ReactionGroup( count: 1, sumScores: 1, - ) + ), }, state: MessageState.sent, ); - when(() => client.deleteReaction(messageId, type)) - .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + when( + () => client.deleteReaction(messageId, type), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); expectLater( // skipping first seed message list -> [] messages - channel.state?.threadsStream - .skip(1) - .map((event) => event['test-parent-id']), + channel.state?.threadsStream.skip(1).map((event) => event['test-parent-id']), emitsInOrder([ [ isSameMessageAs( @@ -2469,8 +3465,7 @@ void main() { extraData: channelData, ); - when(() => client.updateChannel(channelId, channelType, channelData, - message: any(named: 'message'))).thenAnswer( + when(() => client.updateChannel(channelId, channelType, channelData, message: any(named: 'message'))).thenAnswer( (_) async => UpdateChannelResponse() ..channel = channelModel ..message = updateMessage, @@ -2486,8 +3481,7 @@ void main() { expect(res.channel.extraData, channelData); expect(res.message?.id, updateMessage.id); - verify(() => client.updateChannel(channelId, channelType, channelData, - message: any(named: 'message'))).called(1); + verify(() => client.updateChannel(channelId, channelType, channelData, message: any(named: 'message'))).called(1); }); test('`.updateImage`', () async { @@ -2498,11 +3492,13 @@ void main() { extraData: {'image': image}, ); - when(() => client.updateChannelPartial( - channelId, - channelType, - set: {'image': image}, - )).thenAnswer( + when( + () => client.updateChannelPartial( + channelId, + channelType, + set: {'image': image}, + ), + ).thenAnswer( (_) async => PartialUpdateChannelResponse()..channel = channelModel, ); @@ -2511,11 +3507,13 @@ void main() { expect(res, isNotNull); expect(res.channel.extraData['image'], image); - verify(() => client.updateChannelPartial( - channelId, - channelType, - set: {'image': image}, - )).called(1); + verify( + () => client.updateChannelPartial( + channelId, + channelType, + set: {'image': image}, + ), + ).called(1); }); test('`.updateName`', () async { @@ -2526,11 +3524,13 @@ void main() { extraData: {'name': name}, ); - when(() => client.updateChannelPartial( - channelId, - channelType, - set: {'name': name}, - )).thenAnswer( + when( + () => client.updateChannelPartial( + channelId, + channelType, + set: {'name': name}, + ), + ).thenAnswer( (_) async => PartialUpdateChannelResponse()..channel = channelModel, ); @@ -2539,11 +3539,13 @@ void main() { expect(res, isNotNull); expect(res.channel.extraData['name'], name); - verify(() => client.updateChannelPartial( - channelId, - channelType, - set: {'name': name}, - )).called(1); + verify( + () => client.updateChannelPartial( + channelId, + channelType, + set: {'name': name}, + ), + ).called(1); }); test('`.updatePartial`', () async { @@ -2562,12 +3564,14 @@ void main() { }, ); - when(() => client.updateChannelPartial( - channelId, - channelType, - set: set, - unset: unset, - )).thenAnswer( + when( + () => client.updateChannelPartial( + channelId, + channelType, + set: set, + unset: unset, + ), + ).thenAnswer( (_) async => PartialUpdateChannelResponse()..channel = channelModel, ); @@ -2580,17 +3584,18 @@ void main() { {'coolness': 999, ...set}, ); - verify(() => client.updateChannelPartial( - channelId, - channelType, - set: set, - unset: unset, - )).called(1); + verify( + () => client.updateChannelPartial( + channelId, + channelType, + set: set, + unset: unset, + ), + ).called(1); }); test('`.delete`', () async { - when(() => client.deleteChannel(channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => client.deleteChannel(channelId, channelType)).thenAnswer((_) async => EmptyResponse()); final res = await channel.delete(); @@ -2600,8 +3605,7 @@ void main() { }); test('`.truncate`', () async { - when(() => client.truncateChannel(channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => client.truncateChannel(channelId, channelType)).thenAnswer((_) async => EmptyResponse()); final res = await channel.truncate(); @@ -2615,8 +3619,7 @@ void main() { final channelModel = ChannelModel(cid: channelCid); - when(() => client.acceptChannelInvite(channelId, channelType, - message: any(named: 'message'))).thenAnswer( + when(() => client.acceptChannelInvite(channelId, channelType, message: any(named: 'message'))).thenAnswer( (_) async => AcceptInviteResponse() ..channel = channelModel ..message = message, @@ -2628,8 +3631,7 @@ void main() { expect(res.channel.cid, channelModel.cid); expect(res.message?.id, message.id); - verify(() => client.acceptChannelInvite(channelId, channelType, - message: any(named: 'message'))).called(1); + verify(() => client.acceptChannelInvite(channelId, channelType, message: any(named: 'message'))).called(1); }); test('`.rejectInvite`', () async { @@ -2637,8 +3639,7 @@ void main() { final channelModel = ChannelModel(cid: channelCid); - when(() => client.rejectChannelInvite(channelId, channelType, - message: any(named: 'message'))).thenAnswer( + when(() => client.rejectChannelInvite(channelId, channelType, message: any(named: 'message'))).thenAnswer( (_) async => RejectInviteResponse() ..channel = channelModel ..message = message, @@ -2650,8 +3651,7 @@ void main() { expect(res.channel.cid, channelModel.cid); expect(res.message?.id, message.id); - verify(() => client.rejectChannelInvite(channelId, channelType, - message: any(named: 'message'))).called(1); + verify(() => client.rejectChannelInvite(channelId, channelType, message: any(named: 'message'))).called(1); }); test('`.addMembers`', () async { @@ -2659,16 +3659,14 @@ void main() { 3, (index) => Member(userId: 'test-member-id-$index'), ); - final memberIds = members - .map((it) => it.userId) - .whereType() - .toList(growable: false); + final memberIds = members.map((it) => it.userId).whereType().toList(growable: false); final message = Message(id: 'test-message-id', text: 'Members Added'); final channelModel = ChannelModel(cid: channelCid); - when(() => client.addChannelMembers(channelId, channelType, memberIds, - message: any(named: 'message'))).thenAnswer( + when( + () => client.addChannelMembers(channelId, channelType, memberIds, message: any(named: 'message')), + ).thenAnswer( (_) async => AddMembersResponse() ..channel = channelModel ..members = members @@ -2682,8 +3680,9 @@ void main() { expect(res.members.length, members.length); expect(res.message?.id, message.id); - verify(() => client.addChannelMembers(channelId, channelType, memberIds, - message: any(named: 'message'))).called(1); + verify( + () => client.addChannelMembers(channelId, channelType, memberIds, message: any(named: 'message')), + ).called(1); }); test('`.addMembers` with hideHistoryBefore', () async { @@ -2691,22 +3690,21 @@ void main() { 3, (index) => Member(userId: 'test-member-id-$index'), ); - final memberIds = members - .map((it) => it.userId) - .whereType() - .toList(growable: false); + final memberIds = members.map((it) => it.userId).whereType().toList(growable: false); final message = Message(id: 'test-message-id', text: 'Members Added'); final hideHistoryBefore = DateTime.parse('2024-01-01T00:00:00Z'); final channelModel = ChannelModel(cid: channelCid); - when(() => client.addChannelMembers( - channelId, - channelType, - memberIds, - message: message, - hideHistoryBefore: hideHistoryBefore, - )).thenAnswer( + when( + () => client.addChannelMembers( + channelId, + channelType, + memberIds, + message: message, + hideHistoryBefore: hideHistoryBefore, + ), + ).thenAnswer( (_) async => AddMembersResponse() ..channel = channelModel ..members = members @@ -2724,13 +3722,15 @@ void main() { expect(res.members.length, members.length); expect(res.message?.id, message.id); - verify(() => client.addChannelMembers( - channelId, - channelType, - memberIds, - message: message, - hideHistoryBefore: hideHistoryBefore, - )).called(1); + verify( + () => client.addChannelMembers( + channelId, + channelType, + memberIds, + message: message, + hideHistoryBefore: hideHistoryBefore, + ), + ).called(1); }); test('`.inviteMembers`', () async { @@ -2738,16 +3738,14 @@ void main() { 3, (index) => Member(userId: 'test-member-id-$index'), ); - final memberIds = members - .map((it) => it.userId) - .whereType() - .toList(growable: false); + final memberIds = members.map((it) => it.userId).whereType().toList(growable: false); final message = Message(id: 'test-message-id', text: 'Members Invited'); final channelModel = ChannelModel(cid: channelCid); - when(() => client.inviteChannelMembers(channelId, channelType, memberIds, - message: any(named: 'message'))).thenAnswer( + when( + () => client.inviteChannelMembers(channelId, channelType, memberIds, message: any(named: 'message')), + ).thenAnswer( (_) async => InviteMembersResponse() ..channel = channelModel ..members = members @@ -2761,9 +3759,9 @@ void main() { expect(res.members.length, members.length); expect(res.message?.id, message.id); - verify(() => client.inviteChannelMembers( - channelId, channelType, memberIds, - message: any(named: 'message'))).called(1); + verify( + () => client.inviteChannelMembers(channelId, channelType, memberIds, message: any(named: 'message')), + ).called(1); }); test('`.removeMembers`', () async { @@ -2771,16 +3769,14 @@ void main() { 3, (index) => Member(userId: 'test-member-id-$index'), ); - final memberIds = members - .map((it) => it.userId) - .whereType() - .toList(growable: false); + final memberIds = members.map((it) => it.userId).whereType().toList(growable: false); final message = Message(id: 'test-message-id', text: 'Members Removed'); final channelModel = ChannelModel(cid: channelCid); - when(() => client.removeChannelMembers(channelId, channelType, memberIds, - message: any(named: 'message'))).thenAnswer( + when( + () => client.removeChannelMembers(channelId, channelType, memberIds, message: any(named: 'message')), + ).thenAnswer( (_) async => RemoveMembersResponse() ..channel = channelModel ..members = members @@ -2794,9 +3790,9 @@ void main() { expect(res.members.length, members.length); expect(res.message?.id, message.id); - verify(() => client.removeChannelMembers( - channelId, channelType, memberIds, - message: any(named: 'message'))).called(1); + verify( + () => client.removeChannelMembers(channelId, channelType, memberIds, message: any(named: 'message')), + ).called(1); }); group('`.sendAction`', () { @@ -2851,15 +3847,17 @@ void main() { group('`.watch`', () { test('should work fine', () async { - when(() => client.queryChannel( - channelType, - channelId: channelId, - watch: true, - channelData: any(named: 'channelData'), - messagesPagination: any(named: 'messagesPagination'), - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - )).thenAnswer( + when( + () => client.queryChannel( + channelType, + channelId: channelId, + watch: true, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).thenAnswer( (_) async => _generateChannelState(channelId, channelType), ); @@ -2869,27 +3867,31 @@ void main() { expect(res.channel, isNotNull); expect(res.channel?.cid, channelCid); - verify(() => client.queryChannel( - channelType, - channelId: channelId, - watch: true, - channelData: any(named: 'channelData'), - messagesPagination: any(named: 'messagesPagination'), - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - )).called(1); + verify( + () => client.queryChannel( + channelType, + channelId: channelId, + watch: true, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).called(1); }); test('should rethrow if `.query` throws', () async { - when(() => client.queryChannel( - channelType, - channelId: channelId, - watch: true, - channelData: any(named: 'channelData'), - messagesPagination: any(named: 'messagesPagination'), - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - )).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + when( + () => client.queryChannel( + channelType, + channelId: channelId, + watch: true, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); try { await channel.watch(); @@ -2897,28 +3899,28 @@ void main() { expect(e, isA()); } - verify(() => client.queryChannel( - channelType, - channelId: channelId, - watch: true, - channelData: any(named: 'channelData'), - messagesPagination: any(named: 'messagesPagination'), - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - )).called(1); + verify( + () => client.queryChannel( + channelType, + channelId: channelId, + watch: true, + channelData: any(named: 'channelData'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).called(1); }); }); test('`.stopWatching`', () async { - when(() => client.stopChannelWatching(channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => client.stopChannelWatching(channelId, channelType)).thenAnswer((_) async => EmptyResponse()); final res = await channel.stopWatching(); expect(res, isNotNull); - verify(() => client.stopChannelWatching(channelId, channelType)) - .called(1); + verify(() => client.stopChannelWatching(channelId, channelType)).called(1); }); test('`.getReplies`', () async { @@ -2977,8 +3979,7 @@ void main() { final messageIds = messages.map((it) => it.id).toList(growable: false); - when(() => client.getMessagesById(channelId, channelType, messageIds)) - .thenAnswer( + when(() => client.getMessagesById(channelId, channelType, messageIds)).thenAnswer( (_) async => GetMessagesByIdResponse()..messages = messages, ); @@ -3091,16 +4092,19 @@ void main() { channel.state!.updateChannelState(stateWithMessages); expect(channel.state!.messages, hasLength(3)); - final newState = _generateChannelState( - channelId, - channelType, - ).copyWith(messages: [ - Message(id: 'msg-before-1', text: 'Message before 1'), - Message(id: 'msg-before-2', text: 'Message before 2'), - Message(id: 'target-message-id', text: 'Target message'), - Message(id: 'msg-after-1', text: 'Message after 1'), - Message(id: 'msg-after-2', text: 'Message after 2'), - ]); + final newState = + _generateChannelState( + channelId, + channelType, + ).copyWith( + messages: [ + Message(id: 'msg-before-1', text: 'Message before 1'), + Message(id: 'msg-before-2', text: 'Message before 2'), + Message(id: 'target-message-id', text: 'Target message'), + Message(id: 'msg-after-1', text: 'Message after 1'), + Message(id: 'msg-after-2', text: 'Message after 2'), + ], + ); when( () => client.queryChannel( @@ -3149,16 +4153,19 @@ void main() { expect(channel.state!.messages, hasLength(3)); final targetDate = DateTime.now(); - final newState = _generateChannelState( - channelId, - channelType, - ).copyWith(messages: [ - Message(id: 'msg-before-1', text: 'Message before 1'), - Message(id: 'msg-before-2', text: 'Message before 2'), - Message(id: 'target-message', text: 'Target message'), - Message(id: 'msg-after-1', text: 'Message after 1'), - Message(id: 'msg-after-2', text: 'Message after 2'), - ]); + final newState = + _generateChannelState( + channelId, + channelType, + ).copyWith( + messages: [ + Message(id: 'msg-before-1', text: 'Message before 1'), + Message(id: 'msg-before-2', text: 'Message before 2'), + Message(id: 'target-message', text: 'Target message'), + Message(id: 'msg-after-1', text: 'Message after 1'), + Message(id: 'msg-after-2', text: 'Message after 2'), + ], + ); when( () => client.queryChannel( @@ -3257,28 +4264,32 @@ void main() { (index) => Member(userId: 'test-user-id-$index'), ); - when(() => client.queryMembers( - channelType, - channelId: channelId, - filter: filter, - members: any(named: 'members'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => QueryMembersResponse()..members = members); + when( + () => client.queryMembers( + channelType, + channelId: channelId, + filter: filter, + members: any(named: 'members'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => QueryMembersResponse()..members = members); final res = await channel.queryMembers(filter: filter); expect(res, isNotNull); expect(res.members.length, members.length); - verify(() => client.queryMembers( - channelType, - channelId: channelId, - filter: filter, - members: any(named: 'members'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).called(1); + verify( + () => client.queryMembers( + channelType, + channelId: channelId, + filter: filter, + members: any(named: 'members'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).called(1); }); test('`.queryBannedUsers`', () async { @@ -3292,59 +4303,70 @@ void main() { ), ); - when(() => client.queryBannedUsers( - filter: filter, - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => QueryBannedUsersResponse()..bans = bans); + when( + () => client.queryBannedUsers( + filter: filter, + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => QueryBannedUsersResponse()..bans = bans); final res = await channel.queryBannedUsers(); expect(res, isNotNull); expect(res.bans.length, bans.length); - verify(() => client.queryBannedUsers( - filter: filter, - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).called(1); + verify( + () => client.queryBannedUsers( + filter: filter, + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).called(1); }); test('`.mute`', () async { - when(() => client.muteChannel( - channelCid, - expiration: any(named: 'expiration'), - )).thenAnswer((_) async => EmptyResponse()); + when( + () => client.muteChannel( + channelCid, + expiration: any(named: 'expiration'), + ), + ).thenAnswer((_) async => EmptyResponse()); final res = await channel.mute(); expect(res, isNotNull); - verify(() => client.muteChannel( - channelCid, - expiration: any(named: 'expiration'), - )).called(1); + verify( + () => client.muteChannel( + channelCid, + expiration: any(named: 'expiration'), + ), + ).called(1); }); test('`.mute with expiration`', () async { const expiration = Duration(seconds: 3); - when(() => client.muteChannel( - channelCid, - expiration: expiration, - )).thenAnswer((_) async => EmptyResponse()); + when( + () => client.muteChannel( + channelCid, + expiration: expiration, + ), + ).thenAnswer((_) async => EmptyResponse()); - when(() => client.unmuteChannel(channelCid)) - .thenAnswer((_) async => EmptyResponse()); + when(() => client.unmuteChannel(channelCid)).thenAnswer((_) async => EmptyResponse()); final res = await channel.mute(expiration: expiration); expect(res, isNotNull); - verify(() => client.muteChannel( - channelCid, - expiration: expiration, - )).called(1); + verify( + () => client.muteChannel( + channelCid, + expiration: expiration, + ), + ).called(1); // wait for expiration await Future.delayed(expiration); @@ -3373,22 +4395,25 @@ void main() { cooldown: cooldown, ); - when(() => client.enableSlowdown( - channelId, - channelType, - cooldown, - )).thenAnswer((_) async => PartialUpdateChannelResponse() - ..channel = channelModel); + when( + () => client.enableSlowdown( + channelId, + channelType, + cooldown, + ), + ).thenAnswer((_) async => PartialUpdateChannelResponse()..channel = channelModel); final res = await channel.enableSlowMode(cooldownInterval: 10); expect(res, isNotNull); - verify(() => client.enableSlowdown( - channelId, - channelType, - cooldown, - )).called(1); + verify( + () => client.enableSlowdown( + channelId, + channelType, + cooldown, + ), + ).called(1); }); test('`.disableSlowMode`', () async { @@ -3396,11 +4421,12 @@ void main() { cid: channelCid, ); - when(() => client.disableSlowdown( - channelId, - channelType, - )).thenAnswer((_) async => PartialUpdateChannelResponse() - ..channel = channelModel); + when( + () => client.disableSlowdown( + channelId, + channelType, + ), + ).thenAnswer((_) async => PartialUpdateChannelResponse()..channel = channelModel); final res = await channel.disableSlowMode(); @@ -3413,26 +4439,29 @@ void main() { const userId = 'test-user-id'; const options = {'key': 'value'}; - when(() => client.banUser( - userId, - {'type': channelType, 'id': channelId, ...options}, - )).thenAnswer((_) async => EmptyResponse()); + when( + () => client.banUser( + userId, + {'type': channelType, 'id': channelId, ...options}, + ), + ).thenAnswer((_) async => EmptyResponse()); final res = await channel.banMember(userId, options); expect(res, isNotNull); - verify(() => client.banUser( - userId, - {'type': channelType, 'id': channelId, ...options}, - )).called(1); + verify( + () => client.banUser( + userId, + {'type': channelType, 'id': channelId, ...options}, + ), + ).called(1); }); test('`.unbanUser`', () async { const userId = 'test-user-id'; - when(() => client.unbanUser(userId, any())) - .thenAnswer((_) async => EmptyResponse()); + when(() => client.unbanUser(userId, any())).thenAnswer((_) async => EmptyResponse()); final res = await channel.unbanMember(userId); @@ -3445,26 +4474,29 @@ void main() { const userId = 'test-user-id'; const options = {'key': 'value'}; - when(() => client.shadowBan( - userId, - {'type': channelType, 'id': channelId, ...options}, - )).thenAnswer((_) async => EmptyResponse()); + when( + () => client.shadowBan( + userId, + {'type': channelType, 'id': channelId, ...options}, + ), + ).thenAnswer((_) async => EmptyResponse()); final res = await channel.shadowBan(userId, options); expect(res, isNotNull); - verify(() => client.shadowBan( - userId, - {'type': channelType, 'id': channelId, ...options}, - )).called(1); + verify( + () => client.shadowBan( + userId, + {'type': channelType, 'id': channelId, ...options}, + ), + ).called(1); }); test('`.removeShadowBan`', () async { const userId = 'test-user-id'; - when(() => client.removeShadowBan(userId, any())) - .thenAnswer((_) async => EmptyResponse()); + when(() => client.removeShadowBan(userId, any())).thenAnswer((_) async => EmptyResponse()); final res = await channel.removeShadowBan(userId); @@ -3476,26 +4508,29 @@ void main() { test('`.hide`', () async { const clearHistory = true; - when(() => client.hideChannel( - channelId, - channelType, - clearHistory: clearHistory, - )).thenAnswer((_) async => EmptyResponse()); + when( + () => client.hideChannel( + channelId, + channelType, + clearHistory: clearHistory, + ), + ).thenAnswer((_) async => EmptyResponse()); final res = await channel.hide(clearHistory: clearHistory); expect(res, isNotNull); - verify(() => client.hideChannel( - channelId, - channelType, - clearHistory: clearHistory, - )).called(1); + verify( + () => client.hideChannel( + channelId, + channelType, + clearHistory: clearHistory, + ), + ).called(1); }); test('`.show`', () async { - when(() => client.showChannel(channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => client.showChannel(channelId, channelType)).thenAnswer((_) async => EmptyResponse()); final res = await channel.show(); @@ -3506,8 +4541,7 @@ void main() { // testing archiving test('`.archive`', () async { - when(() => client.archiveChannel( - channelId: channelId, channelType: channelType)).thenAnswer( + when(() => client.archiveChannel(channelId: channelId, channelType: channelType)).thenAnswer( (_) async => FakePartialUpdateMemberResponse(), ); @@ -3515,13 +4549,11 @@ void main() { expect(res, isNotNull); - verify(() => client.archiveChannel( - channelId: channelId, channelType: channelType)).called(1); + verify(() => client.archiveChannel(channelId: channelId, channelType: channelType)).called(1); }); test('`.unarchive`', () async { - when(() => client.unarchiveChannel( - channelId: channelId, channelType: channelType)).thenAnswer( + when(() => client.unarchiveChannel(channelId: channelId, channelType: channelType)).thenAnswer( (_) async => FakePartialUpdateMemberResponse(), ); @@ -3529,36 +4561,32 @@ void main() { expect(res, isNotNull); - verify(() => client.unarchiveChannel( - channelId: channelId, channelType: channelType)).called(1); + verify(() => client.unarchiveChannel(channelId: channelId, channelType: channelType)).called(1); }); // testing pinning test('`.pin`', () async { - when(() => - client.pinChannel(channelId: channelId, channelType: channelType)) - .thenAnswer((_) async => FakePartialUpdateMemberResponse()); + when( + () => client.pinChannel(channelId: channelId, channelType: channelType), + ).thenAnswer((_) async => FakePartialUpdateMemberResponse()); final res = await channel.pin(); expect(res, isNotNull); - verify(() => - client.pinChannel(channelId: channelId, channelType: channelType)) - .called(1); + verify(() => client.pinChannel(channelId: channelId, channelType: channelType)).called(1); }); test('`.unpin`', () async { - when(() => client.unpinChannel( - channelId: channelId, channelType: channelType)) - .thenAnswer((_) async => FakePartialUpdateMemberResponse()); + when( + () => client.unpinChannel(channelId: channelId, channelType: channelType), + ).thenAnswer((_) async => FakePartialUpdateMemberResponse()); final res = await channel.unpin(); expect(res, isNotNull); - verify(() => client.unpinChannel( - channelId: channelId, channelType: channelType)).called(1); + verify(() => client.unpinChannel(channelId: channelId, channelType: channelType)).called(1); }); test('`.on`', () async { @@ -3595,9 +4623,9 @@ void main() { // Set up the mock response for sending message final newMessage = Message(text: 'New message'); - when(() => client.sendMessage(any(), channelId, channelType)) - .thenAnswer((_) async => SendMessageResponse() - ..message = newMessage.copyWith(state: MessageState.sent)); + when( + () => client.sendMessage(any(), channelId, channelType), + ).thenAnswer((_) async => SendMessageResponse()..message = newMessage.copyWith(state: MessageState.sent)); // Send a new message await channel.sendMessage(newMessage); @@ -4426,8 +5454,7 @@ void main() { }, ); - test('should update read state on notification mark unread event', - () async { + test('should update read state on notification mark unread event', () async { // Create the current read state final currentUser = User(id: 'test-user'); final currentRead = Read( @@ -4655,8 +5682,7 @@ void main() { 'should add a new read state if not exist on message delivered event', () async { final newUser = User(id: 'new-user'); - final distantPast = - DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); + final distantPast = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); // Verify initial state final read = channel.state?.read; @@ -5208,8 +6234,7 @@ void main() { expect(updatedMessage?.reminder, isNull); }); - test('should handle reminder.created event for thread messages', - () async { + test('should handle reminder.created event for thread messages', () async { const messageId = 'test-message-id'; const parentId = 'test-parent-id'; @@ -5260,8 +6285,7 @@ void main() { expect(updatedMessage?.reminder?.remindAt, reminder.remindAt); }); - test('should handle reminder.updated event for thread messages', - () async { + test('should handle reminder.updated event for thread messages', () async { const messageId = 'test-message-id'; const parentId = 'test-parent-id'; @@ -5306,68 +6330,547 @@ void main() { ); // Dispatch event - client.addEvent(reminderUpdatedEvent); + client.addEvent(reminderUpdatedEvent); + + // 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); + }); + + 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); - // 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); + // 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)); }); }); @@ -5430,38 +6933,518 @@ void main() { ), ); - // Verify initial state - final pushPreferences = channel.state?.channelState.pushPreferences; - expect(pushPreferences?.chatLevel, ChatLevel.all); - expect(pushPreferences?.disabledUntil, isNull); + // Verify initial state + final pushPreferences = channel.state?.channelState.pushPreferences; + expect(pushPreferences?.chatLevel, ChatLevel.all); + expect(pushPreferences?.disabledUntil, isNull); + + // Create updated channel push preference + final updatedPushPreference = ChannelPushPreference( + chatLevel: ChatLevel.none, + disabledUntil: DateTime.now().add(const Duration(hours: 2)), + ); + + // Create channel.push_preference.updated event + final channelPushPreferenceUpdatedEvent = Event( + cid: channel.cid, + type: EventType.channelPushPreferenceUpdated, + channelPushPreference: updatedPushPreference, + ); + + // Dispatch event + client.addEvent(channelPushPreferenceUpdatedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify channel push preferences were updated + final updatedPreferences = channel.state?.channelState.pushPreferences; + expect(updatedPreferences?.chatLevel, ChatLevel.none); + expect( + updatedPreferences?.disabledUntil, + updatedPushPreference.disabledUntil, + ); + }); + }); + + group('User messages deleted event', () { + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + late Channel channel; + late MockPersistenceClient persistenceClient; + + setUp(() { + persistenceClient = MockPersistenceClient(); + when(() => client.chatPersistenceClient).thenReturn(persistenceClient); + when( + () => persistenceClient.deleteMessagesFromUser( + cid: any(named: 'cid'), + userId: any(named: 'userId'), + hardDelete: any(named: 'hardDelete'), + deletedAt: any(named: 'deletedAt'), + ), + ).thenAnswer((_) async {}); + when(() => persistenceClient.deleteMessageByIds(any())).thenAnswer((_) async {}); + when(() => persistenceClient.deletePinnedMessageByIds(any())).thenAnswer((_) async {}); + when(() => persistenceClient.getChannelThreads(any())).thenAnswer((_) async => >{}); + + final channelState = _generateChannelState(channelId, channelType); + channel = Channel.fromState(client, channelState); + }); + + tearDown(() { + channel.dispose(); + }); + + test( + 'should soft delete all messages from user when hardDelete is false', + () async { + // Setup: Add messages from different users + final user1 = User(id: 'user-1', name: 'User 1'); + final user2 = User(id: 'user-2', name: 'User 2'); + + final message1 = Message( + id: 'msg-1', + text: 'Message from user 1', + user: user1, + ); + final message2 = Message( + id: 'msg-2', + text: 'Another message from user 1', + user: user1, + ); + final message3 = Message( + id: 'msg-3', + text: 'Message from user 2', + user: user2, + ); + + channel.state?.addNewMessage(message1); + channel.state?.addNewMessage(message2); + channel.state?.addNewMessage(message3); + + // Verify initial state + expect(channel.state?.messages.length, equals(3)); + expect( + channel.state?.messages.where((m) => m.user?.id == 'user-1').length, + equals(2), + ); + expect( + channel.state?.messages.where((m) => m.user?.id == 'user-2').length, + equals(1), + ); + + // Create user.messages.deleted event (soft delete) + final deletedAt = DateTime.now(); + final userMessagesDeletedEvent = Event( + cid: channel.cid, + type: EventType.userMessagesDeleted, + user: user1, + hardDelete: false, + createdAt: deletedAt, + ); + + // Dispatch event + client.addEvent(userMessagesDeletedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify user1's messages are soft deleted + expect(channel.state?.messages.length, equals(3)); + final deletedMessages = channel.state?.messages.where((m) => m.user?.id == 'user-1').toList(); + expect(deletedMessages?.length, equals(2)); + for (final message in deletedMessages!) { + expect(message.type, equals(MessageType.deleted)); + expect(message.deletedAt, isNotNull); + expect(message.state.isDeleted, isTrue); + } + + // Verify user2's message is unaffected + final user2Message = channel.state?.messages.firstWhere((m) => m.id == 'msg-3'); + expect(user2Message?.type, isNot(MessageType.deleted)); + expect(user2Message?.deletedAt, isNull); + }, + ); + + test( + 'should hard delete all messages from user when hardDelete is true', + () async { + // Setup: Add messages from different users + final user1 = User(id: 'user-1', name: 'User 1'); + final user2 = User(id: 'user-2', name: 'User 2'); + + final message1 = Message( + id: 'msg-1', + text: 'Message from user 1', + user: user1, + ); + final message2 = Message( + id: 'msg-2', + text: 'Another message from user 1', + user: user1, + ); + final message3 = Message( + id: 'msg-3', + text: 'Message from user 2', + user: user2, + ); + + channel.state?.addNewMessage(message1); + channel.state?.addNewMessage(message2); + channel.state?.addNewMessage(message3); + + // Verify initial state + expect(channel.state?.messages.length, equals(3)); + + // Create user.messages.deleted event (hard delete) + final userMessagesDeletedEvent = Event( + cid: channel.cid, + type: EventType.userMessagesDeleted, + user: user1, + hardDelete: true, + ); + + // Dispatch event + client.addEvent(userMessagesDeletedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify user1's messages are removed + expect(channel.state?.messages.length, equals(1)); + expect( + channel.state?.messages.any((m) => m.user?.id == 'user-1'), + isFalse, + ); + + // Verify user2's message still exists + final user2Message = channel.state?.messages.firstWhere((m) => m.id == 'msg-3'); + expect(user2Message, isNotNull); + expect(user2Message?.user?.id, equals('user-2')); + }, + ); + + test( + 'should handle thread messages from user', + () async { + // Setup: Add parent and thread messages + final user1 = User(id: 'user-1', name: 'User 1'); + final user2 = User(id: 'user-2', name: 'User 2'); + + final parentMessage = Message( + id: 'parent-msg', + text: 'Parent message', + user: user2, + ); + final threadMessage1 = Message( + id: 'thread-msg-1', + text: 'Thread message from user 1', + user: user1, + parentId: 'parent-msg', + ); + final threadMessage2 = Message( + id: 'thread-msg-2', + text: 'Another thread message from user 1', + user: user1, + parentId: 'parent-msg', + ); + + channel.state?.addNewMessage(parentMessage); + channel.state?.addNewMessage(threadMessage1); + channel.state?.addNewMessage(threadMessage2); + + // Verify initial state + expect(channel.state?.messages.length, equals(1)); + expect(channel.state?.threads['parent-msg']?.length, equals(2)); + + // Create user.messages.deleted event (soft delete) + final userMessagesDeletedEvent = Event( + cid: channel.cid, + type: EventType.userMessagesDeleted, + user: user1, + hardDelete: false, + ); + + // Dispatch event + client.addEvent(userMessagesDeletedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify thread messages are soft deleted + final threadMessages = channel.state?.threads['parent-msg']; + expect(threadMessages?.length, equals(2)); + for (final message in threadMessages!) { + expect(message.type, equals(MessageType.deleted)); + expect(message.state.isDeleted, isTrue); + } + + // Verify parent message is unaffected + final parent = channel.state?.messages.first; + expect(parent?.type, isNot(MessageType.deleted)); + }, + ); + + test( + 'should do nothing when user is null', + () async { + // Setup: Add messages + final user1 = User(id: 'user-1', name: 'User 1'); + final message1 = Message( + id: 'msg-1', + text: 'Message from user 1', + user: user1, + ); + + channel.state?.addNewMessage(message1); + + // Verify initial state + expect(channel.state?.messages.length, equals(1)); + + // Create user.messages.deleted event without user + final userMessagesDeletedEvent = Event( + cid: channel.cid, + type: EventType.userMessagesDeleted, + hardDelete: false, + ); + + // Dispatch event + client.addEvent(userMessagesDeletedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify messages are unaffected + expect(channel.state?.messages.length, equals(1)); + expect( + channel.state?.messages.first.type, + isNot(MessageType.deleted), + ); + }, + ); + + test( + 'should handle empty message list', + () async { + // Setup: Empty channel + expect(channel.state?.messages.length, equals(0)); + + // Create user.messages.deleted event + final userMessagesDeletedEvent = Event( + cid: channel.cid, + type: EventType.userMessagesDeleted, + user: User(id: 'user-1'), + hardDelete: false, + ); + + // Dispatch event - should not throw + client.addEvent(userMessagesDeletedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify state is still empty + expect(channel.state?.messages.length, equals(0)); + }, + ); + + test( + 'should delete messages from persistence when hardDelete is true', + () async { + // Setup: Add messages from different users + final user1 = User(id: 'user-1', name: 'User 1'); + final user2 = User(id: 'user-2', name: 'User 2'); + + final message1 = Message( + id: 'msg-1', + text: 'Message from user 1', + user: user1, + ); + final message2 = Message( + id: 'msg-2', + text: 'Another message from user 1', + user: user1, + ); + final message3 = Message( + id: 'msg-3', + text: 'Message from user 2', + user: user2, + ); + + channel.state?.addNewMessage(message1); + channel.state?.addNewMessage(message2); + channel.state?.addNewMessage(message3); + + // Verify initial state + expect(channel.state?.messages.length, equals(3)); + + // Create user.messages.deleted event (hard delete) + final userMessagesDeletedEvent = Event( + cid: channel.cid, + type: EventType.userMessagesDeleted, + user: user1, + hardDelete: true, + ); + + // Dispatch event + client.addEvent(userMessagesDeletedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify messages are removed from persistence + verify( + () => persistenceClient.deleteMessageByIds(['msg-1', 'msg-2']), + ).called(1); + verify( + () => persistenceClient.deletePinnedMessageByIds(['msg-1', 'msg-2']), + ).called(1); + + // Verify user1's messages are removed from state + expect(channel.state?.messages.length, equals(1)); + expect( + channel.state?.messages.any((m) => m.user?.id == 'user-1'), + isFalse, + ); + }, + ); + + test( + 'should not delete from persistence when hardDelete is false', + () async { + // Setup: Add messages + final user1 = User(id: 'user-1', name: 'User 1'); + final message1 = Message( + id: 'msg-1', + text: 'Message from user 1', + user: user1, + ); + + channel.state?.addNewMessage(message1); + + // Create user.messages.deleted event (soft delete) + final userMessagesDeletedEvent = Event( + cid: channel.cid, + type: EventType.userMessagesDeleted, + user: user1, + hardDelete: false, + ); + + // Dispatch event + client.addEvent(userMessagesDeletedEvent); + + // Wait for the event to be processed + await Future.delayed(Duration.zero); + + // Verify persistence deletion methods were NOT called + verifyNever(() => persistenceClient.deleteMessageByIds(any())); + verifyNever(() => persistenceClient.deletePinnedMessageByIds(any())); + + // Verify message is soft deleted (still in state) + expect(channel.state?.messages.length, equals(1)); + expect(channel.state?.messages.first.type, equals(MessageType.deleted)); + }, + ); + + test( + 'should delete all user messages including those only in storage', + () async { + final user1 = User(id: 'user-1', name: 'User 1'); + final user2 = User(id: 'user-2', name: 'User 2'); + + final stateMessage1 = Message( + id: 'msg-1', + text: 'Message from user 1 in state', + user: user1, + pinned: true, + ); + final stateMessage2 = Message( + id: 'msg-2', + text: 'Message from user 2 in state', + user: user2, + ); + final stateThreadMessage1 = Message( + id: 'thread-msg-1', + text: 'Thread message from user 1 in state', + user: user1, + parentId: 'msg-1', + ); + final stateThreadMessage2 = Message( + id: 'thread-msg-2', + text: 'Another thread message from user 2 in state', + user: user2, + parentId: 'msg-1', + ); + + // Load the state with only 2 messages and 1 thread with 2 replies. + // Note: In reality, storage may contain many more user1 messages + // (e.g., older messages not loaded into state yet), but the delete + // operation should remove ALL of them from storage. + channel.state?.addNewMessage(stateMessage1); + channel.state?.addNewMessage(stateMessage2); + channel.state?.addNewMessage(stateThreadMessage1); + channel.state?.addNewMessage(stateThreadMessage2); + + // Verify initial state has only 2 messages and 1 thread with 2 replies + expect(channel.state?.messages.length, equals(2)); + expect(channel.state?.threads['msg-1']?.length, equals(2)); + + // Create user.messages.deleted event (hard delete) + final userMessagesDeletedEvent = Event( + cid: channel.cid, + type: EventType.userMessagesDeleted, + user: user1, + hardDelete: true, + ); + + // Dispatch event + client.addEvent(userMessagesDeletedEvent); - // Create updated channel push preference - final updatedPushPreference = ChannelPushPreference( - chatLevel: ChatLevel.none, - disabledUntil: DateTime.now().add(const Duration(hours: 2)), - ); + // Wait for the event to be processed + await Future.delayed(Duration.zero); - // Create channel.push_preference.updated event - final channelPushPreferenceUpdatedEvent = Event( - cid: channel.cid, - type: EventType.channelPushPreferenceUpdated, - channelPushPreference: updatedPushPreference, - ); + // Verify user1's messages are removed from state + expect(channel.state?.messages.length, equals(1)); + expect(channel.state?.threads['msg-1']?.length, equals(1)); - // Dispatch event - client.addEvent(channelPushPreferenceUpdatedEvent); + expect( + channel.state?.messages.any((m) => m.user?.id == 'user-1'), + isFalse, + ); - // Wait for the event to be processed - await Future.delayed(Duration.zero); + expect( + channel.state?.threads['msg-1']?.any((m) => m.user?.id == 'user-1'), + isFalse, + ); - // Verify channel push preferences were updated - final updatedPreferences = channel.state?.channelState.pushPreferences; - expect(updatedPreferences?.chatLevel, ChatLevel.none); - expect( - updatedPreferences?.disabledUntil, - updatedPushPreference.disabledUntil, - ); - }); + // Verify persistence delete was called - this handles ALL messages + // in storage (both those in state AND those only in storage) + verify( + () => persistenceClient.deleteMessagesFromUser( + cid: channel.cid, + userId: user1.id, + hardDelete: true, + deletedAt: any(named: 'deletedAt'), + ), + ).called(1); + + // Verify in-state messages were also removed from state's persistence + final capturedIds = + verify( + () => persistenceClient.deleteMessageByIds(captureAny()), + ).captured.first + as List; + + expect( + capturedIds, + containsAll([ + 'msg-1', // state message + 'thread-msg-1', // state thread message + ]), + ); + }, + ); }); }); @@ -5599,8 +7582,7 @@ void main() { final messageReads = channel.state!.readsOf(message: message); expect(messageReads.length, 2); - expect(messageReads.map((r) => r.user.id), - containsAll(['user-1', 'user-3'])); + expect(messageReads.map((r) => r.user.id), containsAll(['user-1', 'user-3'])); expect(messageReads.map((r) => r.user.id), isNot(contains('user-2'))); expect(messageReads.map((r) => r.user.id), isNot(contains('sender-id'))); }); @@ -5635,15 +7617,12 @@ void main() { channel.state!.updateChannelState( ChannelState( channel: channelState.channel, - read: [ - Read(user: user1, lastRead: now.add(const Duration(seconds: 1))) - ], + read: [Read(user: user1, lastRead: now.add(const Duration(seconds: 1)))], ), ); }); - test('deliveriesOf should return reads that have delivered the message', - () { + test('deliveriesOf should return reads that have delivered the message', () { final now = DateTime.now(); final sender = User(id: 'sender-id', name: 'Sender'); final user1 = User(id: 'user-1', name: 'User 1'); @@ -5699,15 +7678,13 @@ void main() { final deliveries = channel.state!.deliveriesOf(message: message); expect(deliveries.length, 2); - expect( - deliveries.map((r) => r.user.id), containsAll(['user-1', 'user-4'])); + expect(deliveries.map((r) => r.user.id), containsAll(['user-1', 'user-4'])); expect(deliveries.map((r) => r.user.id), isNot(contains('user-2'))); expect(deliveries.map((r) => r.user.id), isNot(contains('user-3'))); expect(deliveries.map((r) => r.user.id), isNot(contains('sender-id'))); }); - test('deliveriesOfStream should emit delivery updates for a message', - () async { + test('deliveriesOfStream should emit delivery updates for a message', () async { final now = DateTime.now(); final sender = User(id: 'sender-id', name: 'Sender'); final user1 = User(id: 'user-1', name: 'User 1'); @@ -5723,8 +7700,7 @@ void main() { final channel = Channel.fromState(client, channelState); addTearDown(channel.dispose); - final deliveriesStream = - channel.state!.deliveriesOfStream(message: message); + final deliveriesStream = channel.state!.deliveriesOfStream(message: message); expectLater( deliveriesStream, @@ -6010,6 +7986,12 @@ void main() { (channel) => channel.canQueryPollVotes, ); + testCapability( + 'ShareLocation', + ChannelCapability.shareLocation, + (channel) => channel.canShareLocation, + ); + test('returns correct values with multiple capabilities', () { final channelState = _generateChannelState( channelId, @@ -7050,4 +9032,330 @@ 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', + text: 'Hello, World!', + 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', + text: 'Hello, World!', + 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', + text: 'Hello, World!', + 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', + text: 'Hello, World!', + 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', + text: 'Hello, World!', + 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.hardDeletingFailed, + ); + + 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.softDeletingFailed, + ); + + 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 call deleteMessageForMe for deletingForMeFailed state', () async { + final message = Message( + id: 'test-message-id', + createdAt: DateTime.now(), + state: MessageState.deletingForMeFailed, + ); + + when(() => client.deleteMessageForMe(message.id)).thenAnswer((_) async => EmptyResponse()); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.deleteMessageForMe(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 932398336d..b2690505bd 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -1,4 +1,4 @@ -// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_redundant_argument_values, lines_longer_than_80_chars import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/http/token.dart'; @@ -75,8 +75,7 @@ void main() { final user = User(id: 'test-user-id'); final token = Token.development(user.id).rawValue; - when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) - .thenAnswer( + when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))).thenAnswer( (_) async => ConnectGuestUserResponse() ..user = user ..accessToken = token, @@ -103,8 +102,9 @@ void main() { test('should throw if `.getGuestUser` fails', () async { final user = User(id: 'test-user-id'); - when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) - .thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + when( + () => api.guest.getGuestUser(any(that: isSameUserAs(user))), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); expectLater( client.wsConnectionStatusStream, @@ -247,8 +247,7 @@ void main() { final user = User(id: 'test-user-id'); final token = Token.development(user.id).rawValue; - when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) - .thenAnswer( + when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))).thenAnswer( (_) async => ConnectGuestUserResponse() ..user = user ..accessToken = token, @@ -331,8 +330,7 @@ void main() { final user = User(id: 'test-user-id'); final token = Token.development(user.id).rawValue; - when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) - .thenAnswer( + when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))).thenAnswer( (_) async => ConnectGuestUserResponse() ..user = user ..accessToken = token, @@ -377,8 +375,7 @@ void main() { setUp(() { final ws = FakeWebSocketWithConnectionError(); - client = StreamChatClient(apiKey, chatApi: api, ws: ws) - ..chatPersistenceClient = persistence; + client = StreamChatClient(apiKey, chatApi: api, ws: ws)..chatPersistenceClient = persistence; }); tearDown(() { @@ -392,9 +389,10 @@ void main() { final token = Token.development(user.id).rawValue; final event = Event( - type: EventType.healthCheck, - connectionId: 'test-connection-id', - me: OwnUser.fromUser(user)); + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ); when(persistence.getConnectionInfo).thenAnswer((_) async => event); final res = await client.connectUser(user, token); @@ -416,9 +414,10 @@ void main() { } final event = Event( - type: EventType.healthCheck, - connectionId: 'test-connection-id', - me: OwnUser.fromUser(user)); + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ); when(persistence.getConnectionInfo).thenAnswer((_) async => event); final res = await client.connectUserWithProvider(user, tokenProvider); @@ -437,13 +436,13 @@ void main() { final token = Token.development(user.id).rawValue; final event = Event( - type: EventType.healthCheck, - connectionId: 'test-connection-id', - me: OwnUser.fromUser(user)); + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ); when(persistence.getConnectionInfo).thenAnswer((_) async => event); - when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) - .thenAnswer( + when(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))).thenAnswer( (_) async => ConnectGuestUserResponse() ..user = user ..accessToken = token, @@ -455,8 +454,7 @@ void main() { verify(persistence.getConnectionInfo).called(1); verifyNoMoreInteractions(persistence); - verify(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))) - .called(1); + verify(() => api.guest.getGuestUser(any(that: isSameUserAs(user)))).called(1); verifyNoMoreInteractions(api.guest); }, ); @@ -501,12 +499,10 @@ void main() { }); setUp(() async { - when(() => persistence.updateLastSyncAt(any())) - .thenAnswer((_) => Future.value()); + when(() => persistence.updateLastSyncAt(any())).thenAnswer((_) => Future.value()); when(persistence.getLastSyncAt).thenAnswer((_) async => null); final ws = FakeWebSocket(); - client = StreamChatClient(apiKey, chatApi: api, ws: ws) - ..chatPersistenceClient = persistence; + client = StreamChatClient(apiKey, chatApi: api, ws: ws)..chatPersistenceClient = persistence; await client.connectUser(user, token); await delay(300); expect(client.persistenceEnabled, isTrue); @@ -527,26 +523,25 @@ void main() { reset(persistence); const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3']; final lastSyncAt = DateTime.now(); - when(() => api.general.sync(cids, lastSyncAt)) - .thenAnswer((_) async => SyncResponse() - ..events = [ - Event( - isLocal: false, - type: EventType.healthCheck, - connectionId: 'test-connection-id', - me: OwnUser.fromUser(user), - ), - Event( - isLocal: false, - type: EventType.messageDeleted, - message: Message(id: 'test-message-id'), - ), - ]); - - when(() => persistence.updateConnectionInfo(any())) - .thenAnswer((_) => Future.value()); - when(() => persistence.updateLastSyncAt(any())) - .thenAnswer((_) => Future.value()); + when(() => api.general.sync(cids, lastSyncAt)).thenAnswer( + (_) async => SyncResponse() + ..events = [ + Event( + isLocal: false, + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ), + Event( + isLocal: false, + type: EventType.messageDeleted, + message: Message(id: 'test-message-id'), + ), + ], + ); + + when(() => persistence.updateConnectionInfo(any())).thenAnswer((_) => Future.value()); + when(() => persistence.updateLastSyncAt(any())).thenAnswer((_) => Future.value()); await client.sync(cids: cids, lastSyncAt: lastSyncAt); @@ -569,26 +564,25 @@ void main() { when(persistence.getChannelCids).thenAnswer((_) async => cids); when(persistence.getLastSyncAt).thenAnswer((_) async => lastSyncAt); - when(() => api.general.sync(cids, lastSyncAt)) - .thenAnswer((_) async => SyncResponse() - ..events = [ - Event( - isLocal: false, - type: EventType.healthCheck, - connectionId: 'test-connection-id', - me: OwnUser.fromUser(user), - ), - Event( - isLocal: false, - type: EventType.messageDeleted, - message: Message(id: 'test-message-id', text: 'Hey!'), - ), - ]); - - when(() => persistence.updateConnectionInfo(any())) - .thenAnswer((_) => Future.value()); - when(() => persistence.updateLastSyncAt(any())) - .thenAnswer((_) => Future.value()); + when(() => api.general.sync(cids, lastSyncAt)).thenAnswer( + (_) async => SyncResponse() + ..events = [ + Event( + isLocal: false, + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ), + Event( + isLocal: false, + type: EventType.messageDeleted, + message: Message(id: 'test-message-id', text: 'Hey!'), + ), + ], + ); + + when(() => persistence.updateConnectionInfo(any())).thenAnswer((_) => Future.value()); + when(() => persistence.updateLastSyncAt(any())).thenAnswer((_) => Future.value()); await client.sync(); @@ -612,11 +606,14 @@ void main() { ), ); - when(() => persistence.getChannelStates( - filter: any(named: 'filter'), - channelStateSort: any(named: 'channelStateSort'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer((_) async => persistentChannelStates); + when( + () => persistence.getChannelStates( + filter: any(named: 'filter'), + messageLimit: any(named: 'messageLimit'), + channelStateSort: any(named: 'channelStateSort'), + paginationParams: any(named: 'paginationParams'), + ), + ).thenAnswer((_) async => persistentChannelStates); final channelStates = List.generate( 3, @@ -625,35 +622,33 @@ void main() { ), ); - when(() => api.channel.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( + when( + () => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + ), + ).thenAnswer( (_) async => QueryChannelsResponse()..channels = channelStates, ); when(() => persistence.getChannelThreads(any())).thenAnswer( (_) async => >{ for (final channelState in channelStates) - channelState.channel!.cid: [ - Message(id: 'test-message-id', text: 'Test message') - ], + channelState.channel!.cid: [Message(id: 'test-message-id', text: 'Test message')], }, ); - when(() => persistence.updateChannelState(any())) - .thenAnswer((_) async {}); - when(() => persistence.updateChannelThreads(any(), any())) - .thenAnswer((_) async {}); - when(() => persistence.updateChannelQueries(any(), any(), - clearQueryCache: any(named: 'clearQueryCache'))) - .thenAnswer((_) => Future.value()); + when(() => persistence.updateChannelState(any())).thenAnswer((_) async {}); + when(() => persistence.updateChannelThreads(any(), any())).thenAnswer((_) async {}); + when( + () => persistence.updateChannelQueries(any(), any(), clearQueryCache: any(named: 'clearQueryCache')), + ).thenAnswer((_) => Future.value()); expectLater( client.queryChannels(), @@ -669,31 +664,34 @@ void main() { // before our stream starts emitting data await delay(1050); - verify(() => persistence.getChannelStates( - filter: any(named: 'filter'), - channelStateSort: any(named: 'channelStateSort'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - - verify(() => api.channel.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - - verify(() => persistence.getChannelThreads(any())) - .called(channelStates.length); - verify(() => persistence.updateChannelState(any())) - .called(channelStates.length); - verify(() => persistence.updateChannelThreads(any(), any())) - .called(channelStates.length); - verify(() => persistence.updateChannelQueries(any(), any(), - clearQueryCache: any(named: 'clearQueryCache'))).called(1); + verify( + () => persistence.getChannelStates( + filter: any(named: 'filter'), + messageLimit: any(named: 'messageLimit'), + channelStateSort: any(named: 'channelStateSort'), + paginationParams: any(named: 'paginationParams'), + ), + ).called(1); + + verify( + () => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + ), + ).called(1); + + verify(() => persistence.getChannelThreads(any())).called(channelStates.length); + verify(() => persistence.updateChannelState(any())).called(channelStates.length); + verify(() => persistence.updateChannelThreads(any(), any())).called(channelStates.length); + verify( + () => persistence.updateChannelQueries(any(), any(), clearQueryCache: any(named: 'clearQueryCache')), + ).called(1); }, ); @@ -707,36 +705,37 @@ void main() { ), ); - when(() => persistence.getChannelStates( - filter: any(named: 'filter'), - channelStateSort: any(named: 'channelStateSort'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer((_) async => persistentChannelStates); - - when(() => api.channel.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + when( + () => persistence.getChannelStates( + filter: any(named: 'filter'), + messageLimit: any(named: 'messageLimit'), + channelStateSort: any(named: 'channelStateSort'), + paginationParams: any(named: 'paginationParams'), + ), + ).thenAnswer((_) async => persistentChannelStates); + + when( + () => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + ), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); when(() => persistence.getChannelThreads(any())).thenAnswer( (_) async => >{ for (final channelState in persistentChannelStates) - channelState.channel!.cid: [ - Message(id: 'test-message-id', text: 'Test message') - ], + channelState.channel!.cid: [Message(id: 'test-message-id', text: 'Test message')], }, ); - when(() => persistence.updateChannelState(any())) - .thenAnswer((_) async => {}); - when(() => persistence.updateChannelThreads(any(), any())) - .thenAnswer((_) async => {}); + when(() => persistence.updateChannelState(any())).thenAnswer((_) async => {}); + when(() => persistence.updateChannelThreads(any(), any())).thenAnswer((_) async => {}); expectLater( client.queryChannels(), @@ -750,29 +749,31 @@ void main() { // before our stream starts emitting data await delay(1050); - verify(() => persistence.getChannelStates( - filter: any(named: 'filter'), - channelStateSort: any(named: 'channelStateSort'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - - verify(() => api.channel.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).called(1); - - verify(() => persistence.getChannelThreads(any())) - .called(persistentChannelStates.length); - verify(() => persistence.updateChannelState(any())) - .called(persistentChannelStates.length); - verify(() => persistence.updateChannelThreads(any(), any())) - .called(persistentChannelStates.length); + verify( + () => persistence.getChannelStates( + filter: any(named: 'filter'), + messageLimit: any(named: 'messageLimit'), + channelStateSort: any(named: 'channelStateSort'), + paginationParams: any(named: 'paginationParams'), + ), + ).called(1); + + verify( + () => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + ), + ).called(1); + + verify(() => persistence.getChannelThreads(any())).called(persistentChannelStates.length); + verify(() => persistence.updateChannelState(any())).called(persistentChannelStates.length); + verify(() => persistence.updateChannelThreads(any(), any())).called(persistentChannelStates.length); }, ); }); @@ -831,21 +832,22 @@ void main() { const cids = ['test-cid-1', 'test-cid-2', 'test-cid-3']; final lastSyncAt = DateTime.now(); - when(() => api.general.sync(cids, lastSyncAt)) - .thenAnswer((_) async => SyncResponse() - ..events = [ - Event( - isLocal: false, - type: EventType.healthCheck, - connectionId: 'test-connection-id', - me: OwnUser.fromUser(user), - ), - Event( - isLocal: false, - type: EventType.messageDeleted, - message: Message(id: 'test-message-id'), - ), - ]); + when(() => api.general.sync(cids, lastSyncAt)).thenAnswer( + (_) async => SyncResponse() + ..events = [ + Event( + isLocal: false, + type: EventType.healthCheck, + connectionId: 'test-connection-id', + me: OwnUser.fromUser(user), + ), + Event( + isLocal: false, + type: EventType.messageDeleted, + message: Message(id: 'test-message-id'), + ), + ], + ); await client.sync(cids: cids, lastSyncAt: lastSyncAt); @@ -872,16 +874,18 @@ void main() { ), ); - when(() => api.channel.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenAnswer( + when( + () => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + ), + ).thenAnswer( (_) async => QueryChannelsResponse()..channels = channelStates, ); @@ -894,7 +898,25 @@ void main() { // before our stream starts emitting data await delay(300); - verify(() => api.channel.queryChannels( + verify( + () => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + ), + ).called(1); + }); + + test( + '''should rethrow if `.queryChannelsOnline` throws and persistence channels are empty''', + () async { + when( + () => api.channel.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), state: any(named: 'state'), @@ -903,22 +925,8 @@ void main() { memberLimit: any(named: 'memberLimit'), messageLimit: any(named: 'messageLimit'), paginationParams: any(named: 'paginationParams'), - )).called(1); - }); - - test( - '''should rethrow if `.queryChannelsOnline` throws and persistence channels are empty''', - () async { - when(() => api.channel.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); + ), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError)); expectLater( client.queryChannels(), @@ -929,16 +937,18 @@ void main() { // before our stream starts emitting data await delay(300); - verify(() => api.channel.queryChannels( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - memberLimit: any(named: 'memberLimit'), - messageLimit: any(named: 'messageLimit'), - paginationParams: any(named: 'paginationParams'), - )).called(1); + verify( + () => api.channel.queryChannels( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + memberLimit: any(named: 'memberLimit'), + messageLimit: any(named: 'messageLimit'), + paginationParams: any(named: 'paginationParams'), + ), + ).called(1); }, ); }); @@ -949,12 +959,14 @@ void main() { (index) => User(id: 'test-user-id-$index'), ); - when(() => api.user.queryUsers( - presence: any(named: 'presence'), - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => QueryUsersResponse()..users = users); + when( + () => api.user.queryUsers( + presence: any(named: 'presence'), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => QueryUsersResponse()..users = users); expectLater( // skipping initial seed event -> {} users @@ -968,12 +980,14 @@ void main() { expect(res, isNotNull); expect(res.users.length, users.length); - verify(() => api.user.queryUsers( - presence: any(named: 'presence'), - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).called(1); + verify( + () => api.user.queryUsers( + presence: any(named: 'presence'), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).called(1); verifyNoMoreInteractions(api.user); }); @@ -989,21 +1003,25 @@ void main() { const cid = 'message:nice-channel'; final filter = Filter.equal('channel_cid', cid); - when(() => api.moderation.queryBannedUsers( - filter: filter, - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => QueryBannedUsersResponse()..bans = bans); + when( + () => api.moderation.queryBannedUsers( + filter: filter, + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => QueryBannedUsersResponse()..bans = bans); final res = await client.queryBannedUsers(filter: filter); expect(res, isNotNull); expect(res.bans.length, bans.length); - verify(() => api.moderation.queryBannedUsers( - filter: filter, - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).called(1); + verify( + () => api.moderation.queryBannedUsers( + filter: filter, + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).called(1); verifyNoMoreInteractions(api.moderation); }); @@ -1018,23 +1036,29 @@ void main() { ..message = Message(id: 'test-message-id-$index'), ); - when(() => api.general.searchMessages(filter, - query: any(named: 'query'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - messageFilters: any(named: 'messageFilters'))) - .thenAnswer( - (_) async => SearchMessagesResponse()..results = messages); + when( + () => api.general.searchMessages( + filter, + query: any(named: 'query'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + messageFilters: any(named: 'messageFilters'), + ), + ).thenAnswer((_) async => SearchMessagesResponse()..results = messages); final res = await client.search(filter); expect(res, isNotNull); expect(res.results.length, messages.length); - verify(() => api.general.searchMessages(filter, + verify( + () => api.general.searchMessages( + filter, query: any(named: 'query'), sort: any(named: 'sort'), pagination: any(named: 'pagination'), - messageFilters: any(named: 'messageFilters'))).called(1); + messageFilters: any(named: 'messageFilters'), + ), + ).called(1); verifyNoMoreInteractions(api.general); }); @@ -1045,15 +1069,15 @@ void main() { const fileUrl = 'test-file-url'; - when(() => api.fileUploader.sendFile(file, channelId, channelType)) - .thenAnswer((_) async => SendFileResponse()..file = fileUrl); + when( + () => api.fileUploader.sendFile(file, channelId, channelType), + ).thenAnswer((_) async => SendFileResponse()..file = fileUrl); final res = await client.sendFile(file, channelId, channelType); expect(res, isNotNull); expect(res.file, fileUrl); - verify(() => api.fileUploader.sendFile(file, channelId, channelType)) - .called(1); + verify(() => api.fileUploader.sendFile(file, channelId, channelType)).called(1); verifyNoMoreInteractions(api.fileUploader); }); @@ -1064,15 +1088,15 @@ void main() { const fileUrl = 'test-image-url'; - when(() => api.fileUploader.sendImage(image, channelId, channelType)) - .thenAnswer((_) async => SendImageResponse()..file = fileUrl); + when( + () => api.fileUploader.sendImage(image, channelId, channelType), + ).thenAnswer((_) async => SendImageResponse()..file = fileUrl); final res = await client.sendImage(image, channelId, channelType); expect(res, isNotNull); expect(res.file, fileUrl); - verify(() => api.fileUploader.sendImage(image, channelId, channelType)) - .called(1); + verify(() => api.fileUploader.sendImage(image, channelId, channelType)).called(1); verifyNoMoreInteractions(api.fileUploader); }); @@ -1081,14 +1105,12 @@ void main() { const channelType = 'test-channel-type'; const fileUrl = 'test-file-url'; - when(() => api.fileUploader.deleteFile(fileUrl, channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.fileUploader.deleteFile(fileUrl, channelId, channelType)).thenAnswer((_) async => EmptyResponse()); final res = await client.deleteFile(fileUrl, channelId, channelType); expect(res, isNotNull); - verify(() => api.fileUploader.deleteFile(fileUrl, channelId, channelType)) - .called(1); + verify(() => api.fileUploader.deleteFile(fileUrl, channelId, channelType)).called(1); verifyNoMoreInteractions(api.fileUploader); }); @@ -1097,8 +1119,9 @@ void main() { const channelType = 'test-channel-type'; const imageUrl = 'test-image-url'; - when(() => api.fileUploader.deleteImage(imageUrl, channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when( + () => api.fileUploader.deleteImage(imageUrl, channelId, channelType), + ).thenAnswer((_) async => EmptyResponse()); final res = await client.deleteImage(imageUrl, channelId, channelType); expect(res, isNotNull); @@ -1109,26 +1132,78 @@ void main() { verifyNoMoreInteractions(api.fileUploader); }); + test('`.uploadImage`', () async { + final image = AttachmentFile(size: 33, path: 'test-image-path'); + const fileUrl = 'test-image-url'; + + when(() => api.fileUploader.uploadImage(image)).thenAnswer((_) async => UploadImageResponse()..file = fileUrl); + + final res = await client.uploadImage(image); + expect(res, isNotNull); + expect(res.file, fileUrl); + + verify(() => api.fileUploader.uploadImage(image)).called(1); + verifyNoMoreInteractions(api.fileUploader); + }); + + test('`.uploadFile`', () async { + final file = AttachmentFile(size: 33, path: 'test-file-path'); + const fileUrl = 'test-file-url'; + + when(() => api.fileUploader.uploadFile(file)).thenAnswer((_) async => UploadFileResponse()..file = fileUrl); + + final res = await client.uploadFile(file); + expect(res, isNotNull); + expect(res.file, fileUrl); + + verify(() => api.fileUploader.uploadFile(file)).called(1); + verifyNoMoreInteractions(api.fileUploader); + }); + + test('`.removeImage`', () async { + const imageUrl = 'test-image-url'; + + when(() => api.fileUploader.removeImage(imageUrl)).thenAnswer((_) async => EmptyResponse()); + + final res = await client.removeImage(imageUrl); + expect(res, isNotNull); + + verify(() => api.fileUploader.removeImage(imageUrl)).called(1); + verifyNoMoreInteractions(api.fileUploader); + }); + + test('`.removeFile`', () async { + const fileUrl = 'test-file-url'; + + when(() => api.fileUploader.removeFile(fileUrl)).thenAnswer((_) async => EmptyResponse()); + + final res = await client.removeFile(fileUrl); + expect(res, isNotNull); + + verify(() => api.fileUploader.removeFile(fileUrl)).called(1); + verifyNoMoreInteractions(api.fileUploader); + }); + test('`.updateChannel`', () async { const channelId = 'test-channel-id'; const channelType = 'test-channel-type'; const data = {'name': 'test-channel'}; - when(() => api.channel.updateChannel(channelId, channelType, data)) - .thenAnswer((invocation) async => UpdateChannelResponse() - ..channel = ChannelModel( - id: channelId, - type: channelType, - extraData: {...data}, - )); + when(() => api.channel.updateChannel(channelId, channelType, data)).thenAnswer( + (invocation) async => UpdateChannelResponse() + ..channel = ChannelModel( + id: channelId, + type: channelType, + extraData: {...data}, + ), + ); final res = await client.updateChannel(channelId, channelType, data); expect(res, isNotNull); expect(res.channel.cid, '$channelType:$channelId'); expect(res.channel.extraData['name'], 'test-channel'); - verify(() => api.channel.updateChannel(channelId, channelType, data)) - .called(1); + verify(() => api.channel.updateChannel(channelId, channelType, data)).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1141,14 +1216,14 @@ void main() { }; const unset = ['tag', 'last_name']; - when(() => api.channel.updateChannelPartial(channelId, channelType, - set: set, unset: unset)) - .thenAnswer((invocation) async => PartialUpdateChannelResponse() - ..channel = ChannelModel( - id: channelId, - type: channelType, - extraData: {...set}, - )); + when(() => api.channel.updateChannelPartial(channelId, channelType, set: set, unset: unset)).thenAnswer( + (invocation) async => PartialUpdateChannelResponse() + ..channel = ChannelModel( + id: channelId, + type: channelType, + extraData: {...set}, + ), + ); final res = await client.updateChannelPartial( channelId, @@ -1160,8 +1235,7 @@ void main() { expect(res.channel.cid, '$channelType:$channelId'); expect(res.channel.extraData, set); - verify(() => api.channel.updateChannelPartial(channelId, channelType, - set: set, unset: unset)).called(1); + verify(() => api.channel.updateChannelPartial(channelId, channelType, set: set, unset: unset)).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1169,8 +1243,7 @@ void main() { const id = 'test-device-id'; const provider = PushProvider.firebase; - when(() => api.device.addDevice(id, provider)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.device.addDevice(id, provider)).thenAnswer((_) async => EmptyResponse()); final res = await client.addDevice(id, provider); expect(res, isNotNull); @@ -1199,11 +1272,13 @@ void main() { ); expect(res, isNotNull); - verify(() => api.device.addDevice( - id, - provider, - pushProviderName: pushProviderName, - )).called(1); + verify( + () => api.device.addDevice( + id, + provider, + pushProviderName: pushProviderName, + ), + ).called(1); verifyNoMoreInteractions(api.device); }); @@ -1216,8 +1291,7 @@ void main() { ), ); - when(() => api.device.getDevices()) - .thenAnswer((_) async => ListDevicesResponse()..devices = devices); + when(() => api.device.getDevices()).thenAnswer((_) async => ListDevicesResponse()..devices = devices); final res = await client.getDevices(); expect(res, isNotNull); @@ -1230,8 +1304,7 @@ void main() { test('`.removeDevice`', () async { const deviceId = 'test-device-id'; - when(() => api.device.removeDevice(deviceId)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.device.removeDevice(deviceId)).thenAnswer((_) async => EmptyResponse()); final res = await client.removeDevice(deviceId); expect(res, isNotNull); @@ -1351,8 +1424,7 @@ void main() { expect(channel.extraData, channelData); }); - test('should return back in memory channel instance if available', - () async { + test('should return back in memory channel instance if available', () async { const channelType = 'test-channel-type'; const channelId = 'test-channel-id'; const channelData = {'name': 'test-channel-name'}; @@ -1368,22 +1440,24 @@ void main() { channel: ChannelModel(cid: channelCid), ); - when(() => api.channel.queryChannel( - channelType, - channelId: channelId, - channelData: channelData, - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - messagesPagination: any(named: 'messagesPagination'), - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - )).thenAnswer((_) async => channelState); + when( + () => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).thenAnswer((_) async => channelState); expectLater( client.state.channelsStream.skip(1), emitsInOrder([ - {channelCid: isCorrectChannelFor(channelState)} + {channelCid: isCorrectChannelFor(channelState)}, ]), ); @@ -1392,17 +1466,19 @@ void main() { final newChannel = client.channel(channelType, id: channelId); expect(newChannel, channel); - verify(() => api.channel.queryChannel( - channelType, - channelId: channelId, - channelData: channelData, - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - messagesPagination: any(named: 'messagesPagination'), - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - )).called(1); + verify( + () => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).called(1); }); }); @@ -1416,17 +1492,19 @@ void main() { channel: ChannelModel(cid: channelCid, extraData: channelData), ); - when(() => api.channel.queryChannel( - channelType, - channelId: channelId, - channelData: channelData, - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - messagesPagination: any(named: 'messagesPagination'), - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - )).thenAnswer((_) async => channelState); + when( + () => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).thenAnswer((_) async => channelState); final res = await client.createChannel( channelType, @@ -1442,17 +1520,19 @@ void main() { expect(channel.cid, '$channelType:$channelId'); expect(channel.extraData, channelData); - verify(() => api.channel.queryChannel( - channelType, - channelId: channelId, - channelData: channelData, - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - messagesPagination: any(named: 'messagesPagination'), - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - )).called(1); + verify( + () => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1466,17 +1546,19 @@ void main() { channel: ChannelModel(cid: channelCid, extraData: channelData), ); - when(() => api.channel.queryChannel( - channelType, - channelId: channelId, - channelData: channelData, - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - messagesPagination: any(named: 'messagesPagination'), - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - )).thenAnswer((_) async => channelState); + when( + () => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).thenAnswer((_) async => channelState); final res = await client.watchChannel( channelType, @@ -1492,17 +1574,19 @@ void main() { expect(channel.cid, '$channelType:$channelId'); expect(channel.extraData, channelData); - verify(() => api.channel.queryChannel( - channelType, - channelId: channelId, - channelData: channelData, - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - messagesPagination: any(named: 'messagesPagination'), - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - )).called(1); + verify( + () => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1516,17 +1600,19 @@ void main() { channel: ChannelModel(cid: channelCid, extraData: channelData), ); - when(() => api.channel.queryChannel( - channelType, - channelId: channelId, - channelData: channelData, - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - messagesPagination: any(named: 'messagesPagination'), - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - )).thenAnswer((_) async => channelState); + when( + () => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).thenAnswer((_) async => channelState); final res = await client.queryChannel( channelType, @@ -1542,17 +1628,19 @@ void main() { expect(channel.cid, '$channelType:$channelId'); expect(channel.extraData, channelData); - verify(() => api.channel.queryChannel( - channelType, - channelId: channelId, - channelData: channelData, - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - messagesPagination: any(named: 'messagesPagination'), - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - )).called(1); + verify( + () => api.channel.queryChannel( + channelType, + channelId: channelId, + channelData: channelData, + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + messagesPagination: any(named: 'messagesPagination'), + membersPagination: any(named: 'membersPagination'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1580,8 +1668,7 @@ void main() { const channelType = 'test-channel-type'; const channelId = 'test-channel-id'; - when(() => api.channel.hideChannel(channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.channel.hideChannel(channelId, channelType)).thenAnswer((_) async => EmptyResponse()); final res = await client.hideChannel(channelId, channelType); @@ -1595,8 +1682,7 @@ void main() { const channelType = 'test-channel-type'; const channelId = 'test-channel-id'; - when(() => api.channel.showChannel(channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.channel.showChannel(channelId, channelType)).thenAnswer((_) async => EmptyResponse()); final res = await client.showChannel(channelId, channelType); @@ -1610,8 +1696,7 @@ void main() { const channelType = 'test-channel-type'; const channelId = 'test-channel-id'; - when(() => api.channel.deleteChannel(channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.channel.deleteChannel(channelId, channelType)).thenAnswer((_) async => EmptyResponse()); final res = await client.deleteChannel(channelId, channelType); @@ -1625,8 +1710,7 @@ void main() { const channelType = 'test-channel-type'; const channelId = 'test-channel-id'; - when(() => api.channel.truncateChannel(channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.channel.truncateChannel(channelId, channelType)).thenAnswer((_) async => EmptyResponse()); final res = await client.truncateChannel(channelId, channelType); @@ -1643,8 +1727,7 @@ void main() { const channelId = 'test-channel-id'; const channelCid = '$channelType:$channelId'; - when(() => api.moderation.muteChannel(channelCid)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.moderation.muteChannel(channelCid)).thenAnswer((_) async => EmptyResponse()); final res = await client.muteChannel(channelCid); @@ -1659,8 +1742,7 @@ void main() { const channelId = 'test-channel-id'; const channelCid = '$channelType:$channelId'; - when(() => api.moderation.unmuteChannel(channelCid)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.moderation.unmuteChannel(channelCid)).thenAnswer((_) async => EmptyResponse()); final res = await client.unmuteChannel(channelCid); @@ -1677,14 +1759,18 @@ void main() { const set = {'pinned': true}; const unset = ['pinned']; - when(() => api.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: set, - unset: unset, - )).thenAnswer((_) async => FakePartialUpdateMemberResponse( - channelMember: Member(userId: otherUserId), - )); + when( + () => api.channel.updateMemberPartial( + channelId: channelId, + channelType: channelType, + set: set, + unset: unset, + ), + ).thenAnswer( + (_) async => FakePartialUpdateMemberResponse( + channelMember: Member(userId: otherUserId), + ), + ); final res = await client.partialMemberUpdate( channelId: channelId, @@ -1696,12 +1782,14 @@ void main() { expect(res, isNotNull); expect(res.channelMember.userId, otherUserId); - verify(() => api.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: set, - unset: unset, - )).called(1); + verify( + () => api.channel.updateMemberPartial( + channelId: channelId, + channelType: channelType, + set: set, + unset: unset, + ), + ).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1711,14 +1799,18 @@ void main() { const set = {'pinned': true}; const unset = ['pinned']; - when(() => api.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: set, - unset: unset, - )).thenAnswer((_) async => FakePartialUpdateMemberResponse( - channelMember: Member(userId: userId), - )); + when( + () => api.channel.updateMemberPartial( + channelId: channelId, + channelType: channelType, + set: set, + unset: unset, + ), + ).thenAnswer( + (_) async => FakePartialUpdateMemberResponse( + channelMember: Member(userId: userId), + ), + ); final res = await client.partialMemberUpdate( channelId: channelId, @@ -1729,12 +1821,14 @@ void main() { expect(res, isNotNull); expect(res.channelMember.userId, userId); - verify(() => api.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: set, - unset: unset, - )).called(1); + verify( + () => api.channel.updateMemberPartial( + channelId: channelId, + channelType: channelType, + set: set, + unset: unset, + ), + ).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1742,13 +1836,17 @@ void main() { const channelType = 'test-channel-type'; const channelId = 'test-channel-id'; - when(() => api.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: const MemberUpdatePayload(pinned: true).toJson(), - )).thenAnswer((_) async => FakePartialUpdateMemberResponse( - channelMember: Member(userId: userId, pinnedAt: DateTime.now()), - )); + when( + () => api.channel.updateMemberPartial( + channelId: channelId, + channelType: channelType, + set: const MemberUpdatePayload(pinned: true).toJson(), + ), + ).thenAnswer( + (_) async => FakePartialUpdateMemberResponse( + channelMember: Member(userId: userId, pinnedAt: DateTime.now()), + ), + ); final res = await client.pinChannel( channelId: channelId, @@ -1757,11 +1855,13 @@ void main() { expect(res, isNotNull); - verify(() => api.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: const MemberUpdatePayload(pinned: true).toJson(), - )).called(1); + verify( + () => api.channel.updateMemberPartial( + channelId: channelId, + channelType: channelType, + set: const MemberUpdatePayload(pinned: true).toJson(), + ), + ).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1769,13 +1869,17 @@ void main() { const channelType = 'test-channel-type'; const channelId = 'test-channel-id'; - when(() => api.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - unset: [MemberUpdateType.pinned.name], - )).thenAnswer((_) async => FakePartialUpdateMemberResponse( - channelMember: Member(userId: userId, pinnedAt: DateTime.now()), - )); + when( + () => api.channel.updateMemberPartial( + channelId: channelId, + channelType: channelType, + unset: [MemberUpdateType.pinned.name], + ), + ).thenAnswer( + (_) async => FakePartialUpdateMemberResponse( + channelMember: Member(userId: userId, pinnedAt: DateTime.now()), + ), + ); final res = await client.unpinChannel( channelId: channelId, @@ -1784,11 +1888,13 @@ void main() { expect(res, isNotNull); - verify(() => api.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - unset: [MemberUpdateType.pinned.name], - )).called(1); + verify( + () => api.channel.updateMemberPartial( + channelId: channelId, + channelType: channelType, + unset: [MemberUpdateType.pinned.name], + ), + ).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1796,13 +1902,17 @@ void main() { const channelType = 'test-channel-type'; const channelId = 'test-channel-id'; - when(() => api.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: const MemberUpdatePayload(archived: true).toJson(), - )).thenAnswer((_) async => FakePartialUpdateMemberResponse( - channelMember: Member(userId: userId, archivedAt: DateTime.now()), - )); + when( + () => api.channel.updateMemberPartial( + channelId: channelId, + channelType: channelType, + set: const MemberUpdatePayload(archived: true).toJson(), + ), + ).thenAnswer( + (_) async => FakePartialUpdateMemberResponse( + channelMember: Member(userId: userId, archivedAt: DateTime.now()), + ), + ); final res = await client.archiveChannel( channelId: channelId, @@ -1811,11 +1921,13 @@ void main() { expect(res, isNotNull); - verify(() => api.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - set: const MemberUpdatePayload(archived: true).toJson(), - )).called(1); + verify( + () => api.channel.updateMemberPartial( + channelId: channelId, + channelType: channelType, + set: const MemberUpdatePayload(archived: true).toJson(), + ), + ).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1823,13 +1935,17 @@ void main() { const channelType = 'test-channel-type'; const channelId = 'test-channel-id'; - when(() => api.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - unset: [MemberUpdateType.archived.name], - )).thenAnswer((_) async => FakePartialUpdateMemberResponse( - channelMember: Member(userId: userId, pinnedAt: DateTime.now()), - )); + when( + () => api.channel.updateMemberPartial( + channelId: channelId, + channelType: channelType, + unset: [MemberUpdateType.archived.name], + ), + ).thenAnswer( + (_) async => FakePartialUpdateMemberResponse( + channelMember: Member(userId: userId, pinnedAt: DateTime.now()), + ), + ); final res = await client.unarchiveChannel( channelId: channelId, @@ -1838,11 +1954,13 @@ void main() { expect(res, isNotNull); - verify(() => api.channel.updateMemberPartial( - channelId: channelId, - channelType: channelType, - unset: [MemberUpdateType.archived.name], - )).called(1); + verify( + () => api.channel.updateMemberPartial( + channelId: channelId, + channelType: channelType, + unset: [MemberUpdateType.archived.name], + ), + ).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1851,16 +1969,15 @@ void main() { const channelId = 'test-channel-id'; const channelCid = '$channelType:$channelId'; - when(() => api.channel.acceptChannelInvite(channelId, channelType)) - .thenAnswer((_) async => - AcceptInviteResponse()..channel = ChannelModel(cid: channelCid)); + when( + () => api.channel.acceptChannelInvite(channelId, channelType), + ).thenAnswer((_) async => AcceptInviteResponse()..channel = ChannelModel(cid: channelCid)); final res = await client.acceptChannelInvite(channelId, channelType); expect(res, isNotNull); expect(res.channel.cid, channelCid); - verify(() => api.channel.acceptChannelInvite(channelId, channelType)) - .called(1); + verify(() => api.channel.acceptChannelInvite(channelId, channelType)).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1869,16 +1986,15 @@ void main() { const channelId = 'test-channel-id'; const channelCid = '$channelType:$channelId'; - when(() => api.channel.rejectChannelInvite(channelId, channelType)) - .thenAnswer((_) async => - RejectInviteResponse()..channel = ChannelModel(cid: channelCid)); + when( + () => api.channel.rejectChannelInvite(channelId, channelType), + ).thenAnswer((_) async => RejectInviteResponse()..channel = ChannelModel(cid: channelCid)); final res = await client.rejectChannelInvite(channelId, channelType); expect(res, isNotNull); expect(res.channel.cid, channelCid); - verify(() => api.channel.rejectChannelInvite(channelId, channelType)) - .called(1); + verify(() => api.channel.rejectChannelInvite(channelId, channelType)).called(1); verifyNoMoreInteractions(api.channel); }); @@ -1894,10 +2010,11 @@ void main() { final memberIds = members.map((e) => e.userId!).toList(growable: false); - when(() => api.channel.addMembers(channelId, channelType, memberIds)) - .thenAnswer((_) async => AddMembersResponse() - ..channel = ChannelModel(cid: channelCid) - ..members = members); + when(() => api.channel.addMembers(channelId, channelType, memberIds)).thenAnswer( + (_) async => AddMembersResponse() + ..channel = ChannelModel(cid: channelCid) + ..members = members, + ); final res = await client.addChannelMembers( channelId, @@ -1928,14 +2045,18 @@ void main() { final memberIds = members.map((e) => e.userId!).toList(growable: false); final hideHistoryBefore = DateTime.parse('2024-01-01T00:00:00Z'); - when(() => api.channel.addMembers( - channelId, - channelType, - memberIds, - hideHistoryBefore: hideHistoryBefore, - )).thenAnswer((_) async => AddMembersResponse() - ..channel = ChannelModel(cid: channelCid) - ..members = members); + when( + () => api.channel.addMembers( + channelId, + channelType, + memberIds, + hideHistoryBefore: hideHistoryBefore, + ), + ).thenAnswer( + (_) async => AddMembersResponse() + ..channel = ChannelModel(cid: channelCid) + ..members = members, + ); final res = await client.addChannelMembers( channelId, @@ -1971,10 +2092,11 @@ void main() { final memberIds = members.map((e) => e.userId!).toList(growable: false); - when(() => api.channel.removeMembers(channelId, channelType, memberIds)) - .thenAnswer((_) async => RemoveMembersResponse() - ..channel = ChannelModel(cid: channelCid) - ..members = members); + when(() => api.channel.removeMembers(channelId, channelType, memberIds)).thenAnswer( + (_) async => RemoveMembersResponse() + ..channel = ChannelModel(cid: channelCid) + ..members = members, + ); final res = await client.removeChannelMembers( channelId, @@ -2004,11 +2126,11 @@ void main() { final memberIds = members.map((e) => e.userId!).toList(growable: false); - when(() => api.channel - .inviteChannelMembers(channelId, channelType, memberIds)) - .thenAnswer((_) async => InviteMembersResponse() - ..channel = ChannelModel(cid: channelCid) - ..members = members); + when(() => api.channel.inviteChannelMembers(channelId, channelType, memberIds)).thenAnswer( + (_) async => InviteMembersResponse() + ..channel = ChannelModel(cid: channelCid) + ..members = members, + ); final res = await client.inviteChannelMembers( channelId, @@ -2020,8 +2142,7 @@ void main() { expect(res.channel.cid, channelCid); expect(res.members.length, memberIds.length); - verify(() => api.channel - .inviteChannelMembers(channelId, channelType, memberIds)).called(1); + verify(() => api.channel.inviteChannelMembers(channelId, channelType, memberIds)).called(1); verifyNoMoreInteractions(api.channel); }); @@ -2029,8 +2150,7 @@ void main() { const channelType = 'test-channel-type'; const channelId = 'test-channel-id'; - when(() => api.channel.stopWatching(channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.channel.stopWatching(channelId, channelType)).thenAnswer((_) async => EmptyResponse()); final res = await client.stopChannelWatching(channelId, channelType); expect(res, isNotNull); @@ -2045,9 +2165,9 @@ void main() { const messageId = 'test-message-id'; const formData = {'key': 'value'}; - when(() => api.message - .sendAction(channelId, channelType, messageId, formData)) - .thenAnswer((_) async => SendActionResponse()); + when( + () => api.message.sendAction(channelId, channelType, messageId, formData), + ).thenAnswer((_) async => SendActionResponse()); final res = await client.sendAction( channelId, @@ -2058,8 +2178,7 @@ void main() { expect(res, isNotNull); - verify(() => api.message - .sendAction(channelId, channelType, messageId, formData)).called(1); + verify(() => api.message.sendAction(channelId, channelType, messageId, formData)).called(1); verifyNoMoreInteractions(api.message); }); @@ -2067,8 +2186,7 @@ void main() { const channelType = 'test-channel-type'; const channelId = 'test-channel-id'; - when(() => api.channel.markRead(channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.channel.markRead(channelId, channelType)).thenAnswer((_) async => EmptyResponse()); final res = await client.markChannelRead(channelId, channelType); @@ -2083,8 +2201,7 @@ void main() { const channelId = 'test-channel-id'; const messageId = 'test-message-id'; - when(() => api.channel.markUnread(channelId, channelType, messageId)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.channel.markUnread(channelId, channelType, messageId)).thenAnswer((_) async => EmptyResponse()); final res = await client.markChannelUnread( channelId, @@ -2094,8 +2211,7 @@ void main() { expect(res, isNotNull); - verify(() => api.channel.markUnread(channelId, channelType, messageId)) - .called(1); + verify(() => api.channel.markUnread(channelId, channelType, messageId)).called(1); verifyNoMoreInteractions(api.channel); }); @@ -2104,11 +2220,13 @@ void main() { const channelId = 'test-channel-id'; final timestamp = DateTime.parse('2024-01-01T00:00:00Z'); - when(() => api.channel.markUnreadByTimestamp( - channelId, - channelType, - timestamp, - )).thenAnswer((_) async => EmptyResponse()); + when( + () => api.channel.markUnreadByTimestamp( + channelId, + channelType, + timestamp, + ), + ).thenAnswer((_) async => EmptyResponse()); final res = await client.markChannelUnreadByTimestamp( channelId, @@ -2118,11 +2236,13 @@ void main() { expect(res, isNotNull); - verify(() => api.channel.markUnreadByTimestamp( - channelId, - channelType, - timestamp, - )).called(1); + verify( + () => api.channel.markUnreadByTimestamp( + channelId, + channelType, + timestamp, + ), + ).called(1); verifyNoMoreInteractions(api.channel); }); @@ -2206,25 +2326,23 @@ void main() { ], ); - when(() => api.polls.partialUpdatePoll(pollId, set: set, unset: unset)) - .thenAnswer((_) async => UpdatePollResponse()..poll = poll); + when( + () => api.polls.partialUpdatePoll(pollId, set: set, unset: unset), + ).thenAnswer((_) async => UpdatePollResponse()..poll = poll); - final res = - await client.partialUpdatePoll(pollId, set: set, unset: unset); + final res = await client.partialUpdatePoll(pollId, set: set, unset: unset); expect(res, isNotNull); expect(res.poll.id, pollId); expect(res.poll.name, set['name']); - verify(() => api.polls.partialUpdatePoll(pollId, set: set, unset: unset)) - .called(1); + verify(() => api.polls.partialUpdatePoll(pollId, set: set, unset: unset)).called(1); verifyNoMoreInteractions(api.polls); }); test('`.deletePoll`', () async { const pollId = 'test-poll-id'; - when(() => api.polls.deletePoll(pollId)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.polls.deletePoll(pollId)).thenAnswer((_) async => EmptyResponse()); final res = await client.deletePoll(pollId); expect(res, isNotNull); @@ -2236,15 +2354,14 @@ void main() { test('`.closePoll`', () async { const pollId = 'test-poll-id'; - when(() => api.polls.partialUpdatePoll(pollId, set: {'is_closed': true})) - .thenAnswer((_) async => UpdatePollResponse()); + when( + () => api.polls.partialUpdatePoll(pollId, set: {'is_closed': true}), + ).thenAnswer((_) async => UpdatePollResponse()); final res = await client.closePoll(pollId); expect(res, isNotNull); - verify(() => - api.polls.partialUpdatePoll(pollId, set: {'is_closed': true})) - .called(1); + verify(() => api.polls.partialUpdatePoll(pollId, set: {'is_closed': true})).called(1); verifyNoMoreInteractions(api.polls); }); @@ -2252,8 +2369,9 @@ void main() { const pollId = 'test-poll-id'; const option = PollOption(text: 'Red'); - when(() => api.polls.createPollOption(pollId, option)).thenAnswer( - (_) async => CreatePollOptionResponse()..pollOption = option); + when( + () => api.polls.createPollOption(pollId, option), + ).thenAnswer((_) async => CreatePollOptionResponse()..pollOption = option); final res = await client.createPollOption(pollId, option); expect(res, isNotNull); @@ -2268,8 +2386,9 @@ void main() { const optionId = 'test-option-id'; const option = PollOption(id: optionId, text: 'Red'); - when(() => api.polls.getPollOption(pollId, optionId)).thenAnswer( - (_) async => GetPollOptionResponse()..pollOption = option); + when( + () => api.polls.getPollOption(pollId, optionId), + ).thenAnswer((_) async => GetPollOptionResponse()..pollOption = option); final res = await client.getPollOption(pollId, optionId); expect(res, isNotNull); @@ -2283,8 +2402,9 @@ void main() { const pollId = 'test-poll-id'; const option = PollOption(id: 'test-option-id', text: 'Red'); - when(() => api.polls.updatePollOption(pollId, option)).thenAnswer( - (_) async => UpdatePollOptionResponse()..pollOption = option); + when( + () => api.polls.updatePollOption(pollId, option), + ).thenAnswer((_) async => UpdatePollOptionResponse()..pollOption = option); final res = await client.updatePollOption(pollId, option); expect(res, isNotNull); @@ -2298,8 +2418,7 @@ void main() { const pollId = 'test-poll-id'; const optionId = 'test-option-id'; - when(() => api.polls.deletePollOption(pollId, optionId)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.polls.deletePollOption(pollId, optionId)).thenAnswer((_) async => EmptyResponse()); final res = await client.deletePollOption(pollId, optionId); expect(res, isNotNull); @@ -2316,21 +2435,19 @@ void main() { // Custom matcher to check if the Vote object has the specified id Matcher matchesVoteOption(String expected) => predicate( - (vote) => vote.optionId == expected, - 'Vote with option $expected', - ); + (vote) => vote.optionId == expected, + 'Vote with option $expected', + ); - when(() => api.polls.castPollVote( - messageId, pollId, any(that: matchesVoteOption(optionId)))) - .thenAnswer((_) async => CastPollVoteResponse()..vote = vote); + when( + () => api.polls.castPollVote(messageId, pollId, any(that: matchesVoteOption(optionId))), + ).thenAnswer((_) async => CastPollVoteResponse()..vote = vote); - final res = - await client.castPollVote(messageId, pollId, optionId: optionId); + final res = await client.castPollVote(messageId, pollId, optionId: optionId); expect(res, isNotNull); expect(res.vote, vote); - verify(() => api.polls.castPollVote( - messageId, pollId, any(that: matchesVoteOption(optionId)))).called(1); + verify(() => api.polls.castPollVote(messageId, pollId, any(that: matchesVoteOption(optionId)))).called(1); verifyNoMoreInteractions(api.polls); }); @@ -2342,22 +2459,19 @@ void main() { // Custom matcher to check if the Vote object has the specified id Matcher matchesVoteAnswer(String expected) => predicate( - (vote) => vote.answerText == expected, - 'Vote with answer $expected', - ); + (vote) => vote.answerText == expected, + 'Vote with answer $expected', + ); - when(() => api.polls.castPollVote( - messageId, pollId, any(that: matchesVoteAnswer(answerText)))) - .thenAnswer((_) async => CastPollVoteResponse()..vote = vote); + when( + () => api.polls.castPollVote(messageId, pollId, any(that: matchesVoteAnswer(answerText))), + ).thenAnswer((_) async => CastPollVoteResponse()..vote = vote); - final res = - await client.addPollAnswer(messageId, pollId, answerText: answerText); + final res = await client.addPollAnswer(messageId, pollId, answerText: answerText); expect(res, isNotNull); expect(res.vote, vote); - verify(() => api.polls.castPollVote( - messageId, pollId, any(that: matchesVoteAnswer(answerText)))) - .called(1); + verify(() => api.polls.castPollVote(messageId, pollId, any(that: matchesVoteAnswer(answerText)))).called(1); verifyNoMoreInteractions(api.polls); }); @@ -2366,14 +2480,12 @@ void main() { const pollId = 'test-poll-id'; const voteId = 'test-vote-id'; - when(() => api.polls.removePollVote(messageId, pollId, voteId)) - .thenAnswer((_) async => RemovePollVoteResponse()); + when(() => api.polls.removePollVote(messageId, pollId, voteId)).thenAnswer((_) async => RemovePollVoteResponse()); final res = await client.removePollVote(messageId, pollId, voteId); expect(res, isNotNull); - verify(() => api.polls.removePollVote(messageId, pollId, voteId)) - .called(1); + verify(() => api.polls.removePollVote(messageId, pollId, voteId)).called(1); verifyNoMoreInteractions(api.polls); }); @@ -2394,11 +2506,13 @@ void main() { ), ); - when(() => api.polls.queryPolls( - filter: filter, - sort: sort, - pagination: pagination, - )).thenAnswer( + when( + () => api.polls.queryPolls( + filter: filter, + sort: sort, + pagination: pagination, + ), + ).thenAnswer( (_) async => QueryPollsResponse()..polls = polls, ); @@ -2410,11 +2524,13 @@ void main() { expect(res, isNotNull); expect(res.polls.length, polls.length); - verify(() => api.polls.queryPolls( - filter: filter, - sort: sort, - pagination: pagination, - )).called(1); + verify( + () => api.polls.queryPolls( + filter: filter, + sort: sort, + pagination: pagination, + ), + ).called(1); verifyNoMoreInteractions(api.polls); }); @@ -2429,12 +2545,14 @@ void main() { (index) => PollVote(id: 'test-vote-id-$index', answerText: 'Red'), ); - when(() => api.polls.queryPollVotes( - pollId, - filter: filter, - sort: sort, - pagination: pagination, - )).thenAnswer( + when( + () => api.polls.queryPollVotes( + pollId, + filter: filter, + sort: sort, + pagination: pagination, + ), + ).thenAnswer( (_) async => QueryPollVotesResponse()..votes = votes, ); @@ -2447,12 +2565,14 @@ void main() { expect(res, isNotNull); expect(res.votes.length, votes.length); - verify(() => api.polls.queryPollVotes( - pollId, - filter: filter, - sort: sort, - pagination: pagination, - )).called(1); + verify( + () => api.polls.queryPollVotes( + pollId, + filter: filter, + sort: sort, + pagination: pagination, + ), + ).called(1); verifyNoMoreInteractions(api.polls); }); @@ -2462,8 +2582,7 @@ void main() { extraData: const {'name': 'test-user'}, ); - when(() => api.user.updateUsers([user])).thenAnswer( - (_) async => UpdateUsersResponse()..users = {user.id: user}); + when(() => api.user.updateUsers([user])).thenAnswer((_) async => UpdateUsersResponse()..users = {user.id: user}); final res = await client.updateUser(user); @@ -2491,8 +2610,7 @@ void main() { extraData: {'color': set['color']}, ); - when(() => api.user.partialUpdateUsers([partialUpdateRequest])) - .thenAnswer( + when(() => api.user.partialUpdateUsers([partialUpdateRequest])).thenAnswer( (_) async => UpdateUsersResponse() ..users = { updatedUser.id: updatedUser, @@ -2517,8 +2635,9 @@ void main() { test('`.banUser`', () async { const userId = 'test-user-id'; - when(() => api.moderation.banUser(userId, options: any(named: 'options'))) - .thenAnswer((_) async => EmptyResponse()); + when( + () => api.moderation.banUser(userId, options: any(named: 'options')), + ).thenAnswer((_) async => EmptyResponse()); final res = await client.banUser(userId); @@ -2533,9 +2652,9 @@ void main() { test('`.unbanUser`', () async { const userId = 'test-user-id'; - when(() => - api.moderation.unbanUser(userId, options: any(named: 'options'))) - .thenAnswer((_) async => EmptyResponse()); + when( + () => api.moderation.unbanUser(userId, options: any(named: 'options')), + ).thenAnswer((_) async => EmptyResponse()); final res = await client.unbanUser(userId); @@ -2571,8 +2690,7 @@ void main() { test('`.unblockUser`', () async { const userId = 'test-user-id'; - when(() => api.user.unblockUser(userId)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.user.unblockUser(userId)).thenAnswer((_) async => EmptyResponse()); final res = await client.unblockUser(userId); @@ -2712,10 +2830,8 @@ void main() { await client.unblockUser(nonBlockedUserId); // Verify - should remain unchanged - expect(client.state.currentUser?.blockedUserIds, - contains(otherBlockedId)); - expect(client.state.currentUser?.blockedUserIds, - isNot(contains(nonBlockedUserId))); + expect(client.state.currentUser?.blockedUserIds, contains(otherBlockedId)); + expect(client.state.currentUser?.blockedUserIds, isNot(contains(nonBlockedUserId))); verify(() => api.user.unblockUser(nonBlockedUserId)).called(1); verifyNoMoreInteractions(api.user); }, @@ -2864,8 +2980,7 @@ void main() { test('`.shadowBan`', () async { const userId = 'test-user-id'; - when(() => api.moderation.banUser(userId, options: {'shadow': true})) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.moderation.banUser(userId, options: {'shadow': true})).thenAnswer((_) async => EmptyResponse()); final res = await client.shadowBan(userId); @@ -2880,8 +2995,7 @@ void main() { test('`.removeShadowBan`', () async { const userId = 'test-user-id'; - when(() => api.moderation.unbanUser(userId, options: {'shadow': true})) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.moderation.unbanUser(userId, options: {'shadow': true})).thenAnswer((_) async => EmptyResponse()); final res = await client.removeShadowBan(userId); @@ -2896,8 +3010,7 @@ void main() { test('`.muteUser`', () async { const userId = 'test-user-id'; - when(() => api.moderation.muteUser(userId)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.moderation.muteUser(userId)).thenAnswer((_) async => EmptyResponse()); final res = await client.muteUser(userId); @@ -2910,8 +3023,7 @@ void main() { test('`.unmuteUser`', () async { const userId = 'test-user-id'; - when(() => api.moderation.unmuteUser(userId)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.moderation.unmuteUser(userId)).thenAnswer((_) async => EmptyResponse()); final res = await client.unmuteUser(userId); @@ -2924,8 +3036,7 @@ void main() { test('`.flagMessage`', () async { const messageId = 'test-message-id'; - when(() => api.moderation.flagMessage(messageId)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.moderation.flagMessage(messageId)).thenAnswer((_) async => EmptyResponse()); final res = await client.flagMessage(messageId); @@ -2938,8 +3049,7 @@ void main() { test('`.unflagMessage`', () async { const messageId = 'test-message-id'; - when(() => api.moderation.unflagMessage(messageId)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.moderation.unflagMessage(messageId)).thenAnswer((_) async => EmptyResponse()); final res = await client.unflagMessage(messageId); @@ -2952,8 +3062,7 @@ void main() { test('`.flagUser`', () async { const userId = 'test-message-id'; - when(() => api.moderation.flagUser(userId)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.moderation.flagUser(userId)).thenAnswer((_) async => EmptyResponse()); final res = await client.flagUser(userId); @@ -2966,8 +3075,7 @@ void main() { test('`.unflagUser`', () async { const userId = 'test-message-id'; - when(() => api.moderation.unflagUser(userId)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.moderation.unflagUser(userId)).thenAnswer((_) async => EmptyResponse()); final res = await client.unflagUser(userId); @@ -2977,174 +3085,455 @@ void main() { verifyNoMoreInteractions(api.moderation); }); - test('`.markAllRead`', () async { - when(() => api.channel.markAllRead()) - .thenAnswer((_) async => EmptyResponse()); - - final res = await client.markAllRead(); - expect(res, isNotNull); - - verify(() => api.channel.markAllRead()).called(1); - verifyNoMoreInteractions(api.channel); - }); - - test('`.markChannelsDelivered`', () async { - final deliveries = [ - const MessageDelivery( - channelCid: 'messaging:test-channel-1', - messageId: 'test-message-id-1', + test('`.getActiveLiveLocations`', () async { + final locations = [ + Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: 'device-1', + endAt: DateTime.now().add(const Duration(hours: 1)), ), - const MessageDelivery( - channelCid: 'messaging:test-channel-2', - messageId: 'test-message-id-2', + Location( + latitude: 34.0522, + longitude: -118.2437, + createdByDeviceId: 'device-2', + endAt: DateTime.now().add(const Duration(hours: 2)), ), ]; - when(() => api.channel.markChannelsDelivered(deliveries)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.user.getActiveLiveLocations()).thenAnswer( + (_) async => + GetActiveLiveLocationsResponse() // + ..activeLiveLocations = locations, + ); + + // Initial state should be empty + expect(client.state.activeLiveLocations, isEmpty); + + final res = await client.getActiveLiveLocations(); - final res = await client.markChannelsDelivered(deliveries); expect(res, isNotNull); + expect(res.activeLiveLocations, hasLength(2)); + expect(res.activeLiveLocations, equals(locations)); + expect(client.state.activeLiveLocations, equals(locations)); - verify(() => api.channel.markChannelsDelivered(deliveries)).called(1); - verifyNoMoreInteractions(api.channel); + verify(() => api.user.getActiveLiveLocations()).called(1); + verifyNoMoreInteractions(api.user); }); - test('`.sendEvent`', () async { - const channelType = 'test-channel-type'; - const channelId = 'test-channel-id'; - final event = Event(type: EventType.any); + 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.channel.sendEvent( - channelId, - channelType, - any(that: isSameEventAs(event)), + () => api.user.updateLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + location: location, + endAt: endAt, ), - ).thenAnswer((_) async => EmptyResponse()); + ).thenAnswer((_) async => expectedLocation); + + final res = await client.updateLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + location: location, + endAt: endAt, + ); - final res = await client.sendEvent(channelId, channelType, event); expect(res, isNotNull); + expect(res, equals(expectedLocation)); - verify(() => api.channel.sendEvent( - channelId, - channelType, - any(that: isSameEventAs(event)), - )).called(1); - verifyNoMoreInteractions(api.channel); + verify( + () => api.user.updateLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + location: location, + endAt: endAt, + ), + ).called(1); + verifyNoMoreInteractions(api.user); }); - group('`.sendReaction`', () { - test('`.sendReaction with default params`', () async { - const messageId = 'test-message-id'; - const reactionType = 'like'; - const extraData = {'score': 1}; + test('`.stopLiveLocation`', () async { + const messageId = 'test-message-id'; + const createdByDeviceId = 'test-device-id'; - when(() => api.message.sendReaction( - messageId, - reactionType, - extraData: extraData, - )).thenAnswer((_) async => SendReactionResponse() - ..message = Message(id: messageId) - ..reaction = Reaction(type: reactionType, messageId: messageId)); + final expectedLocation = Location( + latitude: 40.7128, + longitude: -74.0060, + createdByDeviceId: createdByDeviceId, + endAt: DateTime.now(), // Should be expired + ); - 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); + when( + () => api.user.updateLiveLocation( + messageId: messageId, + createdByDeviceId: createdByDeviceId, + endAt: any(named: 'endAt'), + ), + ).thenAnswer((_) async => expectedLocation); - verify(() => api.message.sendReaction( - messageId, - reactionType, - extraData: extraData, - )).called(1); - verifyNoMoreInteractions(api.message); + 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('`.sendReaction with score`', () async { - const messageId = 'test-message-id'; - const reactionType = 'like'; - const score = 3; - const extraData = {'score': score}; + 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)), + ); - when(() => api.message.sendReaction( - messageId, - reactionType, - extraData: extraData, - )).thenAnswer((_) async => SendReactionResponse() - ..message = Message(id: messageId) - ..reaction = Reaction( - type: reactionType, - messageId: messageId, - score: score, - )); + // 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 res = await client.sendReaction( - messageId, - reactionType, - score: score, + final event = Event( + type: EventType.locationUpdated, + cid: 'test-channel:123', + message: Message( + id: 'message-123', + sharedLocation: updatedLocation, + ), ); - 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); + // 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('`.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}; + 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)), + ); - when(() => api.message.sendReaction( - messageId, - reactionType, - extraData: extraData, - )).thenAnswer((_) async => SendReactionResponse() - ..message = Message(id: messageId) - ..reaction = Reaction( - type: reactionType, - messageId: messageId, - score: extraDataScore, - )); + // Set initial location + client.state.activeLiveLocations = [location]; + expect(client.state.activeLiveLocations, hasLength(1)); - final res = await client.sendReaction( - messageId, - reactionType, - score: score, - extraData: extraData, + final expiredLocation = location.copyWith( + endAt: DateTime.now().subtract(const Duration(hours: 1)), ); - expect(res, isNotNull); - expect(res.message.id, messageId); - expect(res.reaction.type, reactionType); - expect(res.reaction.messageId, messageId); - expect(res.reaction.score, extraDataScore); - verify(() => api.message.sendReaction( - messageId, - reactionType, - extraData: extraData, - )).called(1); - verifyNoMoreInteractions(api.message); + 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()); + + final res = await client.markAllRead(); + expect(res, isNotNull); + + verify(() => api.channel.markAllRead()).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.markChannelsDelivered`', () async { + final deliveries = [ + const MessageDelivery( + channelCid: 'messaging:test-channel-1', + messageId: 'test-message-id-1', + ), + const MessageDelivery( + channelCid: 'messaging:test-channel-2', + messageId: 'test-message-id-2', + ), + ]; + + when(() => api.channel.markChannelsDelivered(deliveries)).thenAnswer((_) async => EmptyResponse()); + + final res = await client.markChannelsDelivered(deliveries); + expect(res, isNotNull); + + verify(() => api.channel.markChannelsDelivered(deliveries)).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.sendEvent`', () async { + const channelType = 'test-channel-type'; + const channelId = 'test-channel-id'; + final event = Event(type: EventType.any); + + when( + () => api.channel.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(event)), + ), + ).thenAnswer((_) async => EmptyResponse()); + + final res = await client.sendEvent(channelId, channelType, event); + expect(res, isNotNull); + + verify( + () => api.channel.sendEvent( + channelId, + channelType, + any(that: isSameEventAs(event)), + ), + ).called(1); + verifyNoMoreInteractions(api.channel); + }); + + test('`.sendReaction`', () async { + const messageId = 'test-message-id'; + const reactionType = 'like'; + const emojiCode = '👍'; + const score = 4; + + final reaction = Reaction( + type: reactionType, + messageId: messageId, + emojiCode: emojiCode, + score: score, + ); + + when(() => api.message.sendReaction(messageId, reaction)).thenAnswer( + (_) async => SendReactionResponse() + ..message = Message(id: messageId) + ..reaction = reaction, + ); + + 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, reaction)).called(1); + verifyNoMoreInteractions(api.message); + }); + test('`.deleteReaction`', () async { const messageId = 'test-message-id'; const reactionType = 'like'; - when(() => api.message.deleteReaction(messageId, reactionType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.message.deleteReaction(messageId, reactionType)).thenAnswer((_) async => EmptyResponse()); final res = await client.deleteReaction(messageId, reactionType); expect(res, isNotNull); @@ -3160,19 +3549,21 @@ void main() { const channelId = 'test-channel-id'; const channelType = 'test-channel-type'; - when(() => api.message.sendMessage( - channelId, channelType, any(that: isSameMessageAs(message)))) - .thenAnswer((_) async => SendMessageResponse()..message = message); + when( + () => api.message.sendMessage(channelId, channelType, any(that: isSameMessageAs(message))), + ).thenAnswer((_) async => SendMessageResponse()..message = message); final res = await client.sendMessage(message, channelId, channelType); expect(res, isNotNull); expect(res.message, isSameMessageAs(message)); - verify(() => api.message.sendMessage( - channelId, - channelType, - any(that: isSameMessageAs(message)), - )).called(1); + verify( + () => api.message.sendMessage( + channelId, + channelType, + any(that: isSameMessageAs(message)), + ), + ).called(1); verifyNoMoreInteractions(api.message); }); @@ -3205,11 +3596,13 @@ void main() { expect(res, isNotNull); expect(res.draft.message, isSameDraftMessageAs(message)); - verify(() => api.message.createDraft( - channelId, - channelType, - any(that: isSameDraftMessageAs(message)), - )).called(1); + verify( + () => api.message.createDraft( + channelId, + channelType, + any(that: isSameDraftMessageAs(message)), + ), + ).called(1); verifyNoMoreInteractions(api.message); }); @@ -3218,8 +3611,7 @@ void main() { const channelId = 'test-channel-id'; const channelType = 'test-channel-type'; - when(() => api.message.deleteDraft(channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.message.deleteDraft(channelId, channelType)).thenAnswer((_) async => EmptyResponse()); final res = await client.deleteDraft(channelId, channelType); expect(res, isNotNull); @@ -3234,13 +3626,14 @@ void main() { final message = DraftMessage(id: 'test-message-id', text: 'Hello!'); - when(() => api.message.getDraft(channelId, channelType)) - .thenAnswer((_) async => GetDraftResponse() - ..draft = Draft( - channelCid: '$channelType:$channelId', - createdAt: DateTime.now(), - message: message, - )); + when(() => api.message.getDraft(channelId, channelType)).thenAnswer( + (_) async => GetDraftResponse() + ..draft = Draft( + channelCid: '$channelType:$channelId', + createdAt: DateTime.now(), + message: message, + ), + ); final res = await client.getDraft(channelId, channelType); @@ -3260,11 +3653,10 @@ void main() { channelCid: '$channelType:$channelId', createdAt: DateTime.now(), message: DraftMessage(id: 'test-message-id', text: 'Hello!'), - ) + ), ]; - when(() => api.message.queryDrafts()) - .thenAnswer((_) async => QueryDraftsResponse()..drafts = drafts); + when(() => api.message.queryDrafts()).thenAnswer((_) async => QueryDraftsResponse()..drafts = drafts); final res = await client.queryDrafts(); @@ -3283,8 +3675,7 @@ void main() { (index) => Message(id: 'test-message-id-$index'), ); - when(() => api.message.getReplies(parentId)) - .thenAnswer((_) async => QueryRepliesResponse()..messages = messages); + when(() => api.message.getReplies(parentId)).thenAnswer((_) async => QueryRepliesResponse()..messages = messages); final res = await client.getReplies(parentId); expect(res, isNotNull); @@ -3305,8 +3696,9 @@ void main() { ), ); - when(() => api.message.getReactions(messageId)).thenAnswer( - (_) async => QueryReactionsResponse()..reactions = reactions); + when( + () => api.message.getReactions(messageId), + ).thenAnswer((_) async => QueryReactionsResponse()..reactions = reactions); final res = await client.getReactions(messageId); expect(res, isNotNull); @@ -3317,11 +3709,40 @@ void main() { verifyNoMoreInteractions(api.message); }); + test('`.queryReactions`', () async { + const messageId = 'test-message-id'; + + final reactions = List.generate( + 3, + (index) => Reaction( + type: 'test-reactions-type-$index', + messageId: messageId, + ), + ); + + when( + () => api.message.queryReactions(messageId), + ).thenAnswer( + (_) async => QueryReactionsResponse() + ..reactions = reactions + ..next = null, + ); + + final res = await client.queryReactions(messageId); + expect(res, isNotNull); + expect(res.reactions.length, reactions.length); + expect(res.reactions.every((it) => it.messageId == messageId), isTrue); + + verify(() => api.message.queryReactions(messageId)).called(1); + verifyNoMoreInteractions(api.message); + }); + test('`.updateMessage`', () async { final message = Message(id: 'test-message-id', text: 'Hello!'); - when(() => api.message.updateMessage(any(that: isSameMessageAs(message)))) - .thenAnswer((_) async => UpdateMessageResponse()..message = message); + when( + () => api.message.updateMessage(any(that: isSameMessageAs(message))), + ).thenAnswer((_) async => UpdateMessageResponse()..message = message); final res = await client.updateMessage(message); expect(res, isNotNull); @@ -3336,8 +3757,7 @@ void main() { test('`.deleteMessage`', () async { const messageId = 'test-message-id'; - when(() => api.message.deleteMessage(messageId, hard: false)) - .thenAnswer((_) async => EmptyResponse()); + when(() => api.message.deleteMessage(messageId, hard: false)).thenAnswer((_) async => EmptyResponse()); final res = await client.deleteMessage(messageId); expect(res, isNotNull); @@ -3346,12 +3766,23 @@ void main() { verifyNoMoreInteractions(api.message); }); + test('`.deleteMessageForMe`', () async { + const messageId = 'test-message-id'; + + when(() => api.message.deleteMessage(messageId, deleteForMe: true)).thenAnswer((_) async => EmptyResponse()); + + final res = await client.deleteMessageForMe(messageId); + expect(res, isNotNull); + + verify(() => api.message.deleteMessage(messageId, deleteForMe: true)).called(1); + verifyNoMoreInteractions(api.message); + }); + test('`.getMessage`', () async { const messageId = 'test-message-id'; final message = Message(id: messageId); - when(() => api.message.getMessage(messageId)) - .thenAnswer((_) async => GetMessageResponse()..message = message); + when(() => api.message.getMessage(messageId)).thenAnswer((_) async => GetMessageResponse()..message = message); final res = await client.getMessage(messageId); expect(res, isNotNull); @@ -3419,11 +3850,13 @@ void main() { final updateMessageResponse = UpdateMessageResponse() ..message = message.copyWith(text: set['text'], pinExpires: null); - when(() => api.message.partialUpdateMessage( - message.id, - set: set, - unset: unset, - )).thenAnswer((_) async => updateMessageResponse); + when( + () => api.message.partialUpdateMessage( + message.id, + set: set, + unset: unset, + ), + ).thenAnswer((_) async => updateMessageResponse); final res = await client.partialUpdateMessage( messageId, @@ -3437,30 +3870,35 @@ void main() { expect(res.message.text, set['text']); expect(res.message.pinExpires, isNull); - verify(() => api.message.partialUpdateMessage( - message.id, - set: set, - unset: unset, - )).called(1); + verify( + () => api.message.partialUpdateMessage( + message.id, + set: set, + unset: unset, + ), + ).called(1); verifyNoMoreInteractions(api.message); }); group('`.pinMessage`', () { - test('should work fine without passing timeoutOrExpirationDate', - () async { + test('should work fine without passing timeoutOrExpirationDate', () async { const messageId = 'test-message-id'; final message = Message(id: messageId); - when(() => api.message.partialUpdateMessage( - messageId, - set: any(named: 'set'), - unset: any(named: 'unset'), - )).thenAnswer((_) async => UpdateMessageResponse() - ..message = message.copyWith( - pinned: true, - pinExpires: null, - state: MessageState.sent, - )); + when( + () => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + ), + ).thenAnswer( + (_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: true, + pinExpires: null, + state: MessageState.sent, + ), + ); final res = await client.pinMessage(messageId); @@ -3468,11 +3906,13 @@ void main() { expect(res.message.pinned, isTrue); expect(res.message.pinExpires, isNull); - verify(() => api.message.partialUpdateMessage( - messageId, - set: any(named: 'set'), - unset: any(named: 'unset'), - )).called(1); + verify( + () => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + ), + ).called(1); verifyNoMoreInteractions(api.message); }); @@ -3483,18 +3923,22 @@ void main() { final message = Message(id: messageId); const timeoutOrExpirationDate = 300; // 300 seconds - when(() => api.message.partialUpdateMessage( - message.id, - set: any(named: 'set'), - unset: any(named: 'unset'), - )).thenAnswer((_) async => UpdateMessageResponse() - ..message = message.copyWith( - pinned: true, - pinExpires: DateTime.now().add( - const Duration(seconds: timeoutOrExpirationDate), + when( + () => api.message.partialUpdateMessage( + message.id, + set: any(named: 'set'), + unset: any(named: 'unset'), + ), + ).thenAnswer( + (_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: true, + pinExpires: DateTime.now().add( + const Duration(seconds: timeoutOrExpirationDate), + ), + state: MessageState.sent, ), - state: MessageState.sent, - )); + ); final res = await client.pinMessage( messageId, @@ -3505,11 +3949,13 @@ void main() { expect(res.message.pinned, isTrue); expect(res.message.pinExpires, isNotNull); - verify(() => api.message.partialUpdateMessage( - messageId, - set: any(named: 'set'), - unset: any(named: 'unset'), - )).called(1); + verify( + () => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + ), + ).called(1); verifyNoMoreInteractions(api.message); }, ); @@ -3519,19 +3965,22 @@ void main() { () async { const messageId = 'test-message-id'; final message = Message(id: messageId); - final timeoutOrExpirationDate = - DateTime.now().add(const Duration(days: 3)); // 3 days - - when(() => api.message.partialUpdateMessage( - messageId, - set: any(named: 'set'), - unset: any(named: 'unset'), - )).thenAnswer((_) async => UpdateMessageResponse() - ..message = message.copyWith( - pinned: true, - pinExpires: timeoutOrExpirationDate, - state: MessageState.sent, - )); + final timeoutOrExpirationDate = DateTime.now().add(const Duration(days: 3)); // 3 days + + when( + () => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + ), + ).thenAnswer( + (_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: true, + pinExpires: timeoutOrExpirationDate, + state: MessageState.sent, + ), + ); final res = await client.pinMessage( messageId, @@ -3543,11 +3992,13 @@ void main() { expect(res.message.pinExpires, isNotNull); expect(res.message.pinExpires, timeoutOrExpirationDate.toUtc()); - verify(() => api.message.partialUpdateMessage( - messageId, - set: any(named: 'set'), - unset: any(named: 'unset'), - )).called(1); + verify( + () => api.message.partialUpdateMessage( + messageId, + set: any(named: 'set'), + unset: any(named: 'unset'), + ), + ).called(1); verifyNoMoreInteractions(api.message); }, ); @@ -3574,30 +4025,35 @@ void main() { const messageId = 'test-message-id'; final message = Message(id: messageId, pinned: true); - when(() => api.message.partialUpdateMessage( - messageId, - set: {'pinned': false}, - )).thenAnswer((_) async => UpdateMessageResponse() - ..message = message.copyWith( - pinned: false, - state: MessageState.sent, - )); + when( + () => api.message.partialUpdateMessage( + messageId, + set: {'pinned': false}, + ), + ).thenAnswer( + (_) async => UpdateMessageResponse() + ..message = message.copyWith( + pinned: false, + state: MessageState.sent, + ), + ); final res = await client.unpinMessage(messageId); expect(res, isNotNull); expect(res.message.pinned, isFalse); - verify(() => api.message.partialUpdateMessage( - messageId, - set: {'pinned': false}, - )).called(1); + verify( + () => api.message.partialUpdateMessage( + messageId, + set: {'pinned': false}, + ), + ).called(1); verifyNoMoreInteractions(api.message); }); test('`.enrichUrl`', () async { - const url = - 'https://www.techyourchance.com/finite-state-machine-with-unit-tests-real-world-example'; + const url = 'https://www.techyourchance.com/finite-state-machine-with-unit-tests-real-world-example'; when(() => api.general.enrichUrl(url)).thenAnswer( (_) async => OGAttachmentResponse() @@ -3828,4 +4284,162 @@ void main() { }); }); }); + + group('WS events', () { + late StreamChatClient client; + + setUp(() async { + final ws = FakeWebSocket(); + client = StreamChatClient('test-api-key', ws: ws); + + final user = User(id: 'test-user-id'); + final token = Token.development(user.id).rawValue; + + await client.connectUser(user, token); + await delay(300); + expect(client.wsConnectionStatus, ConnectionStatus.connected); + }); + + tearDown(() async { + await client.dispose(); + }); + + group('User messages deleted event', () { + test( + 'should broadcast global user.messages.deleted event to all channels', + () async { + // Add messages from the user to be deleted + final bannedUser = User(id: 'banned-user', name: 'Banned User'); + final message1 = Message( + id: 'msg-1', + text: 'Message in channel 1', + user: bannedUser, + ); + final message2 = Message( + id: 'msg-2', + text: 'Message in channel 2', + user: bannedUser, + ); + + // Setup: Create multiple channels with state + final channelState1 = ChannelState( + channel: ChannelModel(id: 'channel-1', type: 'messaging'), + messages: [message1], + ); + final channelState2 = ChannelState( + channel: ChannelModel(id: 'channel-2', type: 'messaging'), + messages: [message2], + ); + + final channel1 = Channel.fromState(client, channelState1); + final channel2 = Channel.fromState(client, channelState2); + + // Register channels in client state + client.state.addChannels({ + 'messaging:channel-1': channel1, + 'messaging:channel-2': channel2, + }); + + // Verify initial state + expect(channel1.state?.messages.length, equals(1)); + expect(channel2.state?.messages.length, equals(1)); + + // Simulate global user.messages.deleted event being broadcast to channels + // (In production, ClientState._listenUserMessagesDeleted does this) + final event = Event( + type: EventType.userMessagesDeleted, + user: bannedUser, + hardDelete: false, + ); + + client.handleEvent(event); + + // Wait for the events to be processed + await Future.delayed(Duration.zero); + + // Verify messages are soft deleted in all channels + final channel1Message = channel1.state?.messages.first; + expect(channel1Message?.type, equals(MessageType.deleted)); + expect(channel1Message?.state.isDeleted, isTrue); + + final channel2Message = channel2.state?.messages.first; + expect(channel2Message?.type, equals(MessageType.deleted)); + expect(channel2Message?.state.isDeleted, isTrue); + }, + ); + + test( + 'should broadcast global hard delete to all channels', + () async { + // Add messages from the user to be deleted + final bannedUser = User(id: 'banned-user', name: 'Banned User'); + final otherUser = User(id: 'other-user', name: 'Other User'); + + final message1 = Message( + id: 'msg-1', + text: 'Message in channel 1', + user: bannedUser, + ); + final message2 = Message( + id: 'msg-2', + text: 'Message in channel 2', + user: bannedUser, + ); + final message3 = Message( + id: 'msg-3', + text: 'Safe message', + user: otherUser, + ); + + // Setup: Create multiple channels with state + final channelState1 = ChannelState( + channel: ChannelModel(id: 'channel-1', type: 'messaging'), + messages: [message1, message3], + ); + final channelState2 = ChannelState( + channel: ChannelModel(id: 'channel-2', type: 'messaging'), + messages: [message2], + ); + + final channel1 = Channel.fromState(client, channelState1); + final channel2 = Channel.fromState(client, channelState2); + + // Register channels in client state + client.state.addChannels({ + 'messaging:channel-1': channel1, + 'messaging:channel-2': channel2, + }); + + // Verify initial state + expect(channel1.state?.messages.length, equals(2)); + expect(channel2.state?.messages.length, equals(1)); + + // Simulate global user.messages.deleted event being broadcast to channels + // (In production, ClientState._listenUserMessagesDeleted does this) + final event = Event( + type: EventType.userMessagesDeleted, + user: bannedUser, + hardDelete: true, + ); + + client.handleEvent(event); + + // Wait for the events to be processed + await Future.delayed(Duration.zero); + + // Verify banned user's messages are removed from all channels + expect(channel1.state?.messages.length, equals(1)); + expect( + channel1.state?.messages.any((m) => m.user?.id == 'banned-user'), + isFalse, + ); + expect(channel2.state?.messages.length, equals(0)); + + // Verify other user's message is unaffected + final safeMessage = channel1.state?.messages.firstWhere((m) => m.id == 'msg-3'); + expect(safeMessage?.user?.id, equals('other-user')); + }, + ); + }); + }); } 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..0aed5d35a7 --- /dev/null +++ b/packages/stream_chat/test/src/client/event_resolvers_test.dart @@ -0,0 +1,665 @@ +// 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 null for event with null pollVote', () { + final event = Event( + type: EventType.pollVoteCasted, + pollVote: null, + cid: 'channel-123', + ); + + final resolved = pollAnswerCastedResolver(event); + + expect(resolved, 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 null for event with null pollVote', () { + final event = Event( + type: EventType.pollVoteRemoved, + pollVote: null, + cid: 'channel-123', + ); + + final resolved = pollAnswerRemovedResolver(event); + + expect(resolved, 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/attachment_file_uploader_test.dart b/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart index 58ffdefb69..1b319718e1 100644 --- a/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart +++ b/packages/stream_chat/test/src/core/api/attachment_file_uploader_test.dart @@ -19,10 +19,10 @@ void main() { }); Response successResponse(String path, {Object? data}) => Response( - data: data, - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); test('sendImage', () async { const channelId = 'test-channel-id'; @@ -37,12 +37,19 @@ void main() { ); final multipartFile = await attachmentFile.toMultipartFile(); - when(() => client.postFile( - path, - any(that: isSameMultipartFileAs(multipartFile)), - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'file': 'test-file-url', - })); + }, + ), + ); final res = await fileUploader.sendImage( attachmentFile, @@ -54,10 +61,12 @@ void main() { expect(res.file, isNotNull); expect(res.file, isNotEmpty); - verify(() => client.postFile( - path, - any(that: isSameMultipartFileAs(multipartFile)), - )).called(1); + verify( + () => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + ), + ).called(1); verifyNoMoreInteractions(client); }); @@ -74,12 +83,19 @@ void main() { ); final multipartFile = await attachmentFile.toMultipartFile(); - when(() => client.postFile( - path, - any(that: isSameMultipartFileAs(multipartFile)), - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'file': 'test-file-url', - })); + }, + ), + ); final res = await fileUploader.sendFile( attachmentFile, @@ -91,10 +107,12 @@ void main() { expect(res.file, isNotNull); expect(res.file, isNotEmpty); - verify(() => client.postFile( - path, - any(that: isSameMultipartFileAs(multipartFile)), - )).called(1); + verify( + () => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + ), + ).called(1); verifyNoMoreInteractions(client); }); @@ -105,8 +123,9 @@ void main() { const url = 'test-image-url'; - when(() => client.delete(path, queryParameters: {'url': url})).thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.delete(path, queryParameters: {'url': url}), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await fileUploader.deleteImage(url, channelId, channelType); @@ -123,8 +142,9 @@ void main() { const url = 'test-file-url'; - when(() => client.delete(path, queryParameters: {'url': url})).thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.delete(path, queryParameters: {'url': url}), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await fileUploader.deleteFile(url, channelId, channelType); @@ -133,4 +153,114 @@ void main() { verify(() => client.delete(path, queryParameters: {'url': url})).called(1); verifyNoMoreInteractions(client); }); + + test('uploadImage', () async { + const path = '/uploads/image'; + final file = assetFile('test_image.jpeg'); + final attachmentFile = AttachmentFile( + size: 333, + path: file.path, + bytes: file.readAsBytesSync(), + ); + final multipartFile = await attachmentFile.toMultipartFile(); + + when( + () => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'file': 'test-image-url', + }, + ), + ); + + final res = await fileUploader.uploadImage(attachmentFile); + + expect(res, isNotNull); + expect(res.file, isNotNull); + expect(res.file, isNotEmpty); + + verify( + () => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + ), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('uploadFile', () async { + const path = '/uploads/file'; + final file = assetFile('example.pdf'); + final attachmentFile = AttachmentFile( + size: 333, + path: file.path, + bytes: file.readAsBytesSync(), + ); + final multipartFile = await attachmentFile.toMultipartFile(); + + when( + () => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'file': 'test-file-url', + }, + ), + ); + + final res = await fileUploader.uploadFile(attachmentFile); + + expect(res, isNotNull); + expect(res.file, isNotNull); + expect(res.file, isNotEmpty); + + verify( + () => client.postFile( + path, + any(that: isSameMultipartFileAs(multipartFile)), + ), + ).called(1); + verifyNoMoreInteractions(client); + }); + + test('removeImage', () async { + const path = '/uploads/image'; + const url = 'test-image-url'; + + when( + () => client.delete(path, queryParameters: {'url': url}), + ).thenAnswer((_) async => successResponse(path, data: {})); + + final res = await fileUploader.removeImage(url); + + expect(res, isNotNull); + + verify(() => client.delete(path, queryParameters: {'url': url})).called(1); + verifyNoMoreInteractions(client); + }); + + test('removeFile', () async { + const path = '/uploads/file'; + const url = 'test-file-url'; + + when( + () => client.delete(path, queryParameters: {'url': url}), + ).thenAnswer((_) async => successResponse(path, data: {})); + + final res = await fileUploader.removeFile(url); + + expect(res, isNotNull); + + verify(() => client.delete(path, queryParameters: {'url': url})).called(1); + verifyNoMoreInteractions(client); + }); } 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/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index ae5a0dba81..1c4ae95c13 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -9,8 +9,7 @@ import 'package:test/test.dart'; import '../../mocks.dart'; void main() { - String _getChannelUrl(String channelId, String channelType) => - '/channels/$channelType/$channelId'; + String _getChannelUrl(String channelId, String channelType) => '/channels/$channelType/$channelId'; ChannelState _generateChannelState( String channelId, @@ -53,10 +52,10 @@ void main() { } Response successResponse(String path, {Object? data}) => Response( - data: data, - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); late final client = MockHttpClient(); late ChannelApi channelApi; @@ -88,13 +87,17 @@ void main() { 'watchers': watchersPagination, }; - when(() => client.post( - path, - data: data, - )).thenAnswer((_) async => successResponse( - path, - data: channelState.toJson(), - )); + when( + () => client.post( + path, + data: data, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: channelState.toJson(), + ), + ); final res = await channelApi.queryChannel( channelType, @@ -143,20 +146,24 @@ void main() { 'message_limit': messageLimit, // pagination - ...const PaginationParams().toJson() + ...const PaginationParams().toJson(), }); - when(() => client.get( - path, - queryParameters: { - 'payload': payload, - }, - )).thenAnswer((_) async => successResponse( - path, - data: { - 'channels': [channelState.toJson()] - }, - )); + when( + () => client.get( + path, + queryParameters: { + 'payload': payload, + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'channels': [channelState.toJson()], + }, + ), + ); final res = await channelApi.queryChannels( filter: filter, @@ -176,8 +183,7 @@ void main() { test('markAllRead', () async { const path = '/channels/read'; - when(() => client.post(path, data: {})).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post(path, data: {})).thenAnswer((_) async => successResponse(path, data: {})); final res = await channelApi.markAllRead(); @@ -201,18 +207,23 @@ void main() { extraData: data, ); - when(() => client.post( - path, - data: any( - named: 'data', - that: wrapMatcher((Map v) => - containsPair('data', data).matches(v, {}) && - contains('message').matches(v, {})), - ), - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: any( + named: 'data', + that: wrapMatcher((Map v) => containsPair('data', data).matches(v, {}) && contains('message').matches(v, {})), + ), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'channel': channelModel.toJson(), 'message': message.toJson(), - })); + }, + ), + ); final res = await channelApi.updateChannel( channelId, @@ -249,9 +260,14 @@ void main() { when( () => client.patch(path, data: {'set': set, 'unset': unset}), - ).thenAnswer((_) async => successResponse(path, data: { + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'channel': channelModel.toJson(), - })); + }, + ), + ); final res = await channelApi.updateChannelPartial( channelId, @@ -277,16 +293,23 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.post( - path, - data: { - 'accept_invite': true, - 'message': message, - }, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: { + 'accept_invite': true, + 'message': message, + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'channel': channelModel.toJson(), 'message': message.toJson(), - })); + }, + ), + ); final res = await channelApi.acceptChannelInvite( channelId, @@ -311,16 +334,23 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.post( - path, - data: { - 'reject_invite': true, - 'message': message, - }, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: { + 'reject_invite': true, + 'message': message, + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'channel': channelModel.toJson(), 'message': message.toJson(), - })); + }, + ), + ); final res = await channelApi.rejectChannelInvite( channelId, @@ -345,16 +375,23 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.post( - path, - data: { - 'invites': memberIds, - 'message': message, - }, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: { + 'invites': memberIds, + 'message': message, + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'channel': channelModel.toJson(), 'message': message.toJson(), - })); + }, + ), + ); final res = await channelApi.inviteChannelMembers( channelId, @@ -381,17 +418,24 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.post( - path, - data: { - 'add_members': memberIds, - 'message': message, - 'hide_history': hideHistory, - }, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: { + 'add_members': memberIds, + 'message': message, + 'hide_history': hideHistory, + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'channel': channelModel.toJson(), 'message': message.toJson(), - })); + }, + ), + ); final res = await channelApi.addMembers( channelId, @@ -419,17 +463,24 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.post( - path, - data: { - 'add_members': memberIds, - 'message': message, - 'hide_history_before': hideHistoryBefore.toUtc().toIso8601String(), - }, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: { + 'add_members': memberIds, + 'message': message, + 'hide_history_before': hideHistoryBefore.toUtc().toIso8601String(), + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'channel': channelModel.toJson(), 'message': message.toJson(), - })); + }, + ), + ); final res = await channelApi.addMembers( channelId, @@ -447,8 +498,7 @@ void main() { verifyNoMoreInteractions(client); }); - test('addMembers with hideHistoryBefore takes precedence over hideHistory', - () async { + test('addMembers with hideHistoryBefore takes precedence over hideHistory', () async { const channelId = 'test-channel-id'; const channelType = 'test-channel-type'; const memberIds = ['test-member-id-1', 'test-member-id-2']; @@ -459,17 +509,24 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.post( - path, - data: { - 'add_members': memberIds, - 'message': message, - 'hide_history_before': hideHistoryBefore.toUtc().toIso8601String(), - }, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: { + 'add_members': memberIds, + 'message': message, + 'hide_history_before': hideHistoryBefore.toUtc().toIso8601String(), + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'channel': channelModel.toJson(), 'message': message.toJson(), - })); + }, + ), + ); final res = await channelApi.addMembers( channelId, @@ -497,16 +554,23 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.post( - path, - data: { - 'remove_members': memberIds, - 'message': message, - }, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: { + 'remove_members': memberIds, + 'message': message, + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'channel': channelModel.toJson(), 'message': message.toJson(), - })); + }, + ), + ); final res = await channelApi.removeMembers( channelId, @@ -530,8 +594,9 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/event'; - when(() => client.post(path, data: {'event': event})).thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post(path, data: {'event': event}), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await channelApi.sendEvent(channelId, channelType, event); @@ -547,8 +612,7 @@ void main() { final path = _getChannelUrl(channelId, channelType); - when(() => client.delete(path)).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.delete(path)).thenAnswer((_) async => successResponse(path, data: {})); final res = await channelApi.deleteChannel(channelId, channelType); @@ -564,21 +628,23 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/truncate'; - when(() => client.post( - path, - data: {}, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: {}, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await channelApi.truncateChannel(channelId, channelType); expect(res, isNotNull); - verify(() => client.post( - path, - data: {}, - )).called(1); + verify( + () => client.post( + path, + data: {}, + ), + ).called(1); verifyNoMoreInteractions(client); }); @@ -638,21 +704,23 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/show'; - when(() => client.post( - path, - data: {}, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: {}, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await channelApi.showChannel(channelId, channelType); expect(res, isNotNull); - verify(() => client.post( - path, - data: {}, - )).called(1); + verify( + () => client.post( + path, + data: {}, + ), + ).called(1); verifyNoMoreInteractions(client); }); @@ -663,14 +731,14 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/read'; - when(() => client.post( - path, - data: { - 'message_id': messageId, - }, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: { + 'message_id': messageId, + }, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await channelApi.markRead( channelId, @@ -691,12 +759,12 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/unread'; - when(() => client.post( - path, - data: {'message_id': messageId}, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: {'message_id': messageId}, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await channelApi.markUnread( channelId, @@ -717,14 +785,14 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/unread'; - when(() => client.post( - path, - data: { - 'message_timestamp': timestamp.toUtc().toIso8601String(), - }, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: { + 'message_timestamp': timestamp.toUtc().toIso8601String(), + }, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await channelApi.markUnreadByTimestamp( channelId, @@ -745,17 +813,22 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/member/{user_id}'; const archivedAt = '2025-04-10 10:27:03.150349'; - when(() => client.patch( - path, - data: { - 'set': {'archived': true}, + when( + () => client.patch( + path, + data: { + 'set': {'archived': true}, + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'channel_member': { + 'archived_at': archivedAt, }, - )).thenAnswer( - (_) async => successResponse(path, data: { - 'channel_member': { - 'archived_at': archivedAt, - } - }), + }, + ), ); final res = await channelApi.updateMemberPartial( @@ -777,14 +850,15 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/member/{user_id}'; - when(() => client.patch( - path, - data: { - 'unset': ['archived'], - }, - )).thenAnswer( - (_) async => successResponse(path, - data: {'channel_member': {}}), + when( + () => client.patch( + path, + data: { + 'unset': ['archived'], + }, + ), + ).thenAnswer( + (_) async => successResponse(path, data: {'channel_member': {}}), ); final res = await channelApi.updateMemberPartial( @@ -807,17 +881,22 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/member/{user_id}'; const pinnedAt = '2025-04-10 10:27:03.150349'; - when(() => client.patch( - path, - data: { - 'set': {'pinned': true}, + when( + () => client.patch( + path, + data: { + 'set': {'pinned': true}, + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'channel_member': { + 'pinned_at': pinnedAt, }, - )).thenAnswer( - (_) async => successResponse(path, data: { - 'channel_member': { - 'pinned_at': pinnedAt, - } - }), + }, + ), ); final res = await channelApi.updateMemberPartial( @@ -839,14 +918,15 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/member/{user_id}'; - when(() => client.patch( - path, - data: { - 'unset': ['pinned'], - }, - )).thenAnswer( - (_) async => successResponse(path, - data: {'channel_member': {}}), + when( + () => client.patch( + path, + data: { + 'unset': ['pinned'], + }, + ), + ).thenAnswer( + (_) async => successResponse(path, data: {'channel_member': {}}), ); final res = await channelApi.updateMemberPartial( @@ -868,8 +948,7 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/stop-watching'; - when(() => client.post(path, data: {})).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post(path, data: {})).thenAnswer((_) async => successResponse(path, data: {})); final res = await channelApi.stopWatching(channelId, channelType); @@ -895,20 +974,34 @@ void main() { extraData: set, ); - when(() => client.patch(path, data: { + when( + () => client.patch( + path, + data: { 'set': set, - })).thenAnswer((_) async => successResponse(path, data: { + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'channel': channelModel.toJson(), - })); + }, + ), + ); - final res = - await channelApi.enableSlowdown(channelId, channelType, cooldown); + final res = await channelApi.enableSlowdown(channelId, channelType, cooldown); expect(res, isNotNull); - verify(() => client.patch(path, data: { + verify( + () => client.patch( + path, + data: { 'set': set, - })).called(1); + }, + ), + ).called(1); verifyNoMoreInteractions(client); }); @@ -924,19 +1017,34 @@ void main() { type: channelType, ); - when(() => client.patch(path, data: { + when( + () => client.patch( + path, + data: { 'unset': unset, - })).thenAnswer((_) async => successResponse(path, data: { + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'channel': channelModel.toJson(), - })); + }, + ), + ); final res = await channelApi.disableSlowdown(channelId, channelType); expect(res, isNotNull); - verify(() => client.patch(path, data: { + verify( + () => client.patch( + path, + data: { 'unset': unset, - })).called(1); + }, + ), + ).called(1); verifyNoMoreInteractions(client); }); @@ -954,24 +1062,30 @@ void main() { ), ]; - when(() => client.post( - path, - data: any(named: 'data'), - )).thenAnswer((_) async => successResponse( - path, - data: {}, - )); + when( + () => client.post( + path, + data: any(named: 'data'), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: {}, + ), + ); final res = await channelApi.markChannelsDelivered(deliveries); expect(res, isNotNull); - verify(() => client.post( - path, - data: jsonEncode({ - 'latest_delivered_messages': deliveries, - }), - )).called(1); + verify( + () => client.post( + path, + data: jsonEncode({ + 'latest_delivered_messages': deliveries, + }), + ), + ).called(1); verifyNoMoreInteractions(client); }); } diff --git a/packages/stream_chat/test/src/core/api/device_api_test.dart b/packages/stream_chat/test/src/core/api/device_api_test.dart index c9d06a843d..8318d8dda6 100644 --- a/packages/stream_chat/test/src/core/api/device_api_test.dart +++ b/packages/stream_chat/test/src/core/api/device_api_test.dart @@ -8,10 +8,10 @@ import '../../mocks.dart'; void main() { Response successResponse(String path, {Object? data}) => Response( - data: data, - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); late final client = MockHttpClient(); late DeviceApi deviceApi; @@ -40,19 +40,16 @@ void main() { path, data: data, ); - }).thenAnswer( - (_) async => successResponse(path, data: {})); + }).thenAnswer((_) async => successResponse(path, data: {})); - final res = - await deviceApi.addDevice(deviceId, pushProviderMapEntry.value); + final res = await deviceApi.addDevice(deviceId, pushProviderMapEntry.value); expect(res, isNotNull); verify(() => client.post(path, data: data)).called(1); } verifyNoMoreInteractions(client); - expect(pushProvidersMap.length, PushProvider.values.length, - reason: 'All PushProvider should be tested'); + expect(pushProvidersMap.length, PushProvider.values.length, reason: 'All PushProvider should be tested'); }); test('addDevice should work with pushProviderName', () async { @@ -62,16 +59,16 @@ void main() { const path = '/devices'; - when(() => client.post( - path, - data: { - 'id': deviceId, - 'push_provider': pushProvider.name, - 'push_provider_name': pushProviderName, - }, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: { + 'id': deviceId, + 'push_provider': pushProvider.name, + 'push_provider_name': pushProviderName, + }, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await deviceApi.addDevice( deviceId, @@ -97,9 +94,12 @@ void main() { ); when(() => client.get(path)).thenAnswer( - (_) async => successResponse(path, data: { - 'devices': [...devices.map((it) => it.toJson())] - }), + (_) async => successResponse( + path, + data: { + 'devices': [...devices.map((it) => it.toJson())], + }, + ), ); final res = await deviceApi.getDevices(); @@ -141,10 +141,13 @@ void main() { ]; when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: { - 'user_preferences': {}, - 'user_channel_preferences': {}, - }), + (_) async => successResponse( + path, + data: { + 'user_preferences': {}, + 'user_channel_preferences': {}, + }, + ), ); final res = await deviceApi.setPushPreferences(preferences); @@ -170,10 +173,13 @@ void main() { ]; when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: { - 'user_preferences': {}, - 'user_channel_preferences': {}, - }), + (_) async => successResponse( + path, + data: { + 'user_preferences': {}, + 'user_channel_preferences': {}, + }, + ), ); final res = await deviceApi.setPushPreferences(preferences); @@ -195,10 +201,13 @@ void main() { ]; when(() => client.post(path, data: any(named: 'data'))).thenAnswer( - (_) async => successResponse(path, data: { - 'user_preferences': {}, - 'user_channel_preferences': {}, - }), + (_) async => successResponse( + path, + data: { + 'user_preferences': {}, + 'user_channel_preferences': {}, + }, + ), ); final res = await deviceApi.setPushPreferences(preferences); diff --git a/packages/stream_chat/test/src/core/api/general_api_test.dart b/packages/stream_chat/test/src/core/api/general_api_test.dart index 52135981fe..cd41156ce1 100644 --- a/packages/stream_chat/test/src/core/api/general_api_test.dart +++ b/packages/stream_chat/test/src/core/api/general_api_test.dart @@ -10,10 +10,10 @@ import '../../mocks.dart'; void main() { Response successResponse(String path, {Object? data}) => Response( - data: data, - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); late final client = MockHttpClient(); late GeneralApi generalApi; @@ -28,20 +28,26 @@ void main() { const path = '/sync'; - final events = - List.generate(3, (index) => Event(type: 'test-event-type-$index')); + final events = List.generate(3, (index) => Event(type: 'test-event-type-$index')); final data = { 'channel_cids': cids, 'last_sync_at': lastSyncAt.toUtc().toIso8601String(), }; - when(() => client.post( - path, - data: data, - )).thenAnswer((_) async => successResponse(path, data: { - 'events': [...events.map((it) => it.toJson())] - })); + when( + () => client.post( + path, + data: data, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'events': [...events.map((it) => it.toJson())], + }, + ), + ); final res = await generalApi.sync(cids, lastSyncAt); @@ -204,14 +210,21 @@ void main() { ...pagination.toJson(), }); - when(() => client.get( - path, - queryParameters: { - 'payload': payload, - }, - )).thenAnswer((_) async => successResponse(path, data: { - 'members': [...members.map((it) => it.toJson())] - })); + when( + () => client.get( + path, + queryParameters: { + 'payload': payload, + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'members': [...members.map((it) => it.toJson())], + }, + ), + ); final res = await generalApi.queryMembers( channelType, @@ -251,14 +264,21 @@ void main() { ...pagination.toJson(), }); - when(() => client.get( - path, - queryParameters: { - 'payload': payload, - }, - )).thenAnswer((_) async => successResponse(path, data: { - 'members': [...members.map((it) => it.toJson())] - })); + when( + () => client.get( + path, + queryParameters: { + 'payload': payload, + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'members': [...members.map((it) => it.toJson())], + }, + ), + ); final res = await generalApi.queryMembers( channelType, @@ -280,18 +300,24 @@ void main() { test('enrichUrl', () async { const path = '/og'; - const url = - 'https://www.techyourchance.com/finite-state-machine-with-unit-tests-real-world-example'; + const url = 'https://www.techyourchance.com/finite-state-machine-with-unit-tests-real-world-example'; - when(() => client.get( - path, - queryParameters: {'url': url}, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.get( + path, + queryParameters: {'url': url}, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'type': 'image', 'og_scrape_url': url, 'author_name': 'TechYourChance', 'title': 'Finite State Machine with Unit Tests: Real World Example', - })); + }, + ), + ); final res = await generalApi.enrichUrl(url); diff --git a/packages/stream_chat/test/src/core/api/guest_api_test.dart b/packages/stream_chat/test/src/core/api/guest_api_test.dart index 7db74b4a31..40f83e7bee 100644 --- a/packages/stream_chat/test/src/core/api/guest_api_test.dart +++ b/packages/stream_chat/test/src/core/api/guest_api_test.dart @@ -8,10 +8,10 @@ import '../../mocks.dart'; void main() { Response successResponse(String path, {Object? data}) => Response( - data: data, - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); late final client = MockHttpClient(); late GuestApi guestApi; @@ -26,13 +26,20 @@ void main() { const path = '/guest'; - when(() => client.post( - path, - data: {'user': user}, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: {'user': user}, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'access_token': accessToken, 'user': user.toJson(), - })); + }, + ), + ); final res = await guestApi.getGuestUser(user); 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..0cfeacdd83 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 @@ -10,10 +10,10 @@ import '../../mocks.dart'; void main() { Response successResponse(String path, {Object? data}) => Response( - data: data, - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); late final client = MockHttpClient(); late MessageApi messageApi; @@ -29,16 +29,23 @@ void main() { const path = '/channels/$channelType/$channelId/message'; - when(() => client.post( - path, - data: { - 'message': message, - 'skip_push': false, - 'skip_enrich_url': false, - }, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: { + 'message': message, + 'skip_push': false, + 'skip_enrich_url': false, + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'message': message.toJson(), - })); + }, + ), + ); final res = await messageApi.sendMessage(channelId, channelType, message); @@ -56,16 +63,23 @@ void main() { const path = '/channels/$channelType/$channelId/message'; - when(() => client.post( - path, - data: { - 'message': message, - 'skip_push': true, - 'skip_enrich_url': false, - }, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: { + 'message': message, + 'skip_push': true, + 'skip_enrich_url': false, + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'message': message.toJson(), - })); + }, + ), + ); final res = await messageApi.sendMessage( channelId, @@ -93,12 +107,19 @@ void main() { (index) => Message(id: 'test-message-id-$index'), ); - when(() => client.get( - path, - queryParameters: {'ids': messageIds.join(',')}, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.get( + path, + queryParameters: {'ids': messageIds.join(',')}, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'messages': [...messages.map((it) => it.toJson())], - })); + }, + ), + ); final res = await messageApi.getMessagesById( channelId, @@ -122,8 +143,7 @@ void main() { final message = Message(id: messageId); - when(() => client.get(path)).thenAnswer((_) async => - successResponse(path, data: {'message': message.toJson()})); + when(() => client.get(path)).thenAnswer((_) async => successResponse(path, data: {'message': message.toJson()})); final res = await messageApi.getMessage(messageId); @@ -139,14 +159,16 @@ void main() { final path = '/messages/${message.id}'; - when(() => client.post( - path, - data: { - 'message': message, - 'skip_push': false, - 'skip_enrich_url': false, - }, - )).thenAnswer( + when( + () => client.post( + path, + data: { + 'message': message, + 'skip_push': false, + 'skip_enrich_url': false, + }, + ), + ).thenAnswer( (_) async => successResponse(path, data: {'message': message.toJson()}), ); @@ -168,14 +190,16 @@ void main() { const path = '/messages/$messageId'; final message = Message(id: 'test-message-id', text: set['text']); - when(() => client.put( - path, - data: { - 'set': set, - 'unset': unset, - 'skip_enrich_url': false, - }, - )).thenAnswer( + when( + () => client.put( + path, + data: { + 'set': set, + 'unset': unset, + 'skip_enrich_url': false, + }, + ), + ).thenAnswer( (_) async => successResponse(path, data: {'message': message.toJson()}), ); @@ -190,14 +214,16 @@ void main() { expect(res.message.text, set['text']); expect(res.message.pinExpires, isNull); - verify(() => client.put( - path, - data: { - 'set': set, - 'unset': unset, - 'skip_enrich_url': false, - }, - )).called(1); + verify( + () => client.put( + path, + data: { + 'set': set, + 'unset': unset, + 'skip_enrich_url': false, + }, + ), + ).called(1); verifyNoMoreInteractions(client); }); @@ -205,16 +231,17 @@ void main() { const messageId = 'test-message-id'; const path = '/messages/$messageId'; + const params = {'delete_for_me': true}; - when(() => client.delete(path)).thenAnswer( + when(() => client.delete(path, queryParameters: params)).thenAnswer( (_) async => successResponse(path, data: {}), ); - final res = await messageApi.deleteMessage(messageId); + final res = await messageApi.deleteMessage(messageId, deleteForMe: true); expect(res, isNotNull); - verify(() => client.delete(path)).called(1); + verify(() => client.delete(path, queryParameters: params)).called(1); verifyNoMoreInteractions(client); }); @@ -226,17 +253,17 @@ void main() { const path = '/messages/$messageId/action'; - when(() => client.post( - path, - data: { - 'id': channelId, - 'type': channelType, - 'form_data': formData, - 'message_id': messageId, - }, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: { + 'id': channelId, + 'type': channelType, + 'form_data': formData, + 'message_id': messageId, + }, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await messageApi.sendAction( channelId, @@ -254,31 +281,33 @@ 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'; final message = Message(id: messageId); final reaction = Reaction(type: reactionType, messageId: messageId); - when(() => client.post( - path, - data: { - 'reaction': Map.from(extraData) - ..addAll({'type': reactionType}), - 'enforce_unique': false, - }, - )).thenAnswer((_) async => successResponse(path, data: { - 'message': message.toJson(), + when( + () => client.post( + path, + data: jsonEncode({ 'reaction': reaction.toJson(), - })); - - final res = await messageApi.sendReaction( - messageId, - reactionType, - extraData: extraData, + 'skip_push': false, + 'enforce_unique': false, + }), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'message': message.toJson(), + 'reaction': {...reaction.toJson(), 'message_id': messageId}, + }, + ), ); + final res = await messageApi.sendReaction(messageId, reaction); + expect(res, isNotNull); expect(res.message.id, messageId); expect(res.reaction.messageId, messageId); @@ -291,29 +320,34 @@ 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'; final message = Message(id: messageId); final reaction = Reaction(type: reactionType, messageId: messageId); - when(() => client.post( - path, - data: { - 'reaction': Map.from(extraData) - ..addAll({'type': reactionType}), - 'enforce_unique': true, - }, - )).thenAnswer((_) async => successResponse(path, data: { - 'message': message.toJson(), + when( + () => client.post( + path, + data: jsonEncode({ 'reaction': reaction.toJson(), - })); + 'skip_push': false, + 'enforce_unique': true, + }), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'message': message.toJson(), + 'reaction': {...reaction.toJson(), 'message_id': messageId}, + }, + ), + ); final res = await messageApi.sendReaction( messageId, - reactionType, - extraData: extraData, + reaction, enforceUnique: true, ); @@ -332,8 +366,7 @@ void main() { const path = '/messages/$messageId/reaction/$reactionType'; - when(() => client.delete(path)).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.delete(path)).thenAnswer((_) async => successResponse(path, data: {})); final res = await messageApi.deleteReaction(messageId, reactionType); @@ -357,14 +390,25 @@ void main() { ), ); - when(() => client.get( - path, - queryParameters: { - ...const PaginationParams().toJson(), - }, - )).thenAnswer((_) async => successResponse(path, data: { - 'reactions': [...reactions.map((it) => it.toJson())] - })); + when( + () => client.get( + path, + queryParameters: { + ...const PaginationParams().toJson(), + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'reactions': [ + ...reactions.map( + (it) => {...it.toJson(), 'message_id': messageId}, + ), + ], + }, + ), + ); final res = await messageApi.getReactions(messageId, pagination: options); @@ -393,17 +437,24 @@ void main() { }, ); - when(() => client.post( - path, - data: {'language': language}, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: {'language': language}, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'message': { ...translatedMessage.toJson(), 'i18n': { language: translatedMessageText, }, }, - })); + }, + ), + ); final res = await messageApi.translateMessage(messageId, language); @@ -429,14 +480,21 @@ void main() { ), ); - when(() => client.get( - path, - queryParameters: { - ...options.toJson(), - }, - )).thenAnswer((_) async => successResponse(path, data: { - 'messages': [...messages.map((it) => it.toJson())] - })); + when( + () => client.get( + path, + queryParameters: { + ...options.toJson(), + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'messages': [...messages.map((it) => it.toJson())], + }, + ), + ); final res = await messageApi.getReplies(parentId, options: options); @@ -466,13 +524,17 @@ void main() { message: draftMessage, ); - when(() => client.post( - path, - data: jsonEncode({'message': draftMessage}), - )).thenAnswer((_) async => successResponse( - path, - data: {'draft': draft.toJson()}, - )); + when( + () => client.post( + path, + data: jsonEncode({'message': draftMessage}), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: {'draft': draft.toJson()}, + ), + ); final res = await messageApi.createDraft( channelId, @@ -495,11 +557,12 @@ void main() { const path = '/channels/$channelType/$channelId/draft'; - when(() => client.delete(path, queryParameters: {})) - .thenAnswer((_) async => successResponse( - path, - data: {}, - )); + when(() => client.delete(path, queryParameters: {})).thenAnswer( + (_) async => successResponse( + path, + data: {}, + ), + ); final res = await messageApi.deleteDraft( channelId, @@ -519,11 +582,12 @@ void main() { const path = '/channels/$channelType/$channelId/draft'; - when(() => client.delete(path, queryParameters: {'parent_id': parentId})) - .thenAnswer((_) async => successResponse( - path, - data: {}, - )); + when(() => client.delete(path, queryParameters: {'parent_id': parentId})).thenAnswer( + (_) async => successResponse( + path, + data: {}, + ), + ); final res = await messageApi.deleteDraft( channelId, @@ -553,10 +617,14 @@ void main() { message: draftMessage, ); - when(() => client.get(path, queryParameters: {})) - .thenAnswer((_) async => successResponse(path, data: { - 'draft': draft.toJson(), - })); + when(() => client.get(path, queryParameters: {})).thenAnswer( + (_) async => successResponse( + path, + data: { + 'draft': draft.toJson(), + }, + ), + ); final res = await messageApi.getDraft( channelId, @@ -591,12 +659,19 @@ void main() { parentId: parentId, ); - when(() => client.get( - path, - queryParameters: {'parent_id': parentId}, - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.get( + path, + queryParameters: {'parent_id': parentId}, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'draft': draft.toJson(), - })); + }, + ), + ); final res = await messageApi.getDraft( channelId, @@ -614,6 +689,84 @@ void main() { verifyNoMoreInteractions(client); }); + test('queryReactions', () async { + const messageId = 'test-message-id'; + const path = '/messages/$messageId/reactions'; + + final reactions = List.generate( + 3, + (index) => Reaction( + type: 'test-reaction-type-$index', + messageId: messageId, + ), + ); + + when( + () => client.post(path, data: any(named: 'data')), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'reactions': [ + ...reactions.map( + (it) => {...it.toJson(), 'message_id': messageId}, + ), + ], + 'next': null, + }, + ), + ); + + final res = await messageApi.queryReactions(messageId); + + expect(res, isNotNull); + expect(res.reactions.length, reactions.length); + expect(res.reactions.every((it) => it.messageId == messageId), isTrue); + expect(res.next, isNull); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + + test('queryReactions with next cursor', () async { + const messageId = 'test-message-id'; + const path = '/messages/$messageId/reactions'; + const nextCursor = 'next_page_cursor'; + + final reactions = List.generate( + 3, + (index) => Reaction( + type: 'test-reaction-type-$index', + messageId: messageId, + ), + ); + + when( + () => client.post(path, data: any(named: 'data')), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'reactions': [ + ...reactions.map( + (it) => {...it.toJson(), 'message_id': messageId}, + ), + ], + 'next': nextCursor, + }, + ), + ); + + final res = await messageApi.queryReactions(messageId); + + expect(res, isNotNull); + expect(res.reactions.length, reactions.length); + expect(res.next, nextCursor); + + verify(() => client.post(path, data: any(named: 'data'))).called(1); + verifyNoMoreInteractions(client); + }); + test('queryDrafts', () async { const path = '/drafts/query'; @@ -626,22 +779,31 @@ void main() { ), ); - when(() => client.post( - path, - data: any(named: 'data'), - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: any(named: 'data'), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'drafts': [...draftMessages.map((it) => it.toJson())], - })); + }, + ), + ); final res = await messageApi.queryDrafts(); expect(res, isNotNull); expect(res.drafts.length, draftMessages.length); - verify(() => client.post( - path, - data: any(named: 'data'), - )).called(1); + verify( + () => client.post( + path, + data: any(named: 'data'), + ), + ).called(1); verifyNoMoreInteractions(client); }); diff --git a/packages/stream_chat/test/src/core/api/moderation_api_test.dart b/packages/stream_chat/test/src/core/api/moderation_api_test.dart index 69d85fd91d..c5c9cfccfc 100644 --- a/packages/stream_chat/test/src/core/api/moderation_api_test.dart +++ b/packages/stream_chat/test/src/core/api/moderation_api_test.dart @@ -7,10 +7,10 @@ import '../../mocks.dart'; void main() { Response successResponse(String path, {Object? data}) => Response( - data: data, - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); late final client = MockHttpClient(); late ModerationApi moderationApi; @@ -65,15 +65,15 @@ void main() { const path = '/moderation/mute/channel'; - when(() => client.post( - path, - data: { - 'channel_cid': channelCid, - 'expiration': expiration.inMilliseconds, - }, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: { + 'channel_cid': channelCid, + 'expiration': expiration.inMilliseconds, + }, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await moderationApi.muteChannel( channelCid, @@ -91,12 +91,12 @@ void main() { const path = '/moderation/unmute/channel'; - when(() => client.post( - path, - data: {'channel_cid': channelCid}, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: {'channel_cid': channelCid}, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await moderationApi.unmuteChannel(channelCid); @@ -111,14 +111,14 @@ void main() { const path = '/moderation/flag'; - when(() => client.post( - path, - data: { - 'target_message_id': messageId, - }, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: { + 'target_message_id': messageId, + }, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await moderationApi.flagMessage(messageId); @@ -133,14 +133,14 @@ void main() { const path = '/moderation/unflag'; - when(() => client.post( - path, - data: { - 'target_message_id': messageId, - }, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: { + 'target_message_id': messageId, + }, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await moderationApi.unflagMessage(messageId); @@ -155,11 +155,14 @@ void main() { const path = '/moderation/flag'; - when(() => client.post(path, data: { - 'target_user_id': userId, - })) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: { + 'target_user_id': userId, + }, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await moderationApi.flagUser(userId); @@ -174,14 +177,14 @@ void main() { const path = '/moderation/unflag'; - when(() => client.post( - path, - data: { - 'target_user_id': userId, - }, - )) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: { + 'target_user_id': userId, + }, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await moderationApi.unflagUser(userId); @@ -197,12 +200,15 @@ void main() { const path = '/moderation/ban'; - when(() => client.post(path, data: { - 'target_user_id': targetUserId, - ...options, - })) - .thenAnswer( - (_) async => successResponse(path, data: {})); + when( + () => client.post( + path, + data: { + 'target_user_id': targetUserId, + ...options, + }, + ), + ).thenAnswer((_) async => successResponse(path, data: {})); final res = await moderationApi.banUser(targetUserId, options: options); diff --git a/packages/stream_chat/test/src/core/api/polls_api_test.dart b/packages/stream_chat/test/src/core/api/polls_api_test.dart index 20a3de8d85..2c303218f2 100644 --- a/packages/stream_chat/test/src/core/api/polls_api_test.dart +++ b/packages/stream_chat/test/src/core/api/polls_api_test.dart @@ -15,10 +15,10 @@ import '../../mocks.dart'; void main() { Response successResponse(String path, {Object? data}) => Response( - data: data, - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); late final client = MockHttpClient(); late PollsApi pollsApi; @@ -41,12 +41,19 @@ void main() { const path = '/polls'; - when(() => client.post( - path, - data: jsonEncode(poll), - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: jsonEncode(poll), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'poll': poll.toJson(), - })); + }, + ), + ); final res = await pollsApi.createPoll(poll); @@ -73,10 +80,14 @@ void main() { ], ); - when(() => client.get(path)) - .thenAnswer((_) async => successResponse(path, data: { - 'poll': poll.toJson(), - })); + when(() => client.get(path)).thenAnswer( + (_) async => successResponse( + path, + data: { + 'poll': poll.toJson(), + }, + ), + ); final res = await pollsApi.getPoll(pollId); @@ -101,12 +112,19 @@ void main() { const path = '/polls'; - when(() => client.put( - path, - data: jsonEncode(poll), - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.put( + path, + data: jsonEncode(poll), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'poll': poll.toJson(), - })); + }, + ), + ); final res = await pollsApi.updatePoll(poll); @@ -134,12 +152,19 @@ void main() { ], ); - when(() => client.patch( - path, - data: jsonEncode({'set': set, 'unset': unset}), - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.patch( + path, + data: jsonEncode({'set': set, 'unset': unset}), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'poll': poll.toJson(), - })); + }, + ), + ); final res = await pollsApi.partialUpdatePoll( pollId, @@ -151,11 +176,15 @@ void main() { expect(res.poll.id, pollId); expect(res.poll.name, set['name']); - verify(() => client.patch(path, + verify( + () => client.patch( + path, data: jsonEncode({ 'set': set, 'unset': unset, - }))).called(1); + }), + ), + ).called(1); verifyNoMoreInteractions(client); }); @@ -164,8 +193,7 @@ void main() { const path = '/polls/$pollId'; - when(() => client.delete(path)).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.delete(path)).thenAnswer((_) async => successResponse(path, data: {})); final res = await pollsApi.deletePoll(pollId); @@ -184,15 +212,22 @@ void main() { const path = '/polls/$pollId/options'; - when(() => client.post( - path, - data: jsonEncode(option), - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: jsonEncode(option), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'poll_option': option.toJson() ..addAll({ 'id': option.id, }), - })); + }, + ), + ); final res = await pollsApi.createPollOption(pollId, option); @@ -214,13 +249,17 @@ void main() { text: 'test-option-value', ); - when(() => client.get(path)) - .thenAnswer((_) async => successResponse(path, data: { - 'poll_option': option.toJson() - ..addAll({ - 'id': option.id, - }), - })); + when(() => client.get(path)).thenAnswer( + (_) async => successResponse( + path, + data: { + 'poll_option': option.toJson() + ..addAll({ + 'id': option.id, + }), + }, + ), + ); final res = await pollsApi.getPollOption(pollId, optionId); @@ -240,15 +279,22 @@ void main() { const path = '/polls/$pollId/options'; - when(() => client.put( - path, - data: jsonEncode(option), - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.put( + path, + data: jsonEncode(option), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'poll_option': option.toJson() ..addAll({ 'id': option.id, }), - })); + }, + ), + ); final res = await pollsApi.updatePollOption(pollId, option); @@ -265,8 +311,7 @@ void main() { const path = '/polls/$pollId/options/$optionId'; - when(() => client.delete(path)).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.delete(path)).thenAnswer((_) async => successResponse(path, data: {})); final res = await pollsApi.deletePollOption(pollId, optionId); @@ -286,14 +331,21 @@ void main() { const path = '/messages/$messageId/polls/$pollId/vote'; - when(() => client.post( - path, - data: jsonEncode({ - 'vote': vote, - }), - )).thenAnswer((_) async => successResponse(path, data: { + when( + () => client.post( + path, + data: jsonEncode({ + 'vote': vote, + }), + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'vote': vote.toJson(), - })); + }, + ), + ); final res = await pollsApi.castPollVote(messageId, pollId, vote); @@ -315,10 +367,14 @@ void main() { optionId: 'test-option-id', ); - when(() => client.delete(path)) - .thenAnswer((_) async => successResponse(path, data: { - 'vote': vote.toJson(), - })); + when(() => client.delete(path)).thenAnswer( + (_) async => successResponse( + path, + data: { + 'vote': vote.toJson(), + }, + ), + ); final res = await pollsApi.removePollVote(messageId, pollId, voteId); @@ -359,9 +415,14 @@ void main() { path, data: payload, ), - ).thenAnswer((_) async => successResponse(path, data: { + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'polls': [...polls.map((it) => it.toJson())], - })); + }, + ), + ); final res = await pollsApi.queryPolls( filter: filter, @@ -405,9 +466,14 @@ void main() { path, data: payload, ), - ).thenAnswer((_) async => successResponse(path, data: { + ).thenAnswer( + (_) async => successResponse( + path, + data: { 'votes': [...votes.map((it) => it.toJson())], - })); + }, + ), + ); final res = await pollsApi.queryPollVotes( pollId, diff --git a/packages/stream_chat/test/src/core/api/reminders_api_test.dart b/packages/stream_chat/test/src/core/api/reminders_api_test.dart index e0c3be7061..919bb47661 100644 --- a/packages/stream_chat/test/src/core/api/reminders_api_test.dart +++ b/packages/stream_chat/test/src/core/api/reminders_api_test.dart @@ -15,10 +15,10 @@ import '../../mocks.dart'; void main() { Response successResponse(String path, {Object? data}) => Response( - data: data, - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); late final client = MockHttpClient(); late RemindersApi remindersApi; @@ -50,10 +50,14 @@ void main() { ), ]; - when(() => client.post(path, data: jsonEncode({}))) - .thenAnswer((_) async => successResponse(path, data: { - 'reminders': reminders.map((r) => r.toJson()).toList(), - })); + when(() => client.post(path, data: jsonEncode({}))).thenAnswer( + (_) async => successResponse( + path, + data: { + 'reminders': reminders.map((r) => r.toJson()).toList(), + }, + ), + ); final res = await remindersApi.queryReminders(); @@ -89,10 +93,14 @@ void main() { ), ); - when(() => client.post(path, data: expectedPayload)) - .thenAnswer((_) async => successResponse(path, data: { - 'reminders': reminders.map((r) => r.toJson()).toList(), - })); + when(() => client.post(path, data: expectedPayload)).thenAnswer( + (_) async => successResponse( + path, + data: { + 'reminders': reminders.map((r) => r.toJson()).toList(), + }, + ), + ); final res = await remindersApi.queryReminders( filter: filter, @@ -122,10 +130,14 @@ void main() { updatedAt: DateTime(2024, 1, 1), ); - when(() => client.post(path, data: jsonEncode({}))) - .thenAnswer((_) async => successResponse(path, data: { - 'reminder': reminder.toJson(), - })); + when(() => client.post(path, data: jsonEncode({}))).thenAnswer( + (_) async => successResponse( + path, + data: { + 'reminder': reminder.toJson(), + }, + ), + ); final res = await remindersApi.createReminder(messageId); @@ -154,13 +166,16 @@ void main() { 'remind_at': remindAt.toUtc().toIso8601String(), }); - when(() => client.post(path, data: expectedPayload)) - .thenAnswer((_) async => successResponse(path, data: { - 'reminder': reminder.toJson(), - })); + when(() => client.post(path, data: expectedPayload)).thenAnswer( + (_) async => successResponse( + path, + data: { + 'reminder': reminder.toJson(), + }, + ), + ); - final res = - await remindersApi.createReminder(messageId, remindAt: remindAt); + final res = await remindersApi.createReminder(messageId, remindAt: remindAt); expect(res, isNotNull); expect(res.reminder.messageId, messageId); @@ -185,10 +200,14 @@ void main() { updatedAt: DateTime(2024, 1, 2), ); - when(() => client.patch(path, data: jsonEncode({}))) - .thenAnswer((_) async => successResponse(path, data: { - 'reminder': reminder.toJson(), - })); + when(() => client.patch(path, data: jsonEncode({}))).thenAnswer( + (_) async => successResponse( + path, + data: { + 'reminder': reminder.toJson(), + }, + ), + ); final res = await remindersApi.updateReminder(messageId); @@ -217,13 +236,16 @@ void main() { 'remind_at': remindAt.toUtc().toIso8601String(), }); - when(() => client.patch(path, data: expectedPayload)) - .thenAnswer((_) async => successResponse(path, data: { - 'reminder': reminder.toJson(), - })); + when(() => client.patch(path, data: expectedPayload)).thenAnswer( + (_) async => successResponse( + path, + data: { + 'reminder': reminder.toJson(), + }, + ), + ); - final res = - await remindersApi.updateReminder(messageId, remindAt: remindAt); + final res = await remindersApi.updateReminder(messageId, remindAt: remindAt); expect(res, isNotNull); expect(res.reminder.messageId, messageId); @@ -239,8 +261,7 @@ void main() { const messageId = 'test-message-id'; const path = '/messages/$messageId/reminders'; - when(() => client.delete(path)).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.delete(path)).thenAnswer((_) async => successResponse(path, data: {})); final res = await remindersApi.deleteReminder(messageId); 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..644e75b847 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'; @@ -3305,8 +3304,7 @@ void main() { const jsonExample = ''' {"reactions": [{"message_id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f","user_id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680","user": {"id": "c1c9b454-2bcc-402d-8bb0-2f3706ce1680","role": "user","created_at": "2020-01-28T22:17:30.83015Z","updated_at": "2020-01-28T22:17:31.19435Z","banned": false,"online": false,"image": "https://randomuser.me/api/portraits/women/2.jpg","name": "Mia Denys"},"type": "love","score": 1,"created_at": "2020-01-28T22:17:31.128376Z","updated_at": "2020-01-28T22:17:31.128376Z"}]} '''; - final response = - QueryReactionsResponse.fromJson(json.decode(jsonExample)); + final response = QueryReactionsResponse.fromJson(json.decode(jsonExample)); expect(response.reactions, isA>()); }); @@ -3413,14 +3411,12 @@ void main() { }] } '''; - final response = - SearchMessagesResponse.fromJson(json.decode(jsonExample)); + final response = SearchMessagesResponse.fromJson(json.decode(jsonExample)); expect(response.results, isA>()); }); test('ListDevicesResponse', () { - const jsonExample = - '''{"devices":[{"push_provider":"firebase","id":"test"}],"duration":"0.35ms"}'''; + const jsonExample = '''{"devices":[{"push_provider":"firebase","id":"test"}],"duration":"0.35ms"}'''; final response = ListDevicesResponse.fromJson(json.decode(jsonExample)); expect(response.devices, isA>()); }); @@ -3518,8 +3514,7 @@ void main() { test('ConnectGuestUserResponse', () { const jsonExample = '''{"user":{"id":"guest-ac612aee-25fe-49fb-b1af-969e41f452a0-wild-breeze-7","role":"guest","created_at":"2020-02-03T10:19:01.538434Z","updated_at":"2020-02-03T10:19:01.539543Z","banned":false,"online":false},"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZ3Vlc3QtYWM2MTJhZWUtMjVmZS00OWZiLWIxYWYtOTY5ZTQxZjQ1MmEwLXdpbGQtYnJlZXplLTcifQ.mmoFGu7oJjpFsp7nFN78UbIpO7gowbuIbyoppsuvbXA","duration":"4.66ms"}'''; - final response = - ConnectGuestUserResponse.fromJson(json.decode(jsonExample)); + final response = ConnectGuestUserResponse.fromJson(json.decode(jsonExample)); expect(response.user, isA()); expect(response.accessToken, isA()); }); @@ -3551,8 +3546,7 @@ void main() { "updated_at": "2020-01-28T22:17:31.092262Z", "mentioned_users": [] }],"duration":"4.66ms"}'''; - final response = - GetMessagesByIdResponse.fromJson(json.decode(jsonExample)); + final response = GetMessagesByIdResponse.fromJson(json.decode(jsonExample)); expect(response.messages, isA>()); }); @@ -4364,37 +4358,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 = ''' { @@ -4506,8 +4469,7 @@ void main() { final channel1Prefs = user1ChannelPrefs['channel1']!; expect(channel1Prefs.chatLevel, ChatLevel.all); - expect( - channel1Prefs.disabledUntil, DateTime.parse('2024-12-31T23:59:59Z')); + expect(channel1Prefs.disabledUntil, DateTime.parse('2024-12-31T23:59:59Z')); final channel2Prefs = user1ChannelPrefs['channel2']!; expect(channel2Prefs.chatLevel, ChatLevel.none); 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..ad61fbc94d 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 @@ -10,10 +10,10 @@ import '../../mocks.dart'; void main() { Response successResponse(String path, {Object? data}) => Response( - data: data, - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); + data: data, + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); late final client = MockHttpClient(); late UserApi userApi; @@ -32,16 +32,26 @@ void main() { final users = List.generate(3, (index) => User(id: 'test-user-id-$index')); - when(() => client.get(path, queryParameters: { + when( + () => client.get( + path, + queryParameters: { 'payload': jsonEncode({ 'presence': presence, 'sort': sort, 'filter_conditions': filter, ...pagination.toJson(), }), - })).thenAnswer((_) async => successResponse(path, data: { - 'users': [...users.map((it) => it.toJson())] - })); + }, + ), + ).thenAnswer( + (_) async => successResponse( + path, + data: { + 'users': [...users.map((it) => it.toJson())], + }, + ), + ); final res = await userApi.queryUsers( presence: presence, @@ -66,13 +76,17 @@ void main() { final updatedUsers = {for (final user in users) user.id: user}; - when(() => client.post(path, data: { + when( + () => client.post( + path, + data: { 'users': updatedUsers, - })).thenAnswer((_) async => successResponse(path, - data: { - 'users': updatedUsers - .map((key, value) => MapEntry(key, value.toJson())) - })); + }, + ), + ).thenAnswer( + (_) async => + successResponse(path, data: {'users': updatedUsers.map((key, value) => MapEntry(key, value.toJson()))}), + ); final res = await userApi.updateUsers(users); @@ -93,16 +107,18 @@ void main() { final updatedUser = {user.id: User(id: user.id, extraData: user.set!)}; - when(() => client.patch(path, data: { - 'users': [user], - })).thenAnswer( - (_) async => successResponse( + when( + () => client.patch( path, data: { - 'users': - updatedUser.map((key, value) => MapEntry(key, value.toJson())) + 'users': [user], }, ), + ).thenAnswer( + (_) async => successResponse( + path, + data: {'users': updatedUser.map((key, value) => MapEntry(key, value.toJson()))}, + ), ); final res = await userApi.partialUpdateUsers([user]); @@ -110,9 +126,14 @@ void main() { expect(res, isNotNull); expect(res.users.length, updatedUser.length); - verify(() => client.patch(path, data: { - 'users': [user] - })).called(1); + verify( + () => client.patch( + path, + data: { + 'users': [user], + }, + ), + ).called(1); verifyNoMoreInteractions(client); }); @@ -121,9 +142,14 @@ void main() { const path = '/users/block'; - when(() => client.post(path, data: { + when( + () => client.post( + path, + data: { 'blocked_user_id': targetUserId, - })).thenAnswer( + }, + ), + ).thenAnswer( (_) async => successResponse( path, data: { @@ -138,9 +164,14 @@ void main() { expect(res, isNotNull); - verify(() => client.post(path, data: { + verify( + () => client.post( + path, + data: { 'blocked_user_id': targetUserId, - })).called(1); + }, + ), + ).called(1); verifyNoMoreInteractions(client); }); @@ -149,9 +180,14 @@ void main() { const path = '/users/unblock'; - when(() => client.post(path, data: { + when( + () => client.post( + path, + data: { 'blocked_user_id': targetUserId, - })).thenAnswer( + }, + ), + ).thenAnswer( (_) async => successResponse( path, data: {}, @@ -162,9 +198,14 @@ void main() { expect(res, isNotNull); - verify(() => client.post(path, data: { + verify( + () => client.post( + path, + data: { 'blocked_user_id': targetUserId, - })).called(1); + }, + ), + ).called(1); verifyNoMoreInteractions(client); }); @@ -187,50 +228,53 @@ void main() { const path = '/unread'; when(() => client.get(path)).thenAnswer( - (_) async => successResponse(path, data: { - 'duration': '5.23ms', - 'total_unread_count': 42, - 'total_unread_threads_count': 8, - 'total_unread_count_by_team': {'team-1': 15, 'team-2': 27}, - 'channels': [ - { - 'channel_id': 'messaging:test-channel-1', - 'unread_count': 5, - 'last_read': '2024-01-15T10:30:00.000Z', - }, - { - 'channel_id': 'messaging:test-channel-2', - 'unread_count': 10, - 'last_read': '2024-01-15T09:15:00.000Z', - }, - ], - 'channel_type': [ - { - 'channel_type': 'messaging', - 'channel_count': 3, - 'unread_count': 25, - }, - { - 'channel_type': 'livestream', - 'channel_count': 1, - 'unread_count': 17, - }, - ], - 'threads': [ - { - 'unread_count': 3, - 'last_read': '2024-01-15T10:30:00.000Z', - 'last_read_message_id': 'message-1', - 'parent_message_id': 'parent-message-1', - }, - { - 'unread_count': 5, - 'last_read': '2024-01-15T09:45:00.000Z', - 'last_read_message_id': 'message-2', - 'parent_message_id': 'parent-message-2', - }, - ], - }), + (_) async => successResponse( + path, + data: { + 'duration': '5.23ms', + 'total_unread_count': 42, + 'total_unread_threads_count': 8, + 'total_unread_count_by_team': {'team-1': 15, 'team-2': 27}, + 'channels': [ + { + 'channel_id': 'messaging:test-channel-1', + 'unread_count': 5, + 'last_read': '2024-01-15T10:30:00.000Z', + }, + { + 'channel_id': 'messaging:test-channel-2', + 'unread_count': 10, + 'last_read': '2024-01-15T09:15:00.000Z', + }, + ], + 'channel_type': [ + { + 'channel_type': 'messaging', + 'channel_count': 3, + 'unread_count': 25, + }, + { + 'channel_type': 'livestream', + 'channel_count': 1, + 'unread_count': 17, + }, + ], + 'threads': [ + { + 'unread_count': 3, + 'last_read': '2024-01-15T10:30:00.000Z', + 'last_read_message_id': 'message-1', + 'parent_message_id': 'parent-message-1', + }, + { + 'unread_count': 5, + 'last_read': '2024-01-15T09:45:00.000Z', + 'last_read_message_id': 'message-2', + 'parent_message_id': 'parent-message-2', + }, + ], + }, + ), ); final res = await userApi.getUnreadCount(); @@ -246,4 +290,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/error/stream_chat_error_test.dart b/packages/stream_chat/test/src/core/error/stream_chat_error_test.dart index c95afd3cb4..4bba3b898d 100644 --- a/packages/stream_chat/test/src/core/error/stream_chat_error_test.dart +++ b/packages/stream_chat/test/src/core/error/stream_chat_error_test.dart @@ -92,7 +92,8 @@ void main() { const statusCode = 666; const message = 'test-error-message'; final options = RequestOptions(path: 'test-path'); - const data = ''' + const data = + ''' { "code": $code, "StatusCode": $statusCode, diff --git a/packages/stream_chat/test/src/core/http/adapter/custom_adapter_test.dart b/packages/stream_chat/test/src/core/http/adapter/custom_adapter_test.dart index 75c6bb76e5..ce334d3e92 100644 --- a/packages/stream_chat/test/src/core/http/adapter/custom_adapter_test.dart +++ b/packages/stream_chat/test/src/core/http/adapter/custom_adapter_test.dart @@ -15,11 +15,12 @@ void main() { final mockAdapter = MockHttpClientAdapter(); final httpClient = StreamHttpClient(apiKey, httpClientAdapter: mockAdapter); - when(() => mockAdapter.fetch(any(), any(), any())) - .thenAnswer((_) async => ResponseBody( - Stream.value(Uint8List(0)), - 200, - )); + when(() => mockAdapter.fetch(any(), any(), any())).thenAnswer( + (_) async => ResponseBody( + Stream.value(Uint8List(0)), + 200, + ), + ); await httpClient.get('/'); diff --git a/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart b/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart index 269e7c884d..cc92b01a8f 100644 --- a/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart +++ b/packages/stream_chat/test/src/core/http/interceptor/auth_interceptor_test.dart @@ -35,8 +35,7 @@ void main() { expect(queryParams.containsKey('user_id'), isFalse); final token = Token.development('test-user-id'); - when(() => tokenManager.loadToken(refresh: any(named: 'refresh'))) - .thenAnswer((_) async => token); + when(() => tokenManager.loadToken(refresh: any(named: 'refresh'))).thenAnswer((_) async => token); authInterceptor.onRequest(options, handler); @@ -51,8 +50,7 @@ void main() { expect(updatedQueryParams.containsKey('user_id'), isTrue); expect(updatedQueryParams['user_id'], token.userId); - verify(() => tokenManager.loadToken(refresh: any(named: 'refresh'))) - .called(1); + verify(() => tokenManager.loadToken(refresh: any(named: 'refresh'))).called(1); verifyNoMoreInteractions(tokenManager); }, ); @@ -95,13 +93,14 @@ void main() { when(() => tokenManager.isStatic).thenReturn(false); final token = Token.development('test-user-id'); - when(() => tokenManager.loadToken(refresh: true)) - .thenAnswer((_) async => token); + when(() => tokenManager.loadToken(refresh: true)).thenAnswer((_) async => token); - when(() => client.fetch(options)).thenAnswer((_) async => Response( - requestOptions: options, - statusCode: 200, - )); + when(() => client.fetch(options)).thenAnswer( + (_) async => Response( + requestOptions: options, + statusCode: 200, + ), + ); authInterceptor.onError(err, handler); @@ -142,8 +141,7 @@ void main() { when(() => tokenManager.isStatic).thenReturn(false); final token = Token.development('test-user-id'); - when(() => tokenManager.loadToken(refresh: true)) - .thenAnswer((_) async => token); + when(() => tokenManager.loadToken(refresh: true)).thenAnswer((_) async => token); when(() => client.fetch(options)).thenThrow(err); diff --git a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart index 2ddc84e3b4..936af1670b 100644 --- a/packages/stream_chat/test/src/core/http/stream_http_client_test.dart +++ b/packages/stream_chat/test/src/core/http/stream_http_client_test.dart @@ -17,9 +17,9 @@ import '../../mocks.dart'; void main() { Response successResponse(String path) => Response( - requestOptions: RequestOptions(path: path), - statusCode: 200, - ); + requestOptions: RequestOptions(path: path), + statusCode: 200, + ); DioException throwableError( String path, { @@ -53,19 +53,14 @@ void main() { const apiKey = 'api-key'; final client = StreamHttpClient(apiKey); - expect( - client.httpClient.interceptors - .whereType() - .length, - 1); + expect(client.httpClient.interceptors.whereType().length, 1); }); test('AuthInterceptor should be added if tokenManager is provided', () { const apiKey = 'api-key'; final client = StreamHttpClient(apiKey, tokenManager: TokenManager()); - expect( - client.httpClient.interceptors.whereType().length, 1); + expect(client.httpClient.interceptors.whereType().length, 1); }); test( @@ -78,9 +73,7 @@ void main() { ); expect( - client.httpClient.interceptors - .whereType() - .length, + client.httpClient.interceptors.whereType().length, 1, ); }, @@ -175,10 +168,12 @@ void main() { final client = StreamHttpClient('api-key', dio: dio); const path = 'test-get-api-path'; - when(() => dio.get( - path, - options: any(named: 'options'), - )).thenAnswer((_) async => successResponse(path)); + when( + () => dio.get( + path, + options: any(named: 'options'), + ), + ).thenAnswer((_) async => successResponse(path)); final res = await client.get(path); @@ -186,10 +181,12 @@ void main() { expect(res.statusCode, 200); expect(res.requestOptions.path, path); - verify(() => dio.get( - path, - options: any(named: 'options'), - )).called(1); + verify( + () => dio.get( + path, + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }); @@ -202,10 +199,12 @@ void main() { path, error: StreamChatNetworkError(ChatErrorCode.internalSystemError), ); - when(() => dio.get( - path, - options: any(named: 'options'), - )).thenThrow(error); + when( + () => dio.get( + path, + options: any(named: 'options'), + ), + ).thenThrow(error); try { await client.get(path); @@ -214,10 +213,12 @@ void main() { expect(e, StreamChatNetworkError.fromDioException(error)); } - verify(() => dio.get( - path, - options: any(named: 'options'), - )).called(1); + verify( + () => dio.get( + path, + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }); @@ -226,10 +227,12 @@ void main() { final client = StreamHttpClient('api-key', dio: dio); const path = 'test-post-api-path'; - when(() => dio.post( - path, - options: any(named: 'options'), - )).thenAnswer((_) async => successResponse(path)); + when( + () => dio.post( + path, + options: any(named: 'options'), + ), + ).thenAnswer((_) async => successResponse(path)); final res = await client.post(path); @@ -237,10 +240,12 @@ void main() { expect(res.statusCode, 200); expect(res.requestOptions.path, path); - verify(() => dio.post( - path, - options: any(named: 'options'), - )).called(1); + verify( + () => dio.post( + path, + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }); @@ -255,10 +260,12 @@ void main() { path, error: StreamChatNetworkError(ChatErrorCode.internalSystemError), ); - when(() => dio.post( - path, - options: any(named: 'options'), - )).thenThrow(error); + when( + () => dio.post( + path, + options: any(named: 'options'), + ), + ).thenThrow(error); try { await client.post(path); @@ -267,10 +274,12 @@ void main() { expect(e, StreamChatNetworkError.fromDioException(error)); } - verify(() => dio.post( - path, - options: any(named: 'options'), - )).called(1); + verify( + () => dio.post( + path, + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }, ); @@ -280,10 +289,12 @@ void main() { final client = StreamHttpClient('api-key', dio: dio); const path = 'test-delete-api-path'; - when(() => dio.delete( - path, - options: any(named: 'options'), - )).thenAnswer((_) async => successResponse(path)); + when( + () => dio.delete( + path, + options: any(named: 'options'), + ), + ).thenAnswer((_) async => successResponse(path)); final res = await client.delete(path); @@ -291,10 +302,12 @@ void main() { expect(res.statusCode, 200); expect(res.requestOptions.path, path); - verify(() => dio.delete( - path, - options: any(named: 'options'), - )).called(1); + verify( + () => dio.delete( + path, + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }); @@ -309,10 +322,12 @@ void main() { path, error: StreamChatNetworkError(ChatErrorCode.internalSystemError), ); - when(() => dio.delete( - path, - options: any(named: 'options'), - )).thenThrow(error); + when( + () => dio.delete( + path, + options: any(named: 'options'), + ), + ).thenThrow(error); try { await client.delete(path); @@ -321,10 +336,12 @@ void main() { expect(e, StreamChatNetworkError.fromDioException(error)); } - verify(() => dio.delete( - path, - options: any(named: 'options'), - )).called(1); + verify( + () => dio.delete( + path, + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }, ); @@ -334,10 +351,12 @@ void main() { final client = StreamHttpClient('api-key', dio: dio); const path = 'test-patch-api-path'; - when(() => dio.patch( - path, - options: any(named: 'options'), - )).thenAnswer((_) async => successResponse(path)); + when( + () => dio.patch( + path, + options: any(named: 'options'), + ), + ).thenAnswer((_) async => successResponse(path)); final res = await client.patch(path); @@ -345,10 +364,12 @@ void main() { expect(res.statusCode, 200); expect(res.requestOptions.path, path); - verify(() => dio.patch( - path, - options: any(named: 'options'), - )).called(1); + verify( + () => dio.patch( + path, + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }); @@ -363,10 +384,12 @@ void main() { path, error: StreamChatNetworkError(ChatErrorCode.internalSystemError), ); - when(() => dio.patch( - path, - options: any(named: 'options'), - )).thenThrow(error); + when( + () => dio.patch( + path, + options: any(named: 'options'), + ), + ).thenThrow(error); try { await client.patch(path); @@ -375,10 +398,12 @@ void main() { expect(e, StreamChatNetworkError.fromDioException(error)); } - verify(() => dio.patch( - path, - options: any(named: 'options'), - )).called(1); + verify( + () => dio.patch( + path, + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }, ); @@ -388,10 +413,12 @@ void main() { final client = StreamHttpClient('api-key', dio: dio); const path = 'test-put-api-path'; - when(() => dio.put( - path, - options: any(named: 'options'), - )).thenAnswer((_) async => successResponse(path)); + when( + () => dio.put( + path, + options: any(named: 'options'), + ), + ).thenAnswer((_) async => successResponse(path)); final res = await client.put(path); @@ -399,10 +426,12 @@ void main() { expect(res.statusCode, 200); expect(res.requestOptions.path, path); - verify(() => dio.put( - path, - options: any(named: 'options'), - )).called(1); + verify( + () => dio.put( + path, + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }); @@ -417,10 +446,12 @@ void main() { path, error: StreamChatNetworkError(ChatErrorCode.internalSystemError), ); - when(() => dio.put( - path, - options: any(named: 'options'), - )).thenThrow(error); + when( + () => dio.put( + path, + options: any(named: 'options'), + ), + ).thenThrow(error); try { await client.put(path); @@ -429,10 +460,12 @@ void main() { expect(e, StreamChatNetworkError.fromDioException(error)); } - verify(() => dio.put( - path, - options: any(named: 'options'), - )).called(1); + verify( + () => dio.put( + path, + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }, ); @@ -444,11 +477,13 @@ void main() { const path = 'test-delete-api-path'; final file = MultipartFile.fromBytes([]); - when(() => dio.post( - path, - data: any(named: 'data'), - options: any(named: 'options'), - )).thenAnswer((_) async => successResponse(path)); + when( + () => dio.post( + path, + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenAnswer((_) async => successResponse(path)); final res = await client.postFile(path, file); @@ -456,11 +491,13 @@ void main() { expect(res.statusCode, 200); expect(res.requestOptions.path, path); - verify(() => dio.post( - path, - data: any(named: 'data'), - options: any(named: 'options'), - )).called(1); + verify( + () => dio.post( + path, + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }); @@ -477,11 +514,13 @@ void main() { path, error: StreamChatNetworkError(ChatErrorCode.internalSystemError), ); - when(() => dio.post( - path, - data: any(named: 'data'), - options: any(named: 'options'), - )).thenThrow(error); + when( + () => dio.post( + path, + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).thenThrow(error); try { await client.postFile(path, file); @@ -490,11 +529,13 @@ void main() { expect(e, StreamChatNetworkError.fromDioException(error)); } - verify(() => dio.post( - path, - data: any(named: 'data'), - options: any(named: 'options'), - )).called(1); + verify( + () => dio.post( + path, + data: any(named: 'data'), + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }, ); @@ -504,10 +545,12 @@ void main() { final client = StreamHttpClient('api-key', dio: dio); const path = 'test-request-api-path'; - when(() => dio.request( - path, - options: any(named: 'options'), - )).thenAnswer((_) async => successResponse(path)); + when( + () => dio.request( + path, + options: any(named: 'options'), + ), + ).thenAnswer((_) async => successResponse(path)); final res = await client.request(path); @@ -515,10 +558,12 @@ void main() { expect(res.statusCode, 200); expect(res.requestOptions.path, path); - verify(() => dio.request( - path, - options: any(named: 'options'), - )).called(1); + verify( + () => dio.request( + path, + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }); @@ -534,10 +579,12 @@ void main() { streamChatDioError: true, error: StreamChatNetworkError(ChatErrorCode.internalSystemError), ); - when(() => dio.request( - path, - options: any(named: 'options'), - )).thenThrow(error); + when( + () => dio.request( + path, + options: any(named: 'options'), + ), + ).thenThrow(error); try { await client.request(path); @@ -546,10 +593,12 @@ void main() { expect(e, error.error); } - verify(() => dio.request( - path, - options: any(named: 'options'), - )).called(1); + verify( + () => dio.request( + path, + options: any(named: 'options'), + ), + ).called(1); verifyNoMoreInteractions(dio); }, ); diff --git a/packages/stream_chat/test/src/core/http/token_manager_test.dart b/packages/stream_chat/test/src/core/http/token_manager_test.dart index 163bafffe2..626fd676b4 100644 --- a/packages/stream_chat/test/src/core/http/token_manager_test.dart +++ b/packages/stream_chat/test/src/core/http/token_manager_test.dart @@ -32,8 +32,7 @@ void main() { expect(tokenManager.userId, isNull); const userId = 'test-user-id'; - Future tokenProvider(String userId) async => - Token.development(userId).rawValue; + Future tokenProvider(String userId) async => Token.development(userId).rawValue; final returnedToken = await tokenManager.setTokenOrProvider( userId, provider: tokenProvider, @@ -65,8 +64,7 @@ void main() { const userId = 'test-user-id'; final token = Token.development(userId); - Future tokenProvider(String userId) async => - Token.development(userId).rawValue; + Future tokenProvider(String userId) async => Token.development(userId).rawValue; try { await tokenManager.setTokenOrProvider( userId, diff --git a/packages/stream_chat/test/src/core/http/token_test.dart b/packages/stream_chat/test/src/core/http/token_test.dart index 0f0e04253e..543ffda6ba 100644 --- a/packages/stream_chat/test/src/core/http/token_test.dart +++ b/packages/stream_chat/test/src/core/http/token_test.dart @@ -43,8 +43,7 @@ void main() { '`.guest` should create a guest-token with provided user and provider', () async { final user = User(id: 'test-user-id'); - Future provider(User user) async => - Token.development(user.id).rawValue; + Future provider(User user) async => Token.development(user.id).rawValue; final token = await Token.guest(user, provider); expect(token, isNotNull); diff --git a/packages/stream_chat/test/src/core/models/attachment_file_test.dart b/packages/stream_chat/test/src/core/models/attachment_file_test.dart index 6dce83f90e..440f1588ab 100644 --- a/packages/stream_chat/test/src/core/models/attachment_file_test.dart +++ b/packages/stream_chat/test/src/core/models/attachment_file_test.dart @@ -8,8 +8,7 @@ import '../../utils.dart'; void main() { group('src/models/attachment_file', () { test('should parse json correctly', () { - final attachment = - AttachmentFile.fromJson(jsonFixture('attachment_file.json')); + final attachment = AttachmentFile.fromJson(jsonFixture('attachment_file.json')); expect(attachment.name, 'test.jpg'); expect(attachment.size, 12); expect( diff --git a/packages/stream_chat/test/src/core/models/attachment_giphy_info_test.dart b/packages/stream_chat/test/src/core/models/attachment_giphy_info_test.dart index d135c217d7..7d044ea81e 100644 --- a/packages/stream_chat/test/src/core/models/attachment_giphy_info_test.dart +++ b/packages/stream_chat/test/src/core/models/attachment_giphy_info_test.dart @@ -17,15 +17,17 @@ void main() { group('GiphyInfoX', () { test('giphyInfo returns valid GiphyInfo object when data is valid', () { - final attachment = Attachment(extraData: const { - 'giphy': { - 'original': { - 'url': 'https://example.com/original.gif', - 'width': '200', - 'height': '150', - } - } - }); + final attachment = Attachment( + extraData: const { + 'giphy': { + 'original': { + 'url': 'https://example.com/original.gif', + 'width': '200', + 'height': '150', + }, + }, + }, + ); final giphyInfo = attachment.giphyInfo(GiphyInfoType.original); @@ -43,17 +45,18 @@ void main() { expect(giphyInfo, isNull); }); - test('giphyInfo returns null when the specific GiphyInfoType is missing', - () { - final attachment = Attachment(extraData: const { - 'giphy': { - 'fixed_height': { - 'url': 'https://example.com/fixed_height.gif', - 'width': '100', - 'height': '100', - } - } - }); + test('giphyInfo returns null when the specific GiphyInfoType is missing', () { + final attachment = Attachment( + extraData: const { + 'giphy': { + 'fixed_height': { + 'url': 'https://example.com/fixed_height.gif', + 'width': '100', + 'height': '100', + }, + }, + }, + ); final giphyInfo = attachment.giphyInfo(GiphyInfoType.original); diff --git a/packages/stream_chat/test/src/core/models/attachment_test.dart b/packages/stream_chat/test/src/core/models/attachment_test.dart index f702956760..4417ef1173 100644 --- a/packages/stream_chat/test/src/core/models/attachment_test.dart +++ b/packages/stream_chat/test/src/core/models/attachment_test.dart @@ -29,8 +29,7 @@ void main() { final channel = Attachment( type: 'giphy', title: 'soo', - titleLink: - 'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti', + titleLink: 'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti', ); expect( @@ -38,8 +37,7 @@ void main() { { 'type': 'giphy', 'title': 'soo', - 'title_link': - 'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti', + 'title_link': 'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti', 'actions': [], }, ); @@ -51,17 +49,12 @@ void main() { expect(attachment.fileSize, 3); expect(attachment.mimeType, 'text/plain'); - expect(attachment.toJson(), { - 'title': 'myfile.txt', - 'actions': [], - 'file_size': 3, - 'mime_type': 'text/plain' - }); + expect(attachment.toJson(), {'title': 'myfile.txt', 'actions': [], 'file_size': 3, 'mime_type': 'text/plain'}); expect(Attachment.fromJson(attachment.toJson()).toJson(), { 'title': 'myfile.txt', 'actions': [], 'file_size': 3, - 'mime_type': 'text/plain' + 'mime_type': 'text/plain', }); // Setting the size and mimeType using extraData should work fine @@ -88,10 +81,13 @@ void main() { // if file is available, should override size and mimeType. final fileThree = AttachmentFile(size: 9, path: 'myfolder/fileThree.png'); - newAttachment = attachment.copyWith(file: fileThree, extraData: { - 'file_size': 88, - 'mime_type': 'application/pdf', - }); + newAttachment = attachment.copyWith( + file: fileThree, + extraData: { + 'file_size': 88, + 'mime_type': 'application/pdf', + }, + ); expect(newAttachment.extraData['file_size'], 9); expect(newAttachment.extraData['mime_type'], 'image/png'); 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..70870104ed 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 @@ -6,8 +6,7 @@ import '../../utils.dart'; void main() { group('src/models/channel_state', () { test('should parse json correctly', () { - final channelState = - ChannelState.fromJson(jsonFixture('channel_state.json')); + final channelState = ChannelState.fromJson(jsonFixture('channel_state.json')); expect(channelState.channel?.cid, 'team:dev'); expect(channelState.channel?.id, 'dev'); expect(channelState.channel?.team, 'test'); @@ -16,12 +15,9 @@ void main() { expect(channelState.channel?.config, isNotNull); expect(channelState.channel?.config.commands, hasLength(1)); expect(channelState.channel?.config.commands[0], isA()); - expect(channelState.channel?.lastMessageAt, - DateTime.parse('2020-01-30T13:43:41.062362Z')); - expect(channelState.channel?.createdAt, - DateTime.parse('2019-04-03T18:43:33.213373Z')); - expect(channelState.channel?.updatedAt, - DateTime.parse('2019-04-03T18:43:33.213374Z')); + expect(channelState.channel?.lastMessageAt, DateTime.parse('2020-01-30T13:43:41.062362Z')); + expect(channelState.channel?.createdAt, DateTime.parse('2019-04-03T18:43:33.213373Z')); + expect(channelState.channel?.updatedAt, DateTime.parse('2019-04-03T18:43:33.213374Z')); expect(channelState.channel?.createdBy, isA()); expect(channelState.channel?.frozen, true); expect(channelState.channel?.extraData['example'], 1); @@ -63,6 +59,7 @@ void main() { chatLevel: ChatLevel.all, disabledUntil: DateTime.parse('2020-01-30T13:43:41.062362Z'), ), + activeLiveLocations: [], ); expect( @@ -146,8 +143,7 @@ void main() { memberCount: 42, ); - final field = - channelState.getComparableField(ChannelSortKey.memberCount); + final field = channelState.getComparableField(ChannelSortKey.memberCount); expect(field, isNotNull); expect(field!.value, equals(42)); }); diff --git a/packages/stream_chat/test/src/core/models/draft_message_test.dart b/packages/stream_chat/test/src/core/models/draft_message_test.dart index 6542e8b4df..0557c58759 100644 --- a/packages/stream_chat/test/src/core/models/draft_message_test.dart +++ b/packages/stream_chat/test/src/core/models/draft_message_test.dart @@ -35,8 +35,7 @@ void main() { expect(draftMessage.extraData, isEmpty); }); - test('should create a valid instance with UUID when id is not provided', - () { + test('should create a valid instance with UUID when id is not provided', () { final messageWithoutId = DraftMessage(text: text); expect(messageWithoutId.id, isNotNull); expect(messageWithoutId.id, isNotEmpty); @@ -156,8 +155,7 @@ void main() { expect(json['poll_id'], equals(pollId)); }); - test('should append command to text field in toJson when command exists', - () { + test('should append command to text field in toJson when command exists', () { final draftWithCommand = DraftMessage( id: id, text: 'Hello world', @@ -244,8 +242,7 @@ void main() { expect(deserializedMessage.id, equals(id)); expect(deserializedMessage.text, equals(text)); - expect(deserializedMessage.extraData['custom_field'], - equals('custom_value')); + expect(deserializedMessage.extraData['custom_field'], equals('custom_value')); expect(deserializedMessage.extraData['priority'], equals(5)); }); diff --git a/packages/stream_chat/test/src/core/models/event_test.dart b/packages/stream_chat/test/src/core/models/event_test.dart index f7847c4c60..82598566e3 100644 --- a/packages/stream_chat/test/src/core/models/event_test.dart +++ b/packages/stream_chat/test/src/core/models/event_test.dart @@ -94,7 +94,7 @@ void main() { 'total_unread_count': 0, 'unread_channels': 0, 'unread_threads': 0, - 'blocked_user_ids': [] + 'blocked_user_ids': [], }, 'user': {'id': 'id', 'teams': [], 'online': false, 'banned': false}, 'total_unread_count': 1, @@ -121,7 +121,7 @@ void main() { 'mentioned_users': [], 'silent': false, }, - } + }, }, ); 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..9b375e2643 --- /dev/null +++ b/packages/stream_chat/test/src/core/models/location_test.dart @@ -0,0 +1,229 @@ +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 convert endAt to UTC in toJson regardless of input timezone', + () { + // Create a non-UTC DateTime (local time) + final localEndAt = DateTime(2024, 10, 16, 17, 12, 30, 338, 726); + final liveLocation = Location( + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: localEndAt, + ); + + final json = liveLocation.toJson(); + final serializedEndAt = json['end_at'] as String?; + + // Verify the serialized date is in UTC format (ends with 'Z') + expect(serializedEndAt, isNotNull); + expect(serializedEndAt, endsWith('Z')); + + // Verify the stored endAt is in UTC + expect(liveLocation.endAt?.isUtc, isTrue); + + // Verify the date is the same instant, just in UTC + final expectedUtc = localEndAt.toUtc(); + expect(liveLocation.endAt, equals(expectedUtc)); + }, + ); + + 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/member_test.dart b/packages/stream_chat/test/src/core/models/member_test.dart index 752ff46b60..b00884d450 100644 --- a/packages/stream_chat/test/src/core/models/member_test.dart +++ b/packages/stream_chat/test/src/core/models/member_test.dart @@ -12,6 +12,7 @@ void main() { expect(member.channelRole, 'channel_member'); expect(member.createdAt, DateTime.parse('2020-01-28T22:17:30.95443Z')); expect(member.updatedAt, DateTime.parse('2020-01-28T22:17:30.95443Z')); + expect(member.deletedMessages, ['msg-1', 'msg-2', 'msg-3']); expect(member.extraData['some_custom_field'], 'with_custom_data'); }); @@ -104,10 +105,8 @@ void main() { final field1 = recentMember.getComparableField(MemberSortKey.createdAt); final field2 = olderMember.getComparableField(MemberSortKey.createdAt); - expect(field1!.compareTo(field2!), - greaterThan(0)); // More recent > Less recent - expect( - field2.compareTo(field1), lessThan(0)); // Less recent < More recent + expect(field1!.compareTo(field2!), greaterThan(0)); // More recent > Less recent + expect(field2.compareTo(field1), lessThan(0)); // Less recent < More recent }); test('should compare two members correctly using userId', () { @@ -158,10 +157,8 @@ void main() { final field1 = owner.getComparableField(MemberSortKey.channelRole); final field2 = moderator.getComparableField(MemberSortKey.channelRole); - expect(field1!.compareTo(field2!), - greaterThan(0)); // 'owner' > 'moderator' alphabetically - expect(field2.compareTo(field1), - lessThan(0)); // 'moderator' < 'owner' alphabetically + expect(field1!.compareTo(field2!), greaterThan(0)); // 'owner' > 'moderator' alphabetically + expect(field2.compareTo(field1), lessThan(0)); // 'moderator' < 'owner' alphabetically }); test('should compare two members correctly using extraData', () { diff --git a/packages/stream_chat/test/src/core/models/message_reaction_helper_test.dart b/packages/stream_chat/test/src/core/models/message_reaction_helper_test.dart index f24748ea73..0aa5ea5561 100644 --- a/packages/stream_chat/test/src/core/models/message_reaction_helper_test.dart +++ b/packages/stream_chat/test/src/core/models/message_reaction_helper_test.dart @@ -45,10 +45,8 @@ void main() { expect(updatedMessage.reactionGroups!.containsKey('like'), isTrue); expect(updatedMessage.reactionGroups!['like']!.count, 1); expect(updatedMessage.reactionGroups!['like']!.sumScores, 1); - expect(updatedMessage.reactionGroups!['like']!.firstReactionAt, - testReaction.createdAt); - expect(updatedMessage.reactionGroups!['like']!.lastReactionAt, - testReaction.createdAt); + expect(updatedMessage.reactionGroups!['like']!.firstReactionAt, testReaction.createdAt); + expect(updatedMessage.reactionGroups!['like']!.lastReactionAt, testReaction.createdAt); }); test('should add reaction to a message with existing reactions', () { @@ -151,8 +149,7 @@ void main() { expect(updatedMessage.ownReactions, isNotNull); expect(updatedMessage.ownReactions!.length, 2); - final reactionTypes = - updatedMessage.ownReactions!.map((r) => r.type).toList(); + final reactionTypes = updatedMessage.ownReactions!.map((r) => r.type).toList(); expect(reactionTypes, contains('like')); expect(reactionTypes, contains('love')); 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..513f2b6f13 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 @@ -1,5 +1,6 @@ -// ignore_for_file: use_named_constants, lines_longer_than_80_chars +// ignore_for_file: use_named_constants, lines_longer_than_80_chars, avoid_redundant_argument_values +import 'package:stream_chat/src/core/models/message_delete_scope.dart'; import 'package:stream_chat/src/core/models/message_state.dart'; import 'package:test/test.dart'; @@ -74,30 +75,45 @@ void main() { ); test( - 'isSoftDeleting should return true if the message state is MessageOutgoing with Deleting state and not hard deleting', + 'isSoftDeleting should return true if the message state is MessageOutgoing with Deleting state and scope is softDeleteForAll', () { const messageState = MessageState.outgoing( - state: OutgoingState.deleting(), + state: OutgoingState.deleting( + scope: MessageDeleteScope.softDeleteForAll, + ), ); expect(messageState.isSoftDeleting, true); }, ); test( - 'isHardDeleting should return true if the message state is MessageOutgoing with Deleting state and hard deleting', + 'isHardDeleting should return true if the message state is MessageOutgoing with Deleting state and scope is HardDeleteForAll', () { const messageState = MessageState.outgoing( - state: OutgoingState.deleting(hard: true), + state: OutgoingState.deleting( + scope: MessageDeleteScope.hardDeleteForAll, + ), ); expect(messageState.isHardDeleting, true); }, ); + test( + 'isDeletingForMe should return true if the message state is MessageOutgoing with Deleting state and scope is DeleteForMe', + () { + const messageState = MessageState.outgoing( + state: OutgoingState.deleting( + scope: MessageDeleteScope.deleteForMe(), + ), + ); + expect(messageState.isDeletingForMe, true); + }, + ); + test( 'isSent should return true if the message state is MessageCompleted with Sent state', () { - const messageState = - MessageState.completed(state: CompletedState.sent()); + const messageState = MessageState.completed(state: CompletedState.sent()); expect(messageState.isSent, true); }, ); @@ -121,25 +137,41 @@ void main() { ); test( - 'isSoftDeleted should return true if the message state is MessageCompleted with Deleted state and not hard deleting', + 'isSoftDeleted should return true if the message state is MessageCompleted with Deleted state and scope is softDeleteForAll', () { const messageState = MessageState.completed( - state: CompletedState.deleted(), + state: CompletedState.deleted( + scope: MessageDeleteScope.softDeleteForAll, + ), ); expect(messageState.isSoftDeleted, true); }, ); test( - 'isHardDeleted should return true if the message state is MessageCompleted with Deleted state and hard deleting', + 'isHardDeleted should return true if the message state is MessageCompleted with Deleted state and scope is hardDeleteForAll', () { const messageState = MessageState.completed( - state: CompletedState.deleted(hard: true), + state: CompletedState.deleted( + scope: MessageDeleteScope.hardDeleteForAll, + ), ); expect(messageState.isHardDeleted, true); }, ); + test( + 'isDeletedForMe should return true if the message state is MessageCompleted with Deleted state and scope is DeleteForMe', + () { + const messageState = MessageState.completed( + state: CompletedState.deleted( + scope: MessageDeleteScope.deleteForMe(), + ), + ); + expect(messageState.isDeletedForMe, true); + }, + ); + test( 'isSendingFailed should return true if the message state is MessageFailed with SendingFailed state', () { @@ -169,24 +201,40 @@ void main() { ); test( - 'isSoftDeletingFailed should return true if the message state is MessageFailed with DeletingFailed state and not hard deleting', + 'isSoftDeletingFailed should return true if the message state is MessageFailed with DeletingFailed state and scope is softDeleteForAll', () { const messageState = MessageState.failed( - state: FailedState.deletingFailed(), + state: FailedState.deletingFailed( + scope: MessageDeleteScope.softDeleteForAll, + ), ); expect(messageState.isSoftDeletingFailed, true); }, ); test( - 'isHardDeletingFailed should return true if the message state is MessageFailed with DeletingFailed state and hard deleting', + 'isHardDeletingFailed should return true if the message state is MessageFailed with DeletingFailed state and scope is hardDeleteForAll', () { const messageState = MessageState.failed( - state: FailedState.deletingFailed(hard: true), + state: FailedState.deletingFailed( + scope: MessageDeleteScope.hardDeleteForAll, + ), ); expect(messageState.isHardDeletingFailed, true); }, ); + + test( + 'isDeletingForMeFailed should return true if the message state is MessageFailed with DeletingFailed state and scope is DeleteForMe', + () { + const messageState = MessageState.failed( + state: FailedState.deletingFailed( + scope: MessageDeleteScope.deleteForMe(), + ), + ); + expect(messageState.isDeletingForMeFailed, true); + }, + ); }, ); @@ -210,22 +258,41 @@ void main() { ); test( - 'MessageState.softDeleting should create a MessageOutgoing instance with Deleting state and not hard deleting', + 'MessageState.softDeleting should create a MessageOutgoing instance with Deleting state and softDeleteForAll scope', () { const messageState = MessageState.softDeleting; expect(messageState, isA()); expect((messageState as MessageOutgoing).state, isA()); - expect((messageState.state as Deleting).hard, false); + + final deletingState = messageState.state as Deleting; + expect(deletingState.scope, isA()); + expect((deletingState.scope as DeleteForAll).hard, false); }, ); test( - 'MessageState.hardDeleting should create a MessageOutgoing instance with Deleting state and hard deleting', + 'MessageState.hardDeleting should create a MessageOutgoing instance with Deleting state and hardDeleteForAll scope', () { const messageState = MessageState.hardDeleting; expect(messageState, isA()); expect((messageState as MessageOutgoing).state, isA()); - expect((messageState.state as Deleting).hard, true); + + final deletingState = messageState.state as Deleting; + expect(deletingState.scope, isA()); + expect((deletingState.scope as DeleteForAll).hard, true); + }, + ); + + test( + 'MessageState.deletingForMe should create a MessageOutgoing instance with Deleting state and DeleteForMe scope', + () { + const messageState = MessageState.deletingForMe; + expect(messageState, isA()); + expect((messageState as MessageOutgoing).state, isA()); + + final deletingState = messageState.state as Deleting; + expect(deletingState.scope, isA()); + expect((deletingState.scope as DeleteForMe).hard, false); }, ); @@ -248,29 +315,51 @@ void main() { ); test( - 'MessageState.softDeleted should create a MessageCompleted instance with Deleted state and not hard deleting', + 'MessageState.softDeleted should create a MessageCompleted instance with Deleted state and softDeleteForAll scope', () { const messageState = MessageState.softDeleted; expect(messageState, isA()); expect((messageState as MessageCompleted).state, isA()); - expect((messageState.state as Deleted).hard, false); + + final deletedState = messageState.state as Deleted; + expect(deletedState.scope, isA()); + expect((deletedState.scope as DeleteForAll).hard, false); }, ); test( - 'MessageState.hardDeleted should create a MessageCompleted instance with Deleted state and hard deleting', + 'MessageState.hardDeleted should create a MessageCompleted instance with Deleted state and hardDeleteForAll scope', () { const messageState = MessageState.hardDeleted; expect(messageState, isA()); expect((messageState as MessageCompleted).state, isA()); - expect((messageState.state as Deleted).hard, true); + + final deletedState = messageState.state as Deleted; + expect(deletedState.scope, isA()); + expect((deletedState.scope as DeleteForAll).hard, true); + }, + ); + + test( + 'MessageState.deletedForMe should create a MessageCompleted instance with Deleted state and DeleteForMe scope', + () { + const messageState = MessageState.deletedForMe; + expect(messageState, isA()); + expect((messageState as MessageCompleted).state, isA()); + + final deletedState = messageState.state as Deleted; + expect(deletedState.scope, isA()); + expect((deletedState.scope as DeleteForMe).hard, false); }, ); 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,29 +368,65 @@ 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.softDeletingFailed should create a MessageFailed instance with DeletingFailed state and not hard deleting', + 'MessageState.partialUpdatingFailed should create a MessageFailed instance with PartialUpdatingFailed 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 softDeleteForAll scope', () { const messageState = MessageState.softDeletingFailed; expect(messageState, isA()); expect((messageState as MessageFailed).state, isA()); - expect((messageState.state as DeletingFailed).hard, false); + + final failedState = messageState.state as DeletingFailed; + expect(failedState.scope, isA()); + expect((failedState.scope as DeleteForAll).hard, false); }, ); test( - 'MessageState.hardDeletingFailed should create a MessageFailed instance with DeletingFailed state and hard deleting', + 'MessageState.hardDeletingFailed should create a MessageFailed instance with DeletingFailed state and hardDeleteForAll scope', () { const messageState = MessageState.hardDeletingFailed; expect(messageState, isA()); expect((messageState as MessageFailed).state, isA()); - expect((messageState.state as DeletingFailed).hard, true); + + final failedState = messageState.state as DeletingFailed; + expect(failedState.scope, isA()); + expect((failedState.scope as DeleteForAll).hard, true); + }, + ); + + test( + 'MessageState.deletingForMeFailed should create a MessageFailed instance with DeletingFailed state and DeleteForMe scope', + () { + const messageState = MessageState.deletingForMeFailed; + expect(messageState, isA()); + expect((messageState as MessageFailed).state, isA()); + + final failedState = messageState.state as DeletingFailed; + expect(failedState.scope, isA()); + expect((failedState.scope as DeleteForMe).hard, false); }, ); }); 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 ff5a7665e7..4f7651e600 100644 --- a/packages/stream_chat/test/src/core/models/message_test.dart +++ b/packages/stream_chat/test/src/core/models/message_test.dart @@ -2,6 +2,7 @@ import 'package:stream_chat/src/core/models/attachment.dart'; import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/core/models/message_state.dart'; import 'package:stream_chat/src/core/models/moderation.dart'; import 'package:stream_chat/src/core/models/reaction.dart'; import 'package:stream_chat/src/core/models/reaction_group.dart'; @@ -15,19 +16,16 @@ void main() { test('should parse json correctly', () { final message = Message.fromJson(jsonFixture('message.json')); expect(message.id, '4637f7e4-a06b-42db-ba5a-8d8270dd926f'); - expect(message.text, - 'https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA'); + expect(message.text, 'https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA'); expect(message.type, 'regular'); expect(message.user, isA()); expect(message.silent, isA()); 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>()); @@ -43,24 +41,19 @@ void main() { test('should serialize to json correctly', () { final message = Message( id: '4637f7e4-a06b-42db-ba5a-8d8270dd926f', - text: - 'https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA', + text: 'https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA', attachments: [ Attachment.fromJson(const { 'type': 'giphy', 'author_name': 'GIPHY', 'title': 'The Lion King Disney GIF - Find \u0026 Share on GIPHY', - 'title_link': - 'https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif', + 'title_link': 'https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif', 'text': '''Discover \u0026 share this Lion King Live Action GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.''', - 'image_url': - 'https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif', - 'thumb_url': - 'https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif', - 'asset_url': - 'https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4', - }) + 'image_url': 'https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif', + 'thumb_url': 'https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif', + 'asset_url': 'https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.mp4', + }), ], showInChannel: true, parentId: 'parentId', @@ -290,13 +283,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); @@ -340,22 +343,18 @@ void main() { }); test('should return true when user is in restrictedVisibility list', () { - final message = - Message(restrictedVisibility: const ['user1', 'user2', 'user3']); + final message = Message(restrictedVisibility: const ['user1', 'user2', 'user3']); expect(message.isVisibleTo('user2'), true); }); - test('should return false when user is not in restrictedVisibility list', - () { - final message = - Message(restrictedVisibility: const ['user1', 'user2', 'user3']); + test('should return false when user is not in restrictedVisibility list', () { + final message = Message(restrictedVisibility: const ['user1', 'user2', 'user3']); expect(message.isVisibleTo('user4'), false); }); test('should handle case sensitivity correctly', () { final message = Message(restrictedVisibility: const ['User1', 'USER2']); - expect(message.isVisibleTo('user1'), false, - reason: 'Should be case sensitive'); + expect(message.isVisibleTo('user1'), false, reason: 'Should be case sensitive'); expect(message.isVisibleTo('User1'), true); }); }); @@ -372,15 +371,12 @@ void main() { }); test('should return false when user is in restrictedVisibility list', () { - final message = - Message(restrictedVisibility: const ['user1', 'user2', 'user3']); + final message = Message(restrictedVisibility: const ['user1', 'user2', 'user3']); expect(message.isNotVisibleTo('user2'), false); }); - test('should return true when user is not in restrictedVisibility list', - () { - final message = - Message(restrictedVisibility: const ['user1', 'user2', 'user3']); + test('should return true when user is not in restrictedVisibility list', () { + final message = Message(restrictedVisibility: const ['user1', 'user2', 'user3']); expect(message.isNotVisibleTo('user4'), true); }); @@ -540,6 +536,94 @@ void main() { expect(message.isFlagged, isTrue); }); }); + + group('syncWith', () { + test('should preserve local timestamps from the other message', () { + final localCreatedAt = DateTime(2024, 1, 1); + final localUpdatedAt = DateTime(2024, 1, 2); + final localDeletedAt = DateTime(2024, 1, 3); + + final serverMessage = Message(id: 'msg-1', text: 'Hello'); + final localMessage = Message( + id: 'msg-1', + text: 'Hello', + localCreatedAt: localCreatedAt, + localUpdatedAt: localUpdatedAt, + localDeletedAt: localDeletedAt, + ); + + final synced = serverMessage.syncWith(localMessage); + expect(synced.localCreatedAt, localCreatedAt); + expect(synced.localUpdatedAt, localUpdatedAt); + expect(synced.localDeletedAt, localDeletedAt); + }); + + test('should return this if other is null', () { + final message = Message(id: 'msg-1', text: 'Hello'); + final synced = message.syncWith(null); + expect(identical(synced, message), isTrue); + }); + + test( + 'should preserve deletedForMe from local when server does not have it', + () { + final serverMessage = Message( + id: 'msg-1', + text: 'Hello', + ); + final localMessage = Message( + id: 'msg-1', + text: 'Hello', + deletedForMe: true, + type: MessageType.deleted, + state: MessageState.deletedForMe, + ); + + final synced = serverMessage.syncWith(localMessage); + expect(synced.deletedForMe, isTrue); + expect(synced.type, MessageType.deleted); + expect(synced.state, MessageState.deletedForMe); + expect(synced.isDeleted, isTrue); + }, + ); + + test( + 'should keep server deletedForMe when both server and local have it', + () { + final serverMessage = Message( + id: 'msg-1', + text: 'Hello', + deletedForMe: true, + type: MessageType.deleted, + state: MessageState.deletedForMe, + ); + final localMessage = Message( + id: 'msg-1', + text: 'Hello', + deletedForMe: true, + type: MessageType.deleted, + state: MessageState.deletedForMe, + ); + + final synced = serverMessage.syncWith(localMessage); + expect(synced.deletedForMe, isTrue); + expect(synced.type, MessageType.deleted); + expect(synced.state, MessageState.deletedForMe); + }, + ); + + test( + 'should not set deletedForMe when neither server nor local have it', + () { + final serverMessage = Message(id: 'msg-1', text: 'Hello'); + final localMessage = Message(id: 'msg-1', text: 'Hello'); + + final synced = serverMessage.syncWith(localMessage); + expect(synced.deletedForMe, isNull); + expect(synced.type, isNot(MessageType.deleted)); + }, + ); + }); } /// Helper function to create a Message for testing diff --git a/packages/stream_chat/test/src/core/models/moderation_test.dart b/packages/stream_chat/test/src/core/models/moderation_test.dart index 4a099d5fc4..2405c44a90 100644 --- a/packages/stream_chat/test/src/core/models/moderation_test.dart +++ b/packages/stream_chat/test/src/core/models/moderation_test.dart @@ -153,8 +153,7 @@ void main() { }); test('should create from custom string correctly', () { - expect(ModerationAction.fromJson('custom'), - const ModerationAction('custom')); + expect(ModerationAction.fromJson('custom'), const ModerationAction('custom')); }); test('should handle legacy v1 moderation actions correctly', () { @@ -179,8 +178,7 @@ void main() { expect(ModerationAction.toJson(ModerationAction.flag), 'flag'); expect(ModerationAction.toJson(ModerationAction.remove), 'remove'); expect(ModerationAction.toJson(ModerationAction.shadow), 'shadow'); - expect(ModerationAction.toJson(const ModerationAction('custom')), - 'custom'); + expect(ModerationAction.toJson(const ModerationAction('custom')), 'custom'); }); test('should serialize legacy v1 action strings correctly', () { diff --git a/packages/stream_chat/test/src/core/models/own_user_test.dart b/packages/stream_chat/test/src/core/models/own_user_test.dart index 10f864ebd8..905a824600 100644 --- a/packages/stream_chat/test/src/core/models/own_user_test.dart +++ b/packages/stream_chat/test/src/core/models/own_user_test.dart @@ -28,8 +28,7 @@ void main() { expect(ownUser.createdAt, DateTime.parse('2020-03-03T16:48:28.853674Z')); expect(ownUser.updatedAt, DateTime.parse('2021-05-26T03:22:20.296181Z')); - expect( - ownUser.lastActive, DateTime.parse('2021-06-16T11:59:59.003453014Z')); + expect(ownUser.lastActive, DateTime.parse('2021-06-16T11:59:59.003453014Z')); expect(ownUser.banned, false); expect(ownUser.online, true); expect(ownUser.devices.length, 1); diff --git a/packages/stream_chat/test/src/core/models/poll_test.dart b/packages/stream_chat/test/src/core/models/poll_test.dart index bb3ba1c0a1..ae44676408 100644 --- a/packages/stream_chat/test/src/core/models/poll_test.dart +++ b/packages/stream_chat/test/src/core/models/poll_test.dart @@ -63,7 +63,7 @@ void main() { expect(json['name'], 'test'); expect(json['description'], isNull); expect(json['options'], [ - {'text': 'option1 text'} + {'text': 'option1 text'}, ]); expect(json['voting_visibility'], 'public'); expect(json['enforce_unique_vote'], true); 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 08a3de80ed..2b9ed6dbeb 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(), @@ -22,18 +23,19 @@ void main() { 'online': false, 'banned': false, 'image': 'https://randomuser.me/api/portraits/women/45.jpg', - 'name': 'Daisy Morgan' + 'name': 'Daisy Morgan', }, ); 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', @@ -41,16 +43,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', }, ); @@ -60,8 +62,8 @@ void main() { final reaction = Reaction.fromJson(jsonFixture('reaction.json')); var newReaction = reaction.copyWith(); expect(newReaction.messageId, '76cd8c82-b557-4e48-9d12-87995d3a0e04'); - expect( - newReaction.createdAt, DateTime.parse('2020-01-28T22:17:31.108742Z')); + 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(), @@ -74,19 +76,20 @@ void main() { 'online': false, 'banned': false, 'image': 'https://randomuser.me/api/portraits/women/45.jpg', - 'name': 'Daisy Morgan' + 'name': 'Daisy Morgan', }, ); 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, @@ -99,10 +102,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); @@ -117,6 +125,41 @@ void main() { expect(newReaction.userId, 'test'); }); + group('ComparableFieldProvider', () { + test('should return ComparableField for reaction.createdAt', () { + final createdAt = DateTime(2020, 1, 28); + final reaction = Reaction(type: 'like', createdAt: createdAt); + + final field = reaction.getComparableField(ReactionSortKey.createdAt); + expect(field, isNotNull); + expect(field!.value, equals(createdAt)); + }); + + test('should return null for non-existent field keys', () { + final reaction = Reaction(type: 'like'); + + final field = reaction.getComparableField('non_existent_key'); + expect(field, isNull); + }); + + test('should compare two reactions correctly using createdAt', () { + final recentReaction = Reaction( + type: 'like', + createdAt: DateTime(2020, 6, 15), + ); + final olderReaction = Reaction( + type: 'like', + createdAt: DateTime(2020, 6, 10), + ); + + final field1 = recentReaction.getComparableField(ReactionSortKey.createdAt); + final field2 = olderReaction.getComparableField(ReactionSortKey.createdAt); + + expect(field1!.compareTo(field2!), greaterThan(0)); // more recent > older + expect(field2.compareTo(field1), lessThan(0)); // older < more recent + }); + }); + test('merge', () { final reaction = Reaction.fromJson(jsonFixture('reaction.json')); final newUserCreateTime = DateTime.now(); @@ -124,8 +167,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( @@ -138,10 +182,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/models/read_test.dart b/packages/stream_chat/test/src/core/models/read_test.dart index 197fa3f161..4a538b51ab 100644 --- a/packages/stream_chat/test/src/core/models/read_test.dart +++ b/packages/stream_chat/test/src/core/models/read_test.dart @@ -33,12 +33,7 @@ void main() { ); expect(read.toJson(), { - 'user': { - 'id': 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e', - 'teams': [], - 'online': false, - 'banned': false - }, + 'user': {'id': 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e', 'teams': [], 'online': false, 'banned': false}, 'last_read': '2020-01-28T22:17:30.966485Z', 'unread_messages': 10, 'last_read_message_id': '8cc1301d-2d47-4305-945a-cd8e19b736d6', diff --git a/packages/stream_chat/test/src/core/models/serialization_test.dart b/packages/stream_chat/test/src/core/models/serialization_test.dart index 63b1428f78..594a3427b1 100644 --- a/packages/stream_chat/test/src/core/models/serialization_test.dart +++ b/packages/stream_chat/test/src/core/models/serialization_test.dart @@ -30,15 +30,14 @@ void main() { }); test('should have empty extraData', () { - final result = Serializer.moveToExtraDataFromRoot({ - 'prop1': 'test', - 'prop2': 123, - 'prop3': true, - }, [ - 'prop1', - 'prop2', - 'prop3' - ]); + final result = Serializer.moveToExtraDataFromRoot( + { + 'prop1': 'test', + 'prop2': 123, + 'prop3': true, + }, + ['prop1', 'prop2', 'prop3'], + ); expect(result, { 'prop1': 'test', diff --git a/packages/stream_chat/test/src/core/models/thread_test.dart b/packages/stream_chat/test/src/core/models/thread_test.dart index 21ab698a59..9e0a04bbee 100644 --- a/packages/stream_chat/test/src/core/models/thread_test.dart +++ b/packages/stream_chat/test/src/core/models/thread_test.dart @@ -91,8 +91,7 @@ void main() { final updatedThread = thread.copyWith(draft: updatedDraft); expect(updatedThread.draft?.message.text, equals('Updated draft')); - expect( - updatedThread.draft?.message.text, isNot(equals(draft.message.text))); + expect(updatedThread.draft?.message.text, isNot(equals(draft.message.text))); // Test copyWith with null draft (removing draft) final removedDraftThread = thread.copyWith(draft: null); @@ -209,8 +208,7 @@ void main() { // Test text equality instead of object identity expect(thread1.draft?.message.text, equals(thread2.draft?.message.text)); - expect(thread1.draft?.message.text, - isNot(equals(thread3.draft?.message.text))); + expect(thread1.draft?.message.text, isNot(equals(thread3.draft?.message.text))); }); }); } diff --git a/packages/stream_chat/test/src/core/models/user_block_test.dart b/packages/stream_chat/test/src/core/models/user_block_test.dart index 3297e0016b..18162de5d7 100644 --- a/packages/stream_chat/test/src/core/models/user_block_test.dart +++ b/packages/stream_chat/test/src/core/models/user_block_test.dart @@ -30,21 +30,11 @@ void main() { expect( userBlock.toJson(), { - 'user': { - 'id': 'user-1', - 'teams': [], - 'online': false, - 'banned': false - }, - 'blocked_user': { - 'id': 'user-2', - 'teams': [], - 'online': false, - 'banned': false - }, + 'user': {'id': 'user-1', 'teams': [], 'online': false, 'banned': false}, + 'blocked_user': {'id': 'user-2', 'teams': [], 'online': false, 'banned': false}, 'user_id': 'user-1', 'blocked_user_id': 'user-2', - 'created_at': '2020-01-28T22:17:30.830150Z' + 'created_at': '2020-01-28T22:17:30.830150Z', }, ); }); diff --git a/packages/stream_chat/test/src/core/models/user_test.dart b/packages/stream_chat/test/src/core/models/user_test.dart index 5e16ea861a..5915e442aa 100644 --- a/packages/stream_chat/test/src/core/models/user_test.dart +++ b/packages/stream_chat/test/src/core/models/user_test.dart @@ -9,8 +9,7 @@ void main() { const id = 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'; const role = 'test-role'; const name = 'John'; - const image = - 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow'; + const image = 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow'; const extraDataStringTest = 'Extra data test'; const extraDataIntTest = 1; const extraDataDoubleTest = 1.1; @@ -352,15 +351,11 @@ void main() { lastActive: DateTime(2023, 6, 10), ); - final field1 = - recentlyActive.getComparableField(UserSortKey.lastActive); - final field2 = - lessRecentlyActive.getComparableField(UserSortKey.lastActive); + final field1 = recentlyActive.getComparableField(UserSortKey.lastActive); + final field2 = lessRecentlyActive.getComparableField(UserSortKey.lastActive); - expect(field1!.compareTo(field2!), - greaterThan(0)); // More recent > Less recent - expect( - field2.compareTo(field1), lessThan(0)); // Less recent < More recent + expect(field1!.compareTo(field2!), greaterThan(0)); // More recent > Less recent + expect(field2.compareTo(field1), lessThan(0)); // Less recent < More recent }); test('should compare two users correctly using banned status', () { 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..fdf330a4d0 --- /dev/null +++ b/packages/stream_chat/test/src/core/util/event_controller_test.dart @@ -0,0 +1,335 @@ +// 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/core/util/message_rules_test.dart b/packages/stream_chat/test/src/core/util/message_rules_test.dart index 06f0d3ac62..8f84f2183b 100644 --- a/packages/stream_chat/test/src/core/util/message_rules_test.dart +++ b/packages/stream_chat/test/src/core/util/message_rules_test.dart @@ -57,6 +57,14 @@ void main() { expect(MessageRules.canUpload(message), isTrue); }); + test('should return true for message with shared location', () { + final message = Message( + sharedLocation: Location(latitude: 1, longitude: 2), + ); + + expect(MessageRules.canUpload(message), isTrue); + }); + test('should return false for empty message', () { final message = Message(); diff --git a/packages/stream_chat/test/src/core/util/serializer_test.dart b/packages/stream_chat/test/src/core/util/serializer_test.dart index fbbb96a20c..9ae3fa536b 100644 --- a/packages/stream_chat/test/src/core/util/serializer_test.dart +++ b/packages/stream_chat/test/src/core/util/serializer_test.dart @@ -19,7 +19,7 @@ void main() { 'name': 'Sahil', 'age': 22, 'country': 'India', - } + }, }); }); @@ -30,7 +30,7 @@ void main() { 'name': 'Sahil', 'age': 22, 'country': 'India', - } + }, }); expect(serializer, { 'test': 'test', 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 faf69c799f..555d13ecd0 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'; @@ -45,24 +46,27 @@ class TestPersistenceClient extends ChatPersistenceClient { Future deletePinnedMessageByCids(List cids) => Future.value(); @override - Future deletePinnedMessageByIds(List messageIds) => - Future.value(); + Future deletePinnedMessageByIds(List messageIds) => Future.value(); @override - Future deleteReactionsByMessageId(List messageIds) => - Future.value(); + Future deleteMessagesFromUser({ + String? cid, + required String userId, + bool hardDelete = false, + DateTime? deletedAt, + }) => throw UnimplementedError(); @override - Future deletePinnedMessageReactionsByMessageId( - List messageIds) => - Future.value(); + Future deleteReactionsByMessageId(List messageIds) => Future.value(); + + @override + Future deletePinnedMessageReactionsByMessageId(List messageIds) => Future.value(); @override Future deletePollVotesByPollIds(List pollIds) => Future.value(); @override - Future deleteDraftMessageByCid(String cid, {String? parentId}) => - Future.value(); + Future deleteDraftMessageByCid(String cid, {String? parentId}) => Future.value(); @override Future disconnect({bool flush = false}) => throw UnimplementedError(); @@ -71,22 +75,21 @@ class TestPersistenceClient extends ChatPersistenceClient { Future flush() => throw UnimplementedError(); @override - Future getChannelByCid(String cid) async => - ChannelModel(cid: cid); + Future getChannelByCid(String cid) async => ChannelModel(cid: cid); @override Future> getChannelCids() => throw UnimplementedError(); @override - Future> getChannelStates( - {Filter? filter, - SortOrder? channelStateSort, - PaginationParams? paginationParams}) => - throw UnimplementedError(); + Future> getChannelStates({ + Filter? filter, + SortOrder? channelStateSort, + int? messageLimit, + PaginationParams? paginationParams, + }) => throw UnimplementedError(); @override - Future>> getChannelThreads(String cid) => - throw UnimplementedError(); + Future>> getChannelThreads(String cid) => throw UnimplementedError(); @override Future getConnectionInfo() => throw UnimplementedError(); @@ -98,14 +101,10 @@ class TestPersistenceClient extends ChatPersistenceClient { Future> getMembersByCid(String cid) async => []; @override - Future> getMessagesByCid(String cid, - {PaginationParams? messagePagination}) async => - []; + Future> getMessagesByCid(String cid, {PaginationParams? messagePagination}) async => []; @override - Future> getPinnedMessagesByCid(String cid, - {PaginationParams? messagePagination}) async => - []; + Future> getPinnedMessagesByCid(String cid, {PaginationParams? messagePagination}) async => []; @override Future> getReadsByCid(String cid) async => []; @@ -114,22 +113,18 @@ class TestPersistenceClient extends ChatPersistenceClient { Future getDraftMessageByCid( String cid, { String? parentId, - }) async => - Draft( - channelCid: cid, - parentId: parentId, - createdAt: DateTime.now(), - message: DraftMessage(id: 'message-id', text: 'message-text'), - ); + }) async => Draft( + channelCid: cid, + parentId: parentId, + createdAt: DateTime.now(), + message: DraftMessage(id: 'message-id', text: 'message-text'), + ); @override - Future> getReplies(String parentId, - {PaginationParams? options}) => - throw UnimplementedError(); + Future> getReplies(String parentId, {PaginationParams? options}) => throw UnimplementedError(); @override - Future updateChannelQueries(Filter? filter, List cids, - {bool clearQueryCache = false}) => + Future updateChannelQueries(Filter? filter, List cids, {bool clearQueryCache = false}) => throw UnimplementedError(); @override @@ -139,15 +134,13 @@ class TestPersistenceClient extends ChatPersistenceClient { Future updateConnectionInfo(Event event) => throw UnimplementedError(); @override - Future updateLastSyncAt(DateTime lastSyncAt) => - throw UnimplementedError(); + Future updateLastSyncAt(DateTime lastSyncAt) => throw UnimplementedError(); @override Future updateReactions(List reactions) => Future.value(); @override - Future updatePinnedMessageReactions(List reactions) => - Future.value(); + Future updatePinnedMessageReactions(List reactions) => Future.value(); @override Future updatePollVotes(List pollVotes) => Future.value(); @@ -156,20 +149,16 @@ class TestPersistenceClient extends ChatPersistenceClient { Future updateUsers(List users) => Future.value(); @override - Future bulkUpdateMembers(Map?> members) => - Future.value(); + Future bulkUpdateMembers(Map?> members) => Future.value(); @override - Future bulkUpdateMessages(Map?> messages) => - Future.value(); + Future bulkUpdateMessages(Map?> messages) => Future.value(); @override - Future bulkUpdatePinnedMessages(Map?> messages) => - Future.value(); + Future bulkUpdatePinnedMessages(Map?> messages) => Future.value(); @override - Future bulkUpdateReads(Map?> reads) => - Future.value(); + Future bulkUpdateReads(Map?> reads) => Future.value(); @override Future deletePollsByIds(List pollIds) => Future.value(); @@ -179,6 +168,21 @@ 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() { @@ -247,8 +251,8 @@ void main() { user: user, ownReactions: [Reaction(type: 'test', user: user)], latestReactions: [Reaction(type: 'test', user: user)], - ) - ] + ), + ], }; persistenceClient.updateChannelThreads(cid, threads); }); @@ -270,7 +274,7 @@ void main() { user: user, ownReactions: [Reaction(type: 'test', user: user)], latestReactions: [Reaction(type: 'test', user: user)], - ) + ), ], pinnedMessages: [ Message( @@ -279,13 +283,10 @@ void main() { user: user, ownReactions: [Reaction(type: 'test', user: user)], latestReactions: [Reaction(type: 'test', user: user)], - ) + ), ], read: [ - Read( - lastRead: DateTime.now(), - user: user, - lastReadMessageId: 'last-test-message'), + Read(lastRead: DateTime.now(), user: user, lastReadMessageId: 'last-test-message'), ], members: [Member(user: user)], ); diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart index ff2c538e74..4e561b0884 100644 --- a/packages/stream_chat/test/src/fakes.dart +++ b/packages/stream_chat/test/src/fakes.dart @@ -34,8 +34,7 @@ class FakeTokenManager extends Fake implements TokenManager { String userId, { Token? token, TokenProvider? provider, - }) async => - this.token; + }) async => this.token; @override void reset() {} @@ -48,8 +47,8 @@ class FakePersistenceClient extends Fake implements ChatPersistenceClient { FakePersistenceClient({ DateTime? lastSyncAt, List? channelCids, - }) : _lastSyncAt = lastSyncAt, - _channelCids = channelCids ?? []; + }) : _lastSyncAt = lastSyncAt, + _channelCids = channelCids ?? []; String? _userId; bool _isConnected = false; @@ -144,8 +143,7 @@ class FakeChatApi extends Fake implements StreamChatApi { AttachmentFileUploader? _fileUploader; @override - AttachmentFileUploader get fileUploader => - _fileUploader ??= MockAttachmentFileUploader(); + AttachmentFileUploader get fileUploader => _fileUploader ??= MockAttachmentFileUploader(); } class FakeClientState extends Fake implements ClientState { @@ -218,8 +216,7 @@ class FakeWebSocket extends Fake implements WebSocket { ConnectionStatus get connectionStatus => _connectionStatusController.value; @override - Stream get connectionStatusStream => - _connectionStatusController.stream; + Stream get connectionStatusStream => _connectionStatusController.stream; @override Completer? connectionCompleter; @@ -265,8 +262,7 @@ class FakeWebSocketWithConnectionError extends Fake implements WebSocket { ConnectionStatus get connectionStatus => _connectionStatusController.value; @override - Stream get connectionStatusStream => - _connectionStatusController.stream; + Stream get connectionStatusStream => _connectionStatusController.stream; @override Completer? connectionCompleter; @@ -296,8 +292,7 @@ class FakeWebSocketWithConnectionError extends Fake implements WebSocket { class FakeChannelState extends Fake implements ChannelState {} -class FakePartialUpdateMemberResponse extends Fake - implements PartialUpdateMemberResponse { +class FakePartialUpdateMemberResponse extends Fake implements PartialUpdateMemberResponse { FakePartialUpdateMemberResponse({ Member? channelMember, }) : _channelMember = channelMember ?? Member(); diff --git a/packages/stream_chat/test/src/matchers.dart b/packages/stream_chat/test/src/matchers.dart index 752f980373..27a3b823ac 100644 --- a/packages/stream_chat/test/src/matchers.dart +++ b/packages/stream_chat/test/src/matchers.dart @@ -9,8 +9,7 @@ import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/user.dart'; import 'package:test/test.dart'; -Matcher isSameMultipartFileAs(MultipartFile targetFile) => - _IsSameMultipartFileAs(targetFile: targetFile); +Matcher isSameMultipartFileAs(MultipartFile targetFile) => _IsSameMultipartFileAs(targetFile: targetFile); class _IsSameMultipartFileAs extends Matcher { const _IsSameMultipartFileAs({required this.targetFile}); @@ -18,16 +17,13 @@ class _IsSameMultipartFileAs extends Matcher { final MultipartFile targetFile; @override - Description describe(Description description) => - description.add('is same multipartFile as $targetFile'); + Description describe(Description description) => description.add('is same multipartFile as $targetFile'); @override - bool matches(covariant MultipartFile file, Map matchState) => - file.length == targetFile.length; + bool matches(covariant MultipartFile file, Map matchState) => file.length == targetFile.length; } -Matcher isSameEventAs(Event targetEvent) => - _IsSameEventAs(targetEvent: targetEvent); +Matcher isSameEventAs(Event targetEvent) => _IsSameEventAs(targetEvent: targetEvent); class _IsSameEventAs extends Matcher { const _IsSameEventAs({required this.targetEvent}); @@ -35,12 +31,10 @@ class _IsSameEventAs extends Matcher { final Event targetEvent; @override - Description describe(Description description) => - description.add('is same event as $targetEvent'); + Description describe(Description description) => description.add('is same event as $targetEvent'); @override - bool matches(covariant Event event, Map matchState) => - event.type == targetEvent.type; + bool matches(covariant Event event, Map matchState) => event.type == targetEvent.type; } Matcher isSameMessageAs( @@ -51,16 +45,15 @@ Matcher isSameMessageAs( bool matchAttachments = false, bool matchAttachmentsUploadState = false, bool matchParentId = false, -}) => - _IsSameMessageAs( - targetMessage: targetMessage, - matchText: matchText, - matchReactions: matchReactions, - matchMessageState: matchMessageState, - matchAttachments: matchAttachments, - matchAttachmentsUploadState: matchAttachmentsUploadState, - matchParentId: matchParentId, - ); +}) => _IsSameMessageAs( + targetMessage: targetMessage, + matchText: matchText, + matchReactions: matchReactions, + matchMessageState: matchMessageState, + matchAttachments: matchAttachments, + matchAttachmentsUploadState: matchAttachmentsUploadState, + matchParentId: matchParentId, +); class _IsSameMessageAs extends Matcher { const _IsSameMessageAs({ @@ -82,8 +75,7 @@ class _IsSameMessageAs extends Matcher { final bool matchParentId; @override - Description describe(Description description) => - description.add('is same message as $targetMessage'); + Description describe(Description description) => description.add('is same message as $targetMessage'); @override bool matches(covariant Message message, Map matchState) { @@ -96,19 +88,13 @@ class _IsSameMessageAs extends Matcher { } if (matchReactions) { matches &= const ListEquality().equals( - message.ownReactions - ?.map((it) => '${it.type}-${it.messageId}') - .toList(), - targetMessage.ownReactions - ?.map((it) => '${it.type}-${it.messageId}') - .toList()); + message.ownReactions?.map((it) => '${it.type}-${it.messageId}').toList(), + targetMessage.ownReactions?.map((it) => '${it.type}-${it.messageId}').toList(), + ); matches &= const ListEquality().equals( - message.latestReactions - ?.map((it) => '${it.type}-${it.messageId}') - .toList(), - targetMessage.latestReactions - ?.map((it) => '${it.type}-${it.messageId}') - .toList()); + message.latestReactions?.map((it) => '${it.type}-${it.messageId}').toList(), + targetMessage.latestReactions?.map((it) => '${it.type}-${it.messageId}').toList(), + ); } if (matchAttachments) { bool matchAttachments() { @@ -142,13 +128,12 @@ Matcher isSameDraftMessageAs( bool matchText = false, bool matchAttachments = false, bool matchParentId = false, -}) => - _IsSameDraftMessageAs( - targetMessage: targetMessage, - matchText: matchText, - matchAttachments: matchAttachments, - matchParentId: matchParentId, - ); +}) => _IsSameDraftMessageAs( + targetMessage: targetMessage, + matchText: matchText, + matchAttachments: matchAttachments, + matchParentId: matchParentId, +); class _IsSameDraftMessageAs extends Matcher { const _IsSameDraftMessageAs({ @@ -164,8 +149,7 @@ class _IsSameDraftMessageAs extends Matcher { final bool matchParentId; @override - Description describe(Description description) => - description.add('is same draft message as $targetMessage'); + Description describe(Description description) => description.add('is same draft message as $targetMessage'); @override bool matches(covariant DraftMessage message, Map matchState) { @@ -199,11 +183,10 @@ class _IsSameDraftMessageAs extends Matcher { Matcher isSameAttachmentAs( Attachment targetAttachment, { bool matchUploadState = false, -}) => - _IsSameAttachmentAs( - targetAttachment: targetAttachment, - matchUploadState: matchUploadState, - ); +}) => _IsSameAttachmentAs( + targetAttachment: targetAttachment, + matchUploadState: matchUploadState, +); class _IsSameAttachmentAs extends Matcher { const _IsSameAttachmentAs({ @@ -215,8 +198,7 @@ class _IsSameAttachmentAs extends Matcher { final bool matchUploadState; @override - Description describe(Description description) => - description.add('is same attachment as $targetAttachment'); + Description describe(Description description) => description.add('is same attachment as $targetAttachment'); @override bool matches(covariant Attachment attachment, Map matchState) { @@ -236,15 +218,13 @@ class _IsSameUserAs extends Matcher { final User targetUser; @override - Description describe(Description description) => - description.add('is same user as $targetUser'); + Description describe(Description description) => description.add('is same user as $targetUser'); @override bool matches(covariant User user, Map matchState) => user.id == targetUser.id; } -Matcher isCorrectChannelFor(ChannelState channelState) => - _IsCorrectChannelFor(channelState: channelState); +Matcher isCorrectChannelFor(ChannelState channelState) => _IsCorrectChannelFor(channelState: channelState); class _IsCorrectChannelFor extends Matcher { const _IsCorrectChannelFor({required this.channelState}); @@ -252,10 +232,8 @@ class _IsCorrectChannelFor extends Matcher { final ChannelState channelState; @override - Description describe(Description description) => - description.add('is correct channel for $channelState'); + Description describe(Description description) => description.add('is correct channel for $channelState'); @override - bool matches(covariant Channel channel, Map matchState) => - channel.cid == channelState.channel?.cid; + bool matches(covariant Channel channel, Map matchState) => channel.cid == channelState.channel?.cid; } diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart index 5aed398bce..a845274bc4 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/channel_delivery_reporter.dart'; import 'package:stream_chat/src/client/client.dart'; @@ -19,6 +18,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'; @@ -67,8 +67,7 @@ class MockModerationApi extends Mock implements ModerationApi {} class MockGeneralApi extends Mock implements GeneralApi {} -class MockAttachmentFileUploader extends Mock - implements AttachmentFileUploader {} +class MockAttachmentFileUploader extends Mock implements AttachmentFileUploader {} class MockPersistenceClient extends Mock implements ChatPersistenceClient { String? _userId; @@ -106,7 +105,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 @@ -117,11 +116,10 @@ class MockStreamChatClient extends Mock implements StreamChatClient { String? eventType4, ]) { if (eventType == null || eventType == EventType.any) return eventStream; - return eventStream.where((event) => - event.type == eventType || - event.type == eventType2 || - event.type == eventType3 || - event.type == eventType4); + return eventStream.where( + (event) => + event.type == eventType || event.type == eventType2 || event.type == eventType3 || event.type == eventType4, + ); } @override @@ -132,8 +130,7 @@ class MockStreamChatClientWithPersistence extends MockStreamChatClient { ChatPersistenceClient? _persistenceClient; @override - ChatPersistenceClient get chatPersistenceClient => - _persistenceClient ??= MockPersistenceClient(); + ChatPersistenceClient get chatPersistenceClient => _persistenceClient ??= MockPersistenceClient(); @override bool get persistenceEnabled => true; @@ -166,13 +163,10 @@ class MockRetryQueueChannel extends Mock implements Channel { String? eventType3, String? eventType4, ]) { - return client - .on(eventType, eventType2, eventType3, eventType4) - .where((e) => e.cid == cid); + return client.on(eventType, eventType2, eventType3, eventType4).where((e) => e.cid == cid); } } class MockWebSocket extends Mock implements WebSocket {} -class MockChannelDeliveryReporter extends Mock - implements ChannelDeliveryReporter {} +class MockChannelDeliveryReporter extends Mock implements ChannelDeliveryReporter {} diff --git a/packages/stream_chat/test/src/utils.dart b/packages/stream_chat/test/src/utils.dart index 768d2fe296..dd3030d94c 100644 --- a/packages/stream_chat/test/src/utils.dart +++ b/packages/stream_chat/test/src/utils.dart @@ -28,5 +28,4 @@ extension IntX on num { } // Top level util function to delay the code execution -Future delay(num milliseconds) => - Future.delayed(Duration(milliseconds: milliseconds.toInt())); +Future delay(num milliseconds) => Future.delayed(Duration(milliseconds: milliseconds.toInt())); diff --git a/packages/stream_chat/test/src/ws/websocket_test.dart b/packages/stream_chat/test/src/ws/websocket_test.dart index 053a8d78ff..5509310083 100644 --- a/packages/stream_chat/test/src/ws/websocket_test.dart +++ b/packages/stream_chat/test/src/ws/websocket_test.dart @@ -24,8 +24,7 @@ void main() { WebSocketChannel channelProvider( Uri uri, { Iterable? protocols, - }) => - webSocketChannel; + }) => webSocketChannel; webSocket = WebSocket( apiKey: 'api-key', diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 32cc64588a..1f35cba22a 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,26 @@ +## 10.0.0-beta.13 + +🛑️ Breaking +- SDK Redesign Changes. For more details, please refer to the [migration guide](https://github.com/GetStream/stream-chat-flutter/blob/210ff93f955be3f85c62e860309bd9aa240a5446/migrations). + The SDK redesign introduces a fresher default UI, but also better APIs for customization of the components. + +## 10.0.0-beta.12 + +🐞 Fixed + +- Fixed regression in `emoji_code` support for reactions. + +🛑️ Breaking + +- Replaced `ArgumentError` with typed errors in `StreamAttachmentPickerController`: + `AttachmentTooLargeError` (file size exceeds limit) and `AttachmentLimitReachedError` + (attachment count exceeds limit). [[#2476]](https://github.com/GetStream/stream-chat-flutter/issues/2476) +- By default the preview of the last message will show deleted message indicator instead of filtering it out. + +For more details, please refer to the [migration guide](../../migrations/v10-migration.md). + +- Included the changes from version [`9.23.0`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 9.23.0 🐞 Fixed @@ -6,6 +29,10 @@ - Fixed audio tone bleeding into recorded voice message when playing custom feedback sound on recording start. - Fixed poll dialog AppBar back button color not being themeable. [[#2484]](https://github.com/GetStream/stream-chat-flutter/issues/2484) +## 10.0.0-beta.11 + +- Included the changes from version [`9.22.0`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 9.22.0 ✅ Added @@ -21,6 +48,10 @@ - Fixed focus randomly shifting to poll title while editing option text in poll creator. [[#2464]](https://github.com/GetStream/stream-chat-flutter/issues/2464) +## 10.0.0-beta.10 + +- Included the changes from version [`9.21.0`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 9.21.0 🐞 Fixed @@ -28,6 +59,78 @@ - Fixed StreamGallery not respecting the safe area for fullscreen media. [[#2454]](https://github.com/GetStream/stream-chat-flutter/issues/2454) +## 10.0.0-beta.9 + +✅ Added + +- Added `reactionIndicatorBuilder` parameter to `StreamMessageWidget` for customizing reaction + indicators. Users can now display reaction counts alongside emojis on mobile, matching desktop/web + behavior. Fixes [#2434](https://github.com/GetStream/stream-chat-flutter/issues/2434). + ```dart + // Example: Show reaction count next to emoji + StreamMessageWidget( + message: message, + reactionIndicatorBuilder: (context, message, onTap) { + return StreamReactionIndicator( + message: message, + onTap: onTap, + reactionIcons: StreamChatConfiguration.of(context).reactionIcons, + reactionIconBuilder: (context, icon) { + final count = message.reactionGroups?[icon.type]?.count ?? 0; + return Row( + children: [ + icon.build(context), + const SizedBox(width: 4), + Text('$count'), + ], + ); + }, + ); + }, + ) + ``` + +- Added `reactionIconBuilder` and `backgroundColor` parameters to `StreamReactionPicker`. +- Exported `StreamReactionIndicator` and related components (`ReactionIndicatorBuilder`, + `ReactionIndicatorIconBuilder`, `ReactionIndicatorIcon`, `ReactionIndicatorIconList`). + +🛑️ Breaking + +- `onAttachmentTap` callback signature changed to include `BuildContext` as first parameter and + returns `FutureOr` to indicate if handled. + ```dart + // Before + StreamMessageWidget( + message: message, + onAttachmentTap: (message, attachment) { + // Could only override - no way to fallback to default behavior + if (attachment.type == 'location') { + showLocationDialog(context, attachment); + } + // Other attachment types lost default behavior + }, + ) + + // After + StreamMessageWidget( + message: message, + onAttachmentTap: (context, message, attachment) async { + if (attachment.type == 'location') { + await showLocationDialog(context, attachment); + return true; // Handled by custom logic + } + return false; // Use default behavior for images, videos, URLs, etc. + }, + ) + ``` + +- `ReactionPickerIconList` constructor changed: removed `message` parameter, changed `reactionIcons` + type to `List`, renamed `onReactionPicked` to `onIconPicked`. + +For more details, please refer to the [migration guide](../../migrations/v10-migration.md). + +- Included the changes from version [`9.20.0`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 9.20.0 ✅ Added @@ -47,6 +150,67 @@ - Fixed high memory usage when displaying multiple image attachments. [[#2228]](https://github.com/GetStream/stream-chat-flutter/issues/2228) +## 10.0.0-beta.8 + +🛑️ Breaking + +- `onCustomAttachmentPickerResult` has been removed. Use `onAttachmentPickerResult` which returns `FutureOr` to indicate if the result was handled. + ```dart + // Before + StreamMessageInput( + onCustomAttachmentPickerResult: (result) { + // Handle custom location attachment + final location = result.data['location']; + sendLocationMessage(location); + }, + ) + + // After + StreamMessageInput( + onAttachmentPickerResult: (result) { + if (result is CustomAttachmentPickerResult) { + // Handle custom location attachment + final location = result.data['location']; + sendLocationMessage(location); + return true; // Skip default handling + } + return false; // Use default handling for built-in types + }, + ) + ``` + +- `customAttachmentPickerOptions` has been removed. Use `attachmentPickerOptionsBuilder` to modify, reorder, or extend default options. + ```dart + // Before - could only add custom options + StreamMessageInput( + customAttachmentPickerOptions: [ + TabbedAttachmentPickerOption( + key: 'location', + icon: Icon(Icons.location_on), + // ... + ), + ], + ) + + // After - can now modify, filter, or extend default options + StreamMessageInput( + attachmentPickerOptionsBuilder: (context, defaultOptions) { + return [ + ...defaultOptions, // Keep all default options + TabbedAttachmentPickerOption( + key: 'location', + icon: Icon(Icons.location_on), + // ... + ), + ]; + }, + ) + ``` + +For more details, please refer to the [migration guide](../../migrations/v10-migration.md). + +- Included the changes from version [`9.19.0`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 9.19.0 ✅ Added @@ -55,6 +219,14 @@ floating date divider. - Added spacing to typing indicator. +## 10.0.0-beta.7 + +✅ Added + +- Added support for `StreamMessageWidget.deletedMessageBuilder` to customize the deleted message UI. + +- Included the changes from version [`9.18.0`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 9.18.0 🐞 Fixed @@ -62,6 +234,15 @@ - Fixed `StreamMessageListView` not marking thread messages as read when scrolled to the bottom of the list. - Fixed `StreamMessageInput` not validating draft messages before creating/updating them. +## 10.0.0-beta.6 + +🐞 Fixed + +- Fixed users with `sendReply` capability unable to send replies in threads. +- Fixed delete/flag message dialogs executing action when dialog is dismissed without confirmation. + +- Included the changes from version [`9.17.0`](https://pub.dev/packages/stream_chat_flutter/changelog). + ## 9.17.0 ✅ Added @@ -79,6 +260,10 @@ - Fixed `GradientAvatars` for users with same-length IDs would have identical colors. [[#2369]](https://github.com/GetStream/stream-chat-flutter/issues/2369) +## 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 @@ -90,7 +275,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 @@ -104,6 +299,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 @@ -111,6 +332,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 @@ -120,6 +345,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/lib/debug/channel_page.dart b/packages/stream_chat_flutter/example/lib/debug/channel_page.dart index a1d4b6a419..a5719b1a16 100644 --- a/packages/stream_chat_flutter/example/lib/debug/channel_page.dart +++ b/packages/stream_chat_flutter/example/lib/debug/channel_page.dart @@ -39,8 +39,7 @@ class _DebugChannelPageState extends State { _channelSubscription = _channel.state!.channelStateStream.listen((state) { setState(() => _channelState = state); }); - _ownUserSubscription = - _channel.client.state.currentUserStream.listen((ownUser) { + _ownUserSubscription = _channel.client.state.currentUserStream.listen((ownUser) { setState(() => _ownUser = ownUser); }); } @@ -54,10 +53,8 @@ class _DebugChannelPageState extends State { @override Widget build(BuildContext context) { - final members = - _channelState?.members ?? _channel.state?.members ?? const []; - final mutes = - _ownUser?.mutes ?? _channel.client.state.currentUser?.mutes ?? const []; + final members = _channelState?.members ?? _channel.state?.members ?? const []; + final mutes = _ownUser?.mutes ?? _channel.client.state.currentUser?.mutes ?? const []; //SingleChildScrollView return Scaffold( appBar: AppBar( diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index 3518d0056f..a9719055b3 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -1,8 +1,6 @@ // ignore_for_file: public_member_api_docs import 'dart:async'; -import 'dart:math' as math; -import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:responsive_builder/responsive_builder.dart'; @@ -195,16 +193,16 @@ class _ChannelListPageState extends State { @override Widget build(BuildContext context) => Scaffold( - body: StreamChannelListView( - onChannelTap: widget.onTap, - controller: _listController, - itemBuilder: (context, channels, index, defaultWidget) { - return defaultWidget.copyWith( - selected: channels[index] == widget.selectedChannel, - ); - }, - ), - ); + body: StreamChannelListView( + onChannelTap: widget.onTap, + controller: _listController, + itemBuilder: (context, channels, index, defaultWidget) { + return defaultWidget.copyWith( + selected: channels[index] == widget.selectedChannel, + ); + }, + ), + ); } class ChannelPage extends StatefulWidget { @@ -254,81 +252,8 @@ class _ChannelPageState extends State { Expanded( child: StreamMessageListView( threadBuilder: (_, parent) => ThreadPage(parent: parent!), - messageBuilder: ( - context, - messageDetails, - messages, - defaultWidget, - ) { - // The threshold after which the message is considered - // swiped. - const threshold = 0.2; - - final isMyMessage = messageDetails.isMyMessage; - - // The direction in which the message can be swiped. - final swipeDirection = isMyMessage - ? SwipeDirection.endToStart // - : SwipeDirection.startToEnd; - - return Swipeable( - key: ValueKey(messageDetails.message.id), - direction: swipeDirection, - swipeThreshold: threshold, - onSwiped: (details) => reply(messageDetails.message), - backgroundBuilder: (context, details) { - // The alignment of the swipe action. - final alignment = isMyMessage - ? Alignment.centerRight // - : Alignment.centerLeft; - - // The progress of the swipe action. - final progress = - math.min(details.progress, threshold) / threshold; - - // The offset for the reply icon. - var offset = Offset.lerp( - const Offset(-24, 0), - const Offset(12, 0), - progress, - )!; - - // If the message is mine, we need to flip the offset. - if (isMyMessage) { - offset = Offset(-offset.dx, -offset.dy); - } - - final _streamTheme = StreamChatTheme.of(context); - - return Align( - alignment: alignment, - child: Transform.translate( - offset: offset, - child: Opacity( - opacity: progress, - child: SizedBox.square( - dimension: 30, - child: CustomPaint( - painter: AnimatedCircleBorderPainter( - progress: progress, - color: _streamTheme.colorTheme.borders, - ), - child: Center( - child: StreamSvgIcon( - icon: StreamSvgIcons.reply, - size: lerpDouble(0, 18, progress), - color: _streamTheme.colorTheme.accentPrimary, - ), - ), - ), - ), - ), - ), - ); - }, - child: defaultWidget.copyWith(onReplyTap: reply), - ); - }, + onReplyTap: reply, + swipeToReply: true, ), ), StreamMessageInput( diff --git a/packages/stream_chat_flutter/example/lib/split_view.dart b/packages/stream_chat_flutter/example/lib/split_view.dart index 6f75b71a69..a873f92265 100644 --- a/packages/stream_chat_flutter/example/lib/split_view.dart +++ b/packages/stream_chat_flutter/example/lib/split_view.dart @@ -113,11 +113,11 @@ class _ChannelListPageState extends State { @override Widget build(BuildContext context) => Scaffold( - body: StreamChannelListView( - onChannelTap: widget.onTap, - controller: _listController, - ), - ); + body: StreamChannelListView( + onChannelTap: widget.onTap, + controller: _listController, + ), + ); } class ChannelPage extends StatelessWidget { @@ -127,20 +127,20 @@ class ChannelPage extends StatelessWidget { @override Widget build(BuildContext context) => Navigator( - onGenerateRoute: (settings) => MaterialPageRoute( - builder: (context) => const Scaffold( - appBar: StreamChannelHeader( - showBackButton: false, - ), - body: Column( - children: [ - Expanded( - child: StreamMessageListView(), - ), - StreamMessageInput(), - ], + onGenerateRoute: (settings) => MaterialPageRoute( + builder: (context) => const Scaffold( + appBar: StreamChannelHeader( + showBackButton: false, + ), + body: Column( + children: [ + Expanded( + child: StreamMessageListView(), ), - ), + StreamMessageInput(), + ], ), - ); + ), + ), + ); } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index 8e5c52142f..d6cbd0d3a2 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -17,7 +17,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// /// We're passing a custom widget /// to [StreamChannelListView.itemBuilder]; -/// this will override the default [StreamChannelListTile] and allows you +/// this will override the default [StreamChannelListItem] and allows you /// to create one yourself. /// /// There are a couple interesting things we do in this widget: @@ -96,27 +96,27 @@ class _ChannelListPageState extends State { @override Widget build(BuildContext context) => Scaffold( - body: StreamChannelListView( - controller: _listController, - itemBuilder: _channelPreviewBuilder, - onChannelTap: (channel) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: const ChannelPage(), - ), - ), - ); - }, - ), - ); + body: StreamChannelListView( + controller: _listController, + itemBuilder: _channelPreviewBuilder, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, + ), + ); Widget _channelPreviewBuilder( BuildContext context, List channels, int index, - StreamChannelListTile defaultTile, + StreamChannelListItem defaultTile, ) { final channel = channels[index]; final lastMessage = channel.state?.messages.reversed.firstWhereOrNull( @@ -142,13 +142,11 @@ class _ChannelListPageState extends State { channel: channel, ), title: StreamChannelName( - textStyle: StreamChannelPreviewTheme.of(context).titleStyle!.copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - // ignore: deprecated_member_use - .withOpacity(opacity), - ), + textStyle: StreamChannelListItemTheme.of(context).titleStyle!.copyWith( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis + // ignore: deprecated_member_use + .withOpacity(opacity), + ), channel: channel, ), subtitle: Text(subtitle), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index 905753f7e1..f271dae858 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -82,20 +82,20 @@ class _ChannelListPageState extends State { @override Widget build(BuildContext context) => Scaffold( - body: StreamChannelListView( - controller: _listController, - onChannelTap: (channel) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: const ChannelPage(), - ), - ), - ); - }, - ), - ); + body: StreamChannelListView( + controller: _listController, + onChannelTap: (channel) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ), + ); + }, + ), + ); } class ChannelPage extends StatelessWidget { diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index ef205abfe5..d667748af8 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -128,13 +128,10 @@ class ChannelPage extends StatelessWidget { Widget _messageBuilder( BuildContext context, - MessageDetails details, - List messages, - StreamMessageWidget _, + Message message, + StreamMessageWidgetProps defaultProps, ) { - final message = details.message; - final isCurrentUser = - StreamChat.of(context).currentUser!.id == message.user!.id; + final isCurrentUser = StreamChat.of(context).currentUser!.id == message.user!.id; final textAlign = isCurrentUser ? TextAlign.right : TextAlign.left; final color = isCurrentUser ? Colors.blueGrey : Colors.blue; diff --git a/packages/stream_chat_flutter/example/linux/flutter/generated_plugins.cmake b/packages/stream_chat_flutter/example/linux/flutter/generated_plugins.cmake index 516e21cdf6..809de92757 100644 --- a/packages/stream_chat_flutter/example/linux/flutter/generated_plugins.cmake +++ b/packages/stream_chat_flutter/example/linux/flutter/generated_plugins.cmake @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index 7bb126031d..f3e8eed3e9 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -16,8 +16,8 @@ version: 1.0.0+1 # 2. Add it to the melos.yaml file for future updates. environment: - sdk: ^3.6.2 - flutter: ">=3.27.4" + sdk: ^3.10.0 + flutter: ">=3.38.1" dependencies: collection: ^1.17.2 @@ -25,9 +25,9 @@ dependencies: flutter: sdk: flutter responsive_builder: ^0.7.0 - stream_chat_flutter: ^9.23.0 - stream_chat_localizations: ^9.23.0 - stream_chat_persistence: ^9.23.0 + stream_chat_flutter: ^10.0.0-beta.13 + stream_chat_localizations: ^10.0.0-beta.13 + stream_chat_persistence: ^10.0.0-beta.13 flutter: uses-material-design: true diff --git a/packages/stream_chat_flutter/example/windows/flutter/generated_plugins.cmake b/packages/stream_chat_flutter/example/windows/flutter/generated_plugins.cmake index 8d857477c7..0fc96fdcf1 100644 --- a/packages/stream_chat_flutter/example/windows/flutter/generated_plugins.cmake +++ b/packages/stream_chat_flutter/example/windows/flutter/generated_plugins.cmake @@ -16,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/packages/stream_chat_flutter/lib/conditional_parent_builder/conditional_parent_builder.dart b/packages/stream_chat_flutter/lib/conditional_parent_builder/conditional_parent_builder.dart index 8314757de3..757be609e2 100644 --- a/packages/stream_chat_flutter/lib/conditional_parent_builder/conditional_parent_builder.dart +++ b/packages/stream_chat_flutter/lib/conditional_parent_builder/conditional_parent_builder.dart @@ -5,10 +5,11 @@ import 'package:flutter/material.dart'; /// {@template parentBuilder} /// A function that provides the [BuildContext] and the [child] widget. /// {@endtemplate} -typedef ParentBuilder = Widget Function( - BuildContext context, - Widget child, -); +typedef ParentBuilder = + Widget Function( + BuildContext context, + Widget child, + ); /// {@template conditionalParentBuilder} /// A widget that allows developers to conditionally wrap the [child] widget diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget.dart index f820eb2c13..5bc8129188 100644 --- a/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget.dart +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget.dart @@ -26,14 +26,11 @@ class DesktopWidget extends DesktopWidgetBase { final PlatformBuilder? linux; @override - Widget createMacosWidget(BuildContext context) => - macOS?.call(context) ?? const Empty(); + Widget createMacosWidget(BuildContext context) => macOS?.call(context) ?? const Empty(); @override - Widget createWindowsWidget(BuildContext context) => - windows?.call(context) ?? const Empty(); + Widget createWindowsWidget(BuildContext context) => windows?.call(context) ?? const Empty(); @override - Widget createLinuxWidget(BuildContext context) => - linux?.call(context) ?? const Empty(); + Widget createLinuxWidget(BuildContext context) => linux?.call(context) ?? const Empty(); } diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_base.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_base.dart index f5a42a7bd7..9a1a822dfc 100644 --- a/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_base.dart +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_base.dart @@ -3,9 +3,10 @@ import 'package:flutter/material.dart' show Theme; import 'package:flutter/widgets.dart'; /// A generic widget builder function. -typedef PlatformBuilder = T Function( - BuildContext context, -); +typedef PlatformBuilder = + T Function( + BuildContext context, + ); /// An abstract class used as a building block for creating /// [DesktopPlatformWidget]s. @@ -21,8 +22,7 @@ typedef PlatformBuilder = T Function( /// * M = macOS /// * W = Windows /// * L = Linux -abstract class DesktopWidgetBase extends StatelessWidget { +abstract class DesktopWidgetBase extends StatelessWidget { /// Builds a [DesktopWidgetBase]. const DesktopWidgetBase({super.key}); diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_builder.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_builder.dart index 923678617d..5aafeee047 100644 --- a/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_builder.dart +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/src/desktop_widget_builder.dart @@ -2,10 +2,11 @@ import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/platform_widget_builder/src/desktop_widget.dart'; /// A widget-building function that includes the child widget. -typedef DesktopTargetBuilder = Widget? Function( - BuildContext context, - Widget? child, -)?; +typedef DesktopTargetBuilder = + Widget? Function( + BuildContext context, + Widget? child, + )?; /// A widget that utilizes [DesktopWidgetBuilder]s to build different widgets /// for each specified desktop platform. diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget.dart index ff558ef1e7..426556b08c 100644 --- a/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget.dart +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget.dart @@ -26,14 +26,11 @@ class PlatformWidget extends PlatformWidgetBase { final PlatformBuilder? web; @override - Widget createDesktopWidget(BuildContext context) => - desktop?.call(context) ?? const Empty(); + Widget createDesktopWidget(BuildContext context) => desktop?.call(context) ?? const Empty(); @override - Widget createMobileWidget(BuildContext context) => - mobile?.call(context) ?? const Empty(); + Widget createMobileWidget(BuildContext context) => mobile?.call(context) ?? const Empty(); @override - Widget createWebWidget(BuildContext context) => - web?.call(context) ?? const Empty(); + Widget createWebWidget(BuildContext context) => web?.call(context) ?? const Empty(); } diff --git a/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_base.dart b/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_base.dart index 0487bd4396..8bf30cc361 100644 --- a/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_base.dart +++ b/packages/stream_chat_flutter/lib/platform_widget_builder/src/platform_widget_base.dart @@ -2,9 +2,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; /// A generic widget builder function. -typedef PlatformBuilder = T Function( - BuildContext context, -); +typedef PlatformBuilder = + T Function( + BuildContext context, + ); /// An abstract class used as a building block for creating [PlatformWidget]s. /// @@ -21,8 +22,7 @@ typedef PlatformBuilder = T Function( /// * M = Mobile /// * D = Desktop /// * W = Web -abstract class PlatformWidgetBase extends StatelessWidget { +abstract class PlatformWidgetBase extends StatelessWidget { /// Builds a [PlatformWidgetBase]. const PlatformWidgetBase({ super.key, 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..ef5e4db01c 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 @@ -2,10 +2,11 @@ import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget.dart'; /// A widget-building function that includes the child widget. -typedef PlatformTargetBuilder = Widget? Function( - BuildContext context, - Widget? child, -)?; +typedef PlatformTargetBuilder = + Widget? Function( + BuildContext context, + Widget? child, + )?; /// A widget that utilizes [PlatformTargetBuilder]s to build different widgets /// for each specified platform. @@ -25,6 +26,7 @@ class PlatformWidgetBuilder extends StatelessWidget { this.mobile, this.desktop, this.web, + this.desktopOrWeb, }); /// The child widget. @@ -39,12 +41,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/scrollable_positioned_list/src/element_registry.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/element_registry.dart index 03f274f387..21d3b59446 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/element_registry.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/element_registry.dart @@ -38,9 +38,9 @@ class _RegistryWidgetState extends State { @override Widget build(BuildContext context) => _InheritedRegistryWidget( - state: this, - child: widget.child, - ); + state: this, + child: widget.child, + ); } class _InheritedRegistryWidget extends InheritedWidget { @@ -66,30 +66,25 @@ class _RegisteredElement extends ProxyElement { @override void mount(Element? parent, dynamic newSlot) { super.mount(parent, newSlot); - final _inheritedRegistryWidget = - dependOnInheritedWidgetOfExactType<_InheritedRegistryWidget>()!; + final _inheritedRegistryWidget = dependOnInheritedWidgetOfExactType<_InheritedRegistryWidget>()!; _registryWidgetState = _inheritedRegistryWidget.state; _registryWidgetState.registeredElements.add(this); - _registryWidgetState.widget.elementNotifier?.value = - _registryWidgetState.registeredElements; + _registryWidgetState.widget.elementNotifier?.value = _registryWidgetState.registeredElements; } @override void didChangeDependencies() { super.didChangeDependencies(); - final _inheritedRegistryWidget = - dependOnInheritedWidgetOfExactType<_InheritedRegistryWidget>()!; + final _inheritedRegistryWidget = dependOnInheritedWidgetOfExactType<_InheritedRegistryWidget>()!; _registryWidgetState = _inheritedRegistryWidget.state; _registryWidgetState.registeredElements.add(this); - _registryWidgetState.widget.elementNotifier?.value = - _registryWidgetState.registeredElements; + _registryWidgetState.widget.elementNotifier?.value = _registryWidgetState.registeredElements; } @override void unmount() { _registryWidgetState.registeredElements.remove(this); - _registryWidgetState.widget.elementNotifier?.value = - _registryWidgetState.registeredElements; + _registryWidgetState.widget.elementNotifier?.value = _registryWidgetState.registeredElements; super.unmount(); } } diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_listener.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_listener.dart index d2752a00b2..808a2906fd 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_listener.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/item_positions_listener.dart @@ -51,9 +51,7 @@ class ItemPosition { itemTrailingEdge == other.itemTrailingEdge; @override - int get hashCode => - 31 * (31 * (index.hashCode + 7) + itemLeadingEdge.hashCode) + - itemTrailingEdge.hashCode; + int get hashCode => 31 * (31 * (index.hashCode + 7) + itemLeadingEdge.hashCode) + itemTrailingEdge.hashCode; @override String toString() => diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart index df08329cf8..a04f7c3397 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart @@ -47,9 +47,9 @@ class PositionedList extends StatefulWidget { this.findChildIndexCallback, this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, }) : assert( - (positionedIndex == 0) || (positionedIndex < itemCount), - 'positionedIndex must be 0 or a value less than itemCount', - ); + (positionedIndex == 0) || (positionedIndex < itemCount), + 'positionedIndex must be 0 or a value less than itemCount', + ); /// Number of items the [itemBuilder] can produce. final int itemCount; @@ -186,81 +186,78 @@ class _PositionedListState extends State { @override Widget build(BuildContext context) => RegistryWidget( - elementNotifier: registeredElements, - child: UnboundedCustomScrollView( - anchor: widget.alignment, - center: _centerKey, - controller: scrollController, - scrollDirection: widget.scrollDirection, - reverse: widget.reverse, - cacheExtent: widget.cacheExtent, - physics: widget.physics, - shrinkWrap: widget.shrinkWrap, - semanticChildCount: widget.semanticChildCount ?? widget.itemCount, - keyboardDismissBehavior: widget.keyboardDismissBehavior, - slivers: [ - if (widget.positionedIndex > 0) - SliverPadding( - padding: _leadingSliverPadding, - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => widget.separatorBuilder == null - ? _buildItem(widget.positionedIndex - (index + 1)) - : _buildSeparatedListElement( - widget.positionedIndex * 2 - (index + 1), - ), - childCount: widget.separatorBuilder == null - ? widget.positionedIndex - : widget.positionedIndex * 2, - addSemanticIndexes: false, - addRepaintBoundaries: widget.addRepaintBoundaries, - addAutomaticKeepAlives: widget.addAutomaticKeepAlives, - findChildIndexCallback: widget.findChildIndexCallback, - ), - ), - ), - SliverPadding( - key: _centerKey, - padding: _centerSliverPadding, - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => widget.separatorBuilder == null - ? _buildItem(index + widget.positionedIndex) - : _buildSeparatedListElement( - index + widget.positionedIndex * 2, - ), - childCount: widget.itemCount != 0 ? 1 : 0, - addSemanticIndexes: false, - addRepaintBoundaries: widget.addRepaintBoundaries, - addAutomaticKeepAlives: widget.addAutomaticKeepAlives, - findChildIndexCallback: widget.findChildIndexCallback, - ), + elementNotifier: registeredElements, + child: UnboundedCustomScrollView( + anchor: widget.alignment, + center: _centerKey, + controller: scrollController, + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + cacheExtent: widget.cacheExtent, + physics: widget.physics, + shrinkWrap: widget.shrinkWrap, + semanticChildCount: widget.semanticChildCount ?? widget.itemCount, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + slivers: [ + if (widget.positionedIndex > 0) + SliverPadding( + padding: _leadingSliverPadding, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => widget.separatorBuilder == null + ? _buildItem(widget.positionedIndex - (index + 1)) + : _buildSeparatedListElement( + widget.positionedIndex * 2 - (index + 1), + ), + childCount: widget.separatorBuilder == null ? widget.positionedIndex : widget.positionedIndex * 2, + addSemanticIndexes: false, + addRepaintBoundaries: widget.addRepaintBoundaries, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + findChildIndexCallback: widget.findChildIndexCallback, ), ), - if (widget.positionedIndex >= 0 && - widget.positionedIndex < widget.itemCount - 1) - SliverPadding( - padding: _trailingSliverPadding, - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => widget.separatorBuilder == null - ? _buildItem(index + widget.positionedIndex + 1) - : _buildSeparatedListElement( - index + widget.positionedIndex * 2 + 1, - ), - childCount: widget.separatorBuilder == null - ? widget.itemCount - widget.positionedIndex - 1 - : 2 * (widget.itemCount - widget.positionedIndex - 1), - addSemanticIndexes: false, - addRepaintBoundaries: widget.addRepaintBoundaries, - addAutomaticKeepAlives: widget.addAutomaticKeepAlives, - findChildIndexCallback: widget.findChildIndexCallback, - ), - ), - ), - ], + ), + SliverPadding( + key: _centerKey, + padding: _centerSliverPadding, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => widget.separatorBuilder == null + ? _buildItem(index + widget.positionedIndex) + : _buildSeparatedListElement( + index + widget.positionedIndex * 2, + ), + childCount: widget.itemCount != 0 ? 1 : 0, + addSemanticIndexes: false, + addRepaintBoundaries: widget.addRepaintBoundaries, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + findChildIndexCallback: widget.findChildIndexCallback, + ), + ), ), - ); + if (widget.positionedIndex >= 0 && widget.positionedIndex < widget.itemCount - 1) + SliverPadding( + padding: _trailingSliverPadding, + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => widget.separatorBuilder == null + ? _buildItem(index + widget.positionedIndex + 1) + : _buildSeparatedListElement( + index + widget.positionedIndex * 2 + 1, + ), + childCount: widget.separatorBuilder == null + ? widget.itemCount - widget.positionedIndex - 1 + : 2 * (widget.itemCount - widget.positionedIndex - 1), + addSemanticIndexes: false, + addRepaintBoundaries: widget.addRepaintBoundaries, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + findChildIndexCallback: widget.findChildIndexCallback, + ), + ), + ), + ], + ), + ); Widget _buildSeparatedListElement(int index) { if (index.isEven) { @@ -274,63 +271,51 @@ class _PositionedListState extends State { final child = widget.itemBuilder(context, index); return RegisteredElementWidget( key: IndexedKey(child.key, index), - child: widget.addSemanticIndexes - ? IndexedSemantics(index: index, child: child) - : child, + child: widget.addSemanticIndexes ? IndexedSemantics(index: index, child: child) : child, ); } EdgeInsets get _leadingSliverPadding => (widget.scrollDirection == Axis.vertical ? widget.reverse - ? widget.padding?.copyWith(top: 0) - : widget.padding?.copyWith(bottom: 0) + ? widget.padding?.copyWith(top: 0) + : widget.padding?.copyWith(bottom: 0) : widget.reverse - ? widget.padding?.copyWith(left: 0) - : widget.padding?.copyWith(right: 0)) ?? + ? widget.padding?.copyWith(left: 0) + : widget.padding?.copyWith(right: 0)) ?? EdgeInsets.zero; EdgeInsets get _centerSliverPadding => widget.scrollDirection == Axis.vertical ? widget.reverse - ? widget.padding?.copyWith( - top: widget.positionedIndex == widget.itemCount - 1 - ? widget.padding!.top - : 0, - bottom: - widget.positionedIndex == 0 ? widget.padding!.bottom : 0, - ) ?? - EdgeInsets.zero - : widget.padding?.copyWith( - top: widget.positionedIndex == 0 ? widget.padding!.top : 0, - bottom: widget.positionedIndex == widget.itemCount - 1 - ? widget.padding!.bottom - : 0, - ) ?? - EdgeInsets.zero + ? widget.padding?.copyWith( + top: widget.positionedIndex == widget.itemCount - 1 ? widget.padding!.top : 0, + bottom: widget.positionedIndex == 0 ? widget.padding!.bottom : 0, + ) ?? + EdgeInsets.zero + : widget.padding?.copyWith( + top: widget.positionedIndex == 0 ? widget.padding!.top : 0, + bottom: widget.positionedIndex == widget.itemCount - 1 ? widget.padding!.bottom : 0, + ) ?? + EdgeInsets.zero : widget.reverse - ? widget.padding?.copyWith( - left: widget.positionedIndex == widget.itemCount - 1 - ? widget.padding!.left - : 0, - right: widget.positionedIndex == 0 ? widget.padding!.right : 0, - ) ?? - EdgeInsets.zero - : widget.padding?.copyWith( - left: widget.positionedIndex == 0 ? widget.padding!.left : 0, - right: widget.positionedIndex == widget.itemCount - 1 - ? widget.padding!.right - : 0, - ) ?? - EdgeInsets.zero; - - EdgeInsets get _trailingSliverPadding => - widget.scrollDirection == Axis.vertical - ? widget.reverse - ? widget.padding?.copyWith(bottom: 0) ?? EdgeInsets.zero - : widget.padding?.copyWith(top: 0) ?? EdgeInsets.zero - : widget.reverse - ? widget.padding?.copyWith(right: 0) ?? EdgeInsets.zero - : widget.padding?.copyWith(left: 0) ?? EdgeInsets.zero; + ? widget.padding?.copyWith( + left: widget.positionedIndex == widget.itemCount - 1 ? widget.padding!.left : 0, + right: widget.positionedIndex == 0 ? widget.padding!.right : 0, + ) ?? + EdgeInsets.zero + : widget.padding?.copyWith( + left: widget.positionedIndex == 0 ? widget.padding!.left : 0, + right: widget.positionedIndex == widget.itemCount - 1 ? widget.padding!.right : 0, + ) ?? + EdgeInsets.zero; + + EdgeInsets get _trailingSliverPadding => widget.scrollDirection == Axis.vertical + ? widget.reverse + ? widget.padding?.copyWith(bottom: 0) ?? EdgeInsets.zero + : widget.padding?.copyWith(top: 0) ?? EdgeInsets.zero + : widget.reverse + ? widget.padding?.copyWith(right: 0) ?? EdgeInsets.zero + : widget.padding?.copyWith(left: 0) ?? EdgeInsets.zero; void _schedulePositionNotificationUpdate() { if (!updateScheduled) { @@ -361,34 +346,34 @@ class _PositionedListState extends State { if (widget.scrollDirection == Axis.vertical) { final reveal = viewport!.getOffsetToReveal(box, 0).offset; if (!reveal.isFinite) continue; - final itemOffset = - reveal - viewport.offset.pixels + anchor * viewport.size.height; - positions.add(ItemPosition( - index: key.index, - itemLeadingEdge: itemOffset.round() / - scrollController.position.viewportDimension, - itemTrailingEdge: (itemOffset + box.size.height).round() / - scrollController.position.viewportDimension, - )); + final itemOffset = reveal - viewport.offset.pixels + anchor * viewport.size.height; + positions.add( + ItemPosition( + index: key.index, + itemLeadingEdge: itemOffset.round() / scrollController.position.viewportDimension, + itemTrailingEdge: (itemOffset + box.size.height).round() / scrollController.position.viewportDimension, + ), + ); } else { - final itemOffset = - box.localToGlobal(Offset.zero, ancestor: viewport).dx; + final itemOffset = box.localToGlobal(Offset.zero, ancestor: viewport).dx; if (!itemOffset.isFinite) continue; - positions.add(ItemPosition( - index: key.index, - itemLeadingEdge: (widget.reverse - ? scrollController.position.viewportDimension - - (itemOffset + box.size.width) - : itemOffset) - .round() / - scrollController.position.viewportDimension, - itemTrailingEdge: (widget.reverse - ? scrollController.position.viewportDimension - - itemOffset - : (itemOffset + box.size.width)) - .round() / - scrollController.position.viewportDimension, - )); + positions.add( + ItemPosition( + index: key.index, + itemLeadingEdge: + (widget.reverse + ? scrollController.position.viewportDimension - (itemOffset + box.size.width) + : itemOffset) + .round() / + scrollController.position.viewportDimension, + itemTrailingEdge: + (widget.reverse + ? scrollController.position.viewportDimension - itemOffset + : (itemOffset + box.size.width)) + .round() / + scrollController.position.viewportDimension, + ), + ); } } widget.itemPositionsNotifier?.itemPositions.value = positions; diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart index 911512495b..160e8aa240 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart @@ -28,9 +28,9 @@ class UnboundedCustomScrollView extends CustomScrollView { super.semanticChildCount, super.dragStartBehavior, super.keyboardDismissBehavior, - }) : _shrinkWrap = shrinkWrap, - _anchor = anchor, - super(shrinkWrap: false); + }) : _shrinkWrap = shrinkWrap, + _anchor = anchor, + super(shrinkWrap: false); final bool _shrinkWrap; diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart index af44d52562..e160dc6148 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart @@ -52,8 +52,8 @@ class ScrollablePositionedList extends StatefulWidget { this.minCacheExtent, this.findChildIndexCallback, this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, - }) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?, - separatorBuilder = null; + }) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?, + separatorBuilder = null; /// Create a [ScrollablePositionedList] whose items are provided by /// [itemBuilder] and separators provided by [separatorBuilder]. @@ -278,8 +278,7 @@ class ItemScrollController { } } -class _ScrollablePositionedListState extends State - with TickerProviderStateMixin { +class _ScrollablePositionedListState extends State with TickerProviderStateMixin { /// Details for the primary (active) [ListView]. _ListDisplayDetails primary = _ListDisplayDetails(const ValueKey('Ping')); @@ -318,10 +317,8 @@ class _ScrollablePositionedListState extends State @override void dispose() { - primary.itemPositionsNotifier.itemPositions - .removeListener(_updatePositions); - secondary.itemPositionsNotifier.itemPositions - .removeListener(_updatePositions); + primary.itemPositionsNotifier.itemPositions.removeListener(_updatePositions); + secondary.itemPositionsNotifier.itemPositions.removeListener(_updatePositions); _animationController?.dispose(); super.dispose(); } @@ -431,12 +428,9 @@ class _ScrollablePositionedListState extends State } double _cacheExtent(BoxConstraints constraints) => max( - (widget.scrollDirection == Axis.vertical - ? constraints.maxHeight - : constraints.maxWidth) * - _screenScrollCount, - widget.minCacheExtent ?? 0, - ); + (widget.scrollDirection == Axis.vertical ? constraints.maxHeight : constraints.maxWidth) * _screenScrollCount, + widget.minCacheExtent ?? 0, + ); void _jumpTo({required int index, required double alignment}) { _stopScroll(canceled: true); @@ -494,14 +488,12 @@ class _ScrollablePositionedListState extends State required List opacityAnimationWeights, }) async { final direction = index > primary.target ? 1 : -1; - final itemPosition = - primary.itemPositionsNotifier.itemPositions.value.firstWhereOrNull( + final itemPosition = primary.itemPositionsNotifier.itemPositions.value.firstWhereOrNull( (ItemPosition itemPosition) => itemPosition.index == index, ); if (itemPosition != null) { // Scroll directly. - final localScrollAmount = itemPosition.itemLeadingEdge * - primary.scrollController.position.viewportDimension; + final localScrollAmount = itemPosition.itemLeadingEdge * primary.scrollController.position.viewportDimension; await primary.scrollController.animateTo( primary.scrollController.offset + localScrollAmount - @@ -510,31 +502,29 @@ class _ScrollablePositionedListState extends State curve: curve, ); } else { - final scrollAmount = _screenScrollCount * - primary.scrollController.position.viewportDimension; + final scrollAmount = _screenScrollCount * primary.scrollController.position.viewportDimension; final startCompleter = Completer(); final endCompleter = Completer(); startAnimationCallback = () { SchedulerBinding.instance.addPostFrameCallback((_) { startAnimationCallback = () {}; _animationController?.dispose(); - _animationController = - AnimationController(vsync: this, duration: duration)..forward(); - opacity.parent = _opacityAnimation(opacityAnimationWeights) - .animate(_animationController!); - secondary.scrollController.jumpTo(-direction * - (_screenScrollCount * - primary.scrollController.position.viewportDimension - - alignment * - secondary.scrollController.position.viewportDimension)); - - startCompleter.complete(primary.scrollController.animateTo( - primary.scrollController.offset + direction * scrollAmount, - duration: duration, - curve: curve, - )); - endCompleter.complete(secondary.scrollController - .animateTo(0, duration: duration, curve: curve)); + _animationController = AnimationController(vsync: this, duration: duration)..forward(); + opacity.parent = _opacityAnimation(opacityAnimationWeights).animate(_animationController!); + secondary.scrollController.jumpTo( + -direction * + (_screenScrollCount * primary.scrollController.position.viewportDimension - + alignment * secondary.scrollController.position.viewportDimension), + ); + + startCompleter.complete( + primary.scrollController.animateTo( + primary.scrollController.offset + direction * scrollAmount, + duration: duration, + curve: curve, + ), + ); + endCompleter.complete(secondary.scrollController.animateTo(0, duration: duration, curve: curve)); }); }; setState(() { @@ -599,14 +589,13 @@ class _ScrollablePositionedListState extends State } void _updatePositions() { - final itemPositions = primary.itemPositionsNotifier.itemPositions.value - .where((ItemPosition position) => - position.itemLeadingEdge < 1 && position.itemTrailingEdge > 0); + final itemPositions = primary.itemPositionsNotifier.itemPositions.value.where( + (ItemPosition position) => position.itemLeadingEdge < 1 && position.itemTrailingEdge > 0, + ); if (itemPositions.isNotEmpty) { PageStorage.of(context).writeState( context, - itemPositions.reduce((value, element) => - value.itemLeadingEdge < element.itemLeadingEdge ? value : element), + itemPositions.reduce((value, element) => value.itemLeadingEdge < element.itemLeadingEdge ? value : element), ); } widget.itemPositionsNotifier?.itemPositions.value = itemPositions; diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/viewport.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/viewport.dart index aac9acc9e0..dac5e20f19 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/viewport.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/viewport.dart @@ -38,8 +38,7 @@ class UnboundedViewport extends Viewport { RenderViewport createRenderObject(BuildContext context) { return UnboundedRenderViewport( axisDirection: axisDirection, - crossAxisDirection: crossAxisDirection ?? - Viewport.getDefaultCrossAxisDirection(context, axisDirection), + crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection), anchor: anchor, offset: offset, cacheExtent: cacheExtent, @@ -233,10 +232,8 @@ class UnboundedRenderViewport extends RenderViewport { // to the zero scroll offset (the line between the forward slivers and the // reverse slivers). final centerOffset = mainAxisExtent * anchor - correctedOffset; - final reverseDirectionRemainingPaintExtent = - centerOffset.clamp(0.0, mainAxisExtent); - final forwardDirectionRemainingPaintExtent = - (mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent); + final reverseDirectionRemainingPaintExtent = centerOffset.clamp(0.0, mainAxisExtent); + final forwardDirectionRemainingPaintExtent = (mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent); switch (cacheExtentStyle) { case CacheExtentStyle.pixel: @@ -249,10 +246,8 @@ class UnboundedRenderViewport extends RenderViewport { final fullCacheExtent = mainAxisExtent + 2 * _calculatedCacheExtent!; final centerCacheOffset = centerOffset + _calculatedCacheExtent!; - final reverseDirectionRemainingCacheExtent = - centerCacheOffset.clamp(0.0, fullCacheExtent); - final forwardDirectionRemainingCacheExtent = - (fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent); + final reverseDirectionRemainingCacheExtent = centerCacheOffset.clamp(0.0, fullCacheExtent); + final forwardDirectionRemainingCacheExtent = (fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent); final leadingNegativeChild = childBefore(center!); @@ -269,8 +264,7 @@ class UnboundedRenderViewport extends RenderViewport { growthDirection: GrowthDirection.reverse, advance: childBefore, remainingCacheExtent: reverseDirectionRemainingCacheExtent, - cacheOrigin: (mainAxisExtent - centerOffset) - .clamp(-_calculatedCacheExtent!, 0.0), + cacheOrigin: (mainAxisExtent - centerOffset).clamp(-_calculatedCacheExtent!, 0.0), ); if (result != 0.0) return -result; } @@ -280,9 +274,7 @@ class UnboundedRenderViewport extends RenderViewport { child: center, scrollOffset: math.max(0, -centerOffset), overlap: leadingNegativeChild == null ? math.min(0, -centerOffset) : 0.0, - layoutOffset: centerOffset >= mainAxisExtent - ? centerOffset - : reverseDirectionRemainingPaintExtent, + layoutOffset: centerOffset >= mainAxisExtent ? centerOffset : reverseDirectionRemainingPaintExtent, remainingPaintExtent: forwardDirectionRemainingPaintExtent, mainAxisExtent: mainAxisExtent, crossAxisExtent: crossAxisExtent, diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/wrapping.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/wrapping.dart index c3a8129ce7..e746813cae 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/wrapping.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/wrapping.dart @@ -59,8 +59,7 @@ class CustomShrinkWrappingViewport extends CustomViewport { CustomRenderShrinkWrappingViewport createRenderObject(BuildContext context) { return CustomRenderShrinkWrappingViewport( axisDirection: axisDirection, - crossAxisDirection: crossAxisDirection ?? - Viewport.getDefaultCrossAxisDirection(context, axisDirection), + crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection), offset: offset, anchor: anchor, cacheExtent: cacheExtent, @@ -74,8 +73,7 @@ class CustomShrinkWrappingViewport extends CustomViewport { ) { renderObject ..axisDirection = axisDirection - ..crossAxisDirection = crossAxisDirection ?? - Viewport.getDefaultCrossAxisDirection(context, axisDirection) + ..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection) ..anchor = anchor ..offset = offset ..cacheExtent = cacheExtent @@ -267,10 +265,8 @@ class CustomRenderShrinkWrappingViewport extends CustomRenderViewport { final maxScrollOffset = math.max(math.min(0, top), bottom); final minScrollOffset = math.min(top, maxScrollOffset); - final didAcceptViewportDimension = - offset.applyViewportDimension(effectiveExtent); - final didAcceptContentDimension = - offset.applyContentDimensions(minScrollOffset, maxScrollOffset); + final didAcceptViewportDimension = offset.applyViewportDimension(effectiveExtent); + final didAcceptContentDimension = offset.applyContentDimensions(minScrollOffset, maxScrollOffset); if (didAcceptViewportDimension && didAcceptContentDimension) { break; } @@ -278,12 +274,10 @@ class CustomRenderShrinkWrappingViewport extends CustomRenderViewport { } while (true); switch (axis) { case Axis.vertical: - size = - constraints.constrainDimensions(crossAxisExtent, effectiveExtent); + size = constraints.constrainDimensions(crossAxisExtent, effectiveExtent); break; case Axis.horizontal: - size = - constraints.constrainDimensions(effectiveExtent, crossAxisExtent); + size = constraints.constrainDimensions(effectiveExtent, crossAxisExtent); break; } } @@ -313,10 +307,8 @@ class CustomRenderShrinkWrappingViewport extends CustomRenderViewport { // to the zero scroll offset (the line between the forward slivers and the // reverse slivers). final centerOffset = mainAxisExtent * anchor - correctedOffset; - final reverseDirectionRemainingPaintExtent = - centerOffset.clamp(0.0, mainAxisExtent); - final forwardDirectionRemainingPaintExtent = - (mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent); + final reverseDirectionRemainingPaintExtent = centerOffset.clamp(0.0, mainAxisExtent); + final forwardDirectionRemainingPaintExtent = (mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent); switch (cacheExtentStyle) { case CacheExtentStyle.pixel: @@ -329,10 +321,8 @@ class CustomRenderShrinkWrappingViewport extends CustomRenderViewport { final fullCacheExtent = mainAxisExtent + 2 * _calculatedCacheExtent!; final centerCacheOffset = centerOffset + _calculatedCacheExtent!; - final reverseDirectionRemainingCacheExtent = - centerCacheOffset.clamp(0.0, fullCacheExtent); - final forwardDirectionRemainingCacheExtent = - (fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent); + final reverseDirectionRemainingCacheExtent = centerCacheOffset.clamp(0.0, fullCacheExtent); + final forwardDirectionRemainingCacheExtent = (fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent); final leadingNegativeChild = childBefore(center!); @@ -349,8 +339,7 @@ class CustomRenderShrinkWrappingViewport extends CustomRenderViewport { growthDirection: GrowthDirection.reverse, advance: childBefore, remainingCacheExtent: reverseDirectionRemainingCacheExtent, - cacheOrigin: (mainAxisExtent - centerOffset) - .clamp(-_calculatedCacheExtent!, 0.0), + cacheOrigin: (mainAxisExtent - centerOffset).clamp(-_calculatedCacheExtent!, 0.0), ); if (result != 0.0) return -result; } @@ -360,9 +349,7 @@ class CustomRenderShrinkWrappingViewport extends CustomRenderViewport { child: center, scrollOffset: math.max(0, -centerOffset), overlap: leadingNegativeChild == null ? math.min(0, -centerOffset) : 0.0, - layoutOffset: centerOffset >= mainAxisExtent - ? centerOffset - : reverseDirectionRemainingPaintExtent, + layoutOffset: centerOffset >= mainAxisExtent ? centerOffset : reverseDirectionRemainingPaintExtent, remainingPaintExtent: forwardDirectionRemainingPaintExtent, mainAxisExtent: mainAxisExtent, crossAxisExtent: crossAxisExtent, @@ -450,16 +437,15 @@ abstract class CustomViewport extends MultiChildRenderObjectWidget { this.cacheExtentStyle = CacheExtentStyle.pixel, this.clipBehavior = Clip.hardEdge, List slivers = const [], - }) : assert( - center == null || - slivers.where((Widget child) => child.key == center).length == 1, - 'There should be at most one child with the same key as the center child: $center', - ), - assert( - cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null, - 'A cacheExtent is required when using cacheExtentStyle.viewport', - ), - super(children: slivers); + }) : assert( + center == null || slivers.where((Widget child) => child.key == center).length == 1, + 'There should be at most one child with the same key as the center child: $center', + ), + assert( + cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null, + 'A cacheExtent is required when using cacheExtentStyle.viewport', + ), + super(children: slivers); /// The direction in which the [offset]'s [ViewportOffset.pixels] increases. /// @@ -533,24 +519,24 @@ abstract class CustomViewport extends MultiChildRenderObjectWidget { ) { switch (axisDirection) { case AxisDirection.up: - assert(debugCheckHasDirectionality( - context, - why: - "to determine the cross-axis direction when the viewport has an 'up' axisDirection", - alternative: - "Alternatively, consider specifying the 'crossAxisDirection' argument on the Viewport.", - )); + assert( + debugCheckHasDirectionality( + context, + why: "to determine the cross-axis direction when the viewport has an 'up' axisDirection", + alternative: "Alternatively, consider specifying the 'crossAxisDirection' argument on the Viewport.", + ), + ); return textDirectionToAxisDirection(Directionality.of(context)); case AxisDirection.right: return AxisDirection.down; case AxisDirection.down: - assert(debugCheckHasDirectionality( - context, - why: - "to determine the cross-axis direction when the viewport has a 'down' axisDirection", - alternative: - "Alternatively, consider specifying the 'crossAxisDirection' argument on the Viewport.", - )); + assert( + debugCheckHasDirectionality( + context, + why: "to determine the cross-axis direction when the viewport has a 'down' axisDirection", + alternative: "Alternatively, consider specifying the 'crossAxisDirection' argument on the Viewport.", + ), + ); return textDirectionToAxisDirection(Directionality.of(context)); case AxisDirection.left: return AxisDirection.down; @@ -568,28 +554,34 @@ abstract class CustomViewport extends MultiChildRenderObjectWidget { super.debugFillProperties(properties); properties ..add(EnumProperty('axisDirection', axisDirection)) - ..add(EnumProperty( - 'crossAxisDirection', - crossAxisDirection, - defaultValue: null, - )) + ..add( + EnumProperty( + 'crossAxisDirection', + crossAxisDirection, + defaultValue: null, + ), + ) ..add(DoubleProperty('anchor', anchor)) ..add(DiagnosticsProperty('offset', offset)); if (center != null) { properties.add(DiagnosticsProperty('center', center)); } else if (children.isNotEmpty && children.first.key != null) { - properties.add(DiagnosticsProperty( - 'center', - children.first.key, - tooltip: 'implicit', - )); + properties.add( + DiagnosticsProperty( + 'center', + children.first.key, + tooltip: 'implicit', + ), + ); } properties ..add(DiagnosticsProperty('cacheExtent', cacheExtent)) - ..add(DiagnosticsProperty( - 'cacheExtentStyle', - cacheExtentStyle, - )); + ..add( + DiagnosticsProperty( + 'cacheExtentStyle', + cacheExtentStyle, + ), + ); } } @@ -601,8 +593,7 @@ class _ViewportElement extends MultiChildRenderObjectElement { CustomViewport get widget => super.widget as CustomViewport; @override - CustomRenderViewport get renderObject => - super.renderObject as CustomRenderViewport; + CustomRenderViewport get renderObject => super.renderObject as CustomRenderViewport; @override void mount(Element? parent, dynamic newSlot) { @@ -618,9 +609,8 @@ class _ViewportElement extends MultiChildRenderObjectElement { void _updateCenter() { if (widget.center != null) { - renderObject.center = children - .singleWhere((Element element) => element.widget.key == widget.center) - .renderObject as RenderSliver?; + renderObject.center = + children.singleWhere((Element element) => element.widget.key == widget.center).renderObject as RenderSliver?; } else if (children.isNotEmpty) { renderObject.center = children.first.renderObject as RenderSliver?; } else { @@ -630,15 +620,16 @@ class _ViewportElement extends MultiChildRenderObjectElement { @override void debugVisitOnstageChildren(ElementVisitor visitor) { - children.where((Element e) { - final renderSliver = e.renderObject! as RenderSliver; - return renderSliver.geometry!.visible; - }).forEach(visitor); + children + .where((Element e) { + final renderSliver = e.renderObject! as RenderSliver; + return renderSliver.geometry!.visible; + }) + .forEach(visitor); } } -class CustomSliverPhysicalContainerParentData - extends SliverPhysicalContainerParentData { +class CustomSliverPhysicalContainerParentData extends SliverPhysicalContainerParentData { /// The position of the child relative to the zero scroll offset. /// /// The number of pixels from from the zero scroll offset of the parent sliver @@ -685,8 +676,7 @@ class CustomSliverPhysicalContainerParentData /// placed inside a [RenderSliver] (the opposite of this class). /// * [RenderShrinkWrappingViewport], a variant of [RenderViewport] that /// shrink-wraps its contents along the main axis. -abstract class CustomRenderViewport - extends RenderViewportBase { +abstract class CustomRenderViewport extends RenderViewportBase { /// Creates a viewport for [RenderSliver] objects. /// /// If the [center] is not specified, then the first child in the `children` @@ -704,15 +694,15 @@ abstract class CustomRenderViewport super.cacheExtent, super.cacheExtentStyle, super.clipBehavior, - }) : assert( - anchor >= 0.0 && anchor <= 1.0, - 'Anchor must be between 0.0 and 1.0.', - ), - assert( - cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null, - 'A cacheExtent is required when using CacheExtentStyle.viewport.', - ), - _center = center { + }) : assert( + anchor >= 0.0 && anchor <= 1.0, + 'Anchor must be between 0.0 and 1.0.', + ), + assert( + cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null, + 'A cacheExtent is required when using CacheExtentStyle.viewport.', + ), + _center = center { addAll(children); if (center == null && firstChild != null) _center = firstChild; } @@ -735,8 +725,7 @@ abstract class CustomRenderViewport /// /// * [RenderViewportBase.describeSemanticsConfiguration], which adds this /// tag to its [SemanticsConfiguration]. - static const SemanticsTag useTwoPaneSemantics = - SemanticsTag('RenderViewport.twoPane'); + static const SemanticsTag useTwoPaneSemantics = SemanticsTag('RenderViewport.twoPane'); /// When a top-level [SemanticsNode] below a [RenderAbstractViewport] is /// tagged with [excludeFromScrolling] it will not be part of the scrolling @@ -751,8 +740,7 @@ abstract class CustomRenderViewport /// bar) can tag its [SemanticsNode] with [excludeFromScrolling] to indicate /// that it should no longer be considered for semantic actions related to /// scrolling. - static const SemanticsTag excludeFromScrolling = - SemanticsTag('RenderViewport.excludeFromScrolling'); + static const SemanticsTag excludeFromScrolling = SemanticsTag('RenderViewport.excludeFromScrolling'); @override void setupParentData(RenderObject child) { @@ -900,8 +888,7 @@ abstract class CustomRenderViewport double layoutOffset, GrowthDirection growthDirection, ) { - final childParentData = - child.parentData! as CustomSliverPhysicalContainerParentData; + final childParentData = child.parentData! as CustomSliverPhysicalContainerParentData; childParentData ..layoutOffset = layoutOffset ..growthDirection = growthDirection; @@ -909,8 +896,7 @@ abstract class CustomRenderViewport @override Offset paintOffsetOf(RenderSliver child) { - final childParentData = - child.parentData! as CustomSliverPhysicalContainerParentData; + final childParentData = child.parentData! as CustomSliverPhysicalContainerParentData; return computeAbsolutePaintOffset( child, childParentData.layoutOffset!, @@ -983,8 +969,7 @@ abstract class CustomRenderViewport RenderSliver child, double parentMainAxisPosition, ) { - final childParentData = - child.parentData! as CustomSliverPhysicalContainerParentData; + final childParentData = child.parentData! as CustomSliverPhysicalContainerParentData; switch (applyGrowthDirectionToAxisDirection( child.constraints.axisDirection, child.constraints.growthDirection, @@ -993,11 +978,9 @@ abstract class CustomRenderViewport case AxisDirection.right: return parentMainAxisPosition - childParentData.layoutOffset!; case AxisDirection.up: - return (size.height - parentMainAxisPosition) - - childParentData.layoutOffset!; + return (size.height - parentMainAxisPosition) - childParentData.layoutOffset!; case AxisDirection.left: - return (size.width - parentMainAxisPosition) - - childParentData.layoutOffset!; + return (size.width - parentMainAxisPosition) - childParentData.layoutOffset!; } } diff --git a/packages/stream_chat_flutter/lib/src/ai_assistant/ai_typing_indicator_view.dart b/packages/stream_chat_flutter/lib/src/ai_assistant/ai_typing_indicator_view.dart index ebafb3ef2a..288cedf09b 100644 --- a/packages/stream_chat_flutter/lib/src/ai_assistant/ai_typing_indicator_view.dart +++ b/packages/stream_chat_flutter/lib/src/ai_assistant/ai_typing_indicator_view.dart @@ -145,25 +145,25 @@ class _AnimatedDot extends StatefulWidget { State<_AnimatedDot> createState() => _AnimatedDotState(); } -class _AnimatedDotState extends State<_AnimatedDot> - with SingleTickerProviderStateMixin<_AnimatedDot> { +class _AnimatedDotState extends State<_AnimatedDot> with SingleTickerProviderStateMixin<_AnimatedDot> { late final AnimationController _repeatingController; @override void initState() { super.initState(); - _repeatingController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 800), - )..addStatusListener( - (status) { - if (status == AnimationStatus.completed) { - if (mounted) _repeatingController.reverse(); - } else if (status == AnimationStatus.dismissed) { - if (mounted) _repeatingController.forward(); - } - }, - ); + _repeatingController = + AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + )..addStatusListener( + (status) { + if (status == AnimationStatus.completed) { + if (mounted) _repeatingController.reverse(); + } else if (status == AnimationStatus.dismissed) { + if (mounted) _repeatingController.forward(); + } + }, + ); Future.delayed( Duration(milliseconds: 200 * widget.index), diff --git a/packages/stream_chat_flutter/lib/src/ai_assistant/stream_typewriter_builder.dart b/packages/stream_chat_flutter/lib/src/ai_assistant/stream_typewriter_builder.dart index e004784bf6..f686766004 100644 --- a/packages/stream_chat_flutter/lib/src/ai_assistant/stream_typewriter_builder.dart +++ b/packages/stream_chat_flutter/lib/src/ai_assistant/stream_typewriter_builder.dart @@ -54,9 +54,7 @@ class TypewriterValue { @override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is TypewriterValue && - other.text == text && - other.state == state; + return other is TypewriterValue && other.text == text && other.state == state; } @override @@ -205,11 +203,12 @@ class TypewriterController extends ValueNotifier { /// A widget builder for a [StreamTypewriterBuilder]. It allows you to build a /// widget depending on the [TypewriterValue]'s value. /// {@endtemplate} -typedef TypewriterWidgetBuilder = Widget Function( - BuildContext context, - TypewriterValue value, - Widget? child, -); +typedef TypewriterWidgetBuilder = + Widget Function( + BuildContext context, + TypewriterValue value, + Widget? child, + ); /// {@template streamTypewriterBuilder} /// A widget that listens to a [TypewriterController] and rebuilds whenever the diff --git a/packages/stream_chat_flutter/lib/src/ai_assistant/streaming_message_view.dart b/packages/stream_chat_flutter/lib/src/ai_assistant/streaming_message_view.dart index 4422b5d86a..0a81884920 100644 --- a/packages/stream_chat_flutter/lib/src/ai_assistant/streaming_message_view.dart +++ b/packages/stream_chat_flutter/lib/src/ai_assistant/streaming_message_view.dart @@ -82,8 +82,8 @@ class _StreamingMessageViewState extends State { onTapLink: switch (widget.onTapLink) { final onTapLink? => onTapLink, _ => (String link, String? href, String title) { - if (href != null) launchURL(context, href); - }, + if (href != null) launchURL(context, href); + }, }, ); } diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment.dart index 0b73a93c7c..3b1ca39f30 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 'url_attachment.dart'; +export 'link_preview_attachment.dart'; +export 'poll_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_upload_state_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart index dcb6a47a08..354f83bf99 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart @@ -44,7 +44,8 @@ class StreamAttachmentUploadStateBuilder extends StatelessWidget { final messageId = message.id; final attachmentId = attachment.id; - final inProgress = inProgressBuilder ?? + final inProgress = + inProgressBuilder ?? (context, int sent, int total) { return _InProgressState( sent: sent, @@ -53,7 +54,8 @@ class StreamAttachmentUploadStateBuilder extends StatelessWidget { ); }; - final failed = failedBuilder ?? + final failed = + failedBuilder ?? (context, error) { return _FailedState( error: error, @@ -64,8 +66,7 @@ class StreamAttachmentUploadStateBuilder extends StatelessWidget { final success = successBuilder ?? (context) => _SuccessState(); - final preparing = preparingBuilder ?? - (context) => _PreparingState(attachmentId: attachmentId); + final preparing = preparingBuilder ?? (context) => _PreparingState(attachmentId: attachmentId); return attachment.uploadState.when( preparing: () => preparing(context), @@ -87,20 +88,22 @@ class _IconButton extends StatelessWidget { @override Widget build(BuildContext context) { - return SizedBox( - height: 24, - width: 24, - child: RawMaterialButton( - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - hoverElevation: 0, - onPressed: onPressed, - fillColor: StreamChatTheme.of(context).colorTheme.overlayDark, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + return SizedBox.square( + dimension: 20, + child: IconTheme.merge( + data: const IconThemeData(size: 16), + child: RawMaterialButton( + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + hoverElevation: 0, + onPressed: onPressed, + fillColor: StreamChatTheme.of(context).colorTheme.overlayDark, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: icon, ), - child: icon, ), ); } @@ -121,8 +124,8 @@ class _PreparingState extends StatelessWidget { Align( alignment: Alignment.topRight, child: _IconButton( - icon: StreamSvgIcon( - icon: StreamSvgIcons.close, + icon: Icon( + context.streamIcons.xmark20, color: StreamChatTheme.of(context).colorTheme.barsBg, ), onPressed: () => channel.cancelAttachmentUpload(attachmentId), @@ -161,8 +164,8 @@ class _InProgressState extends StatelessWidget { Align( alignment: Alignment.topRight, child: _IconButton( - icon: StreamSvgIcon( - icon: StreamSvgIcons.close, + icon: Icon( + context.streamIcons.xmark20, color: StreamChatTheme.of(context).colorTheme.barsBg, ), onPressed: () => channel.cancelAttachmentUpload(attachmentId), @@ -200,9 +203,8 @@ class _FailedState extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _IconButton( - icon: StreamSvgIcon( - size: 14, - icon: StreamSvgIcons.retry, + icon: Icon( + context.streamIcons.retry20, color: theme.colorTheme.barsBg, ), onPressed: () { @@ -243,9 +245,10 @@ class _SuccessState extends StatelessWidget { alignment: Alignment.topRight, child: CircleAvatar( backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark, - maxRadius: 12, - child: StreamSvgIcon( - icon: StreamSvgIcons.check, + maxRadius: 10, + child: Icon( + size: 16, + context.streamIcons.checkmark16, color: StreamChatTheme.of(context).colorTheme.barsBg, ), ), 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..32c8eefcd6 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 @@ -20,7 +20,10 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// widget for the [Message.attachments]. class AttachmentWidgetCatalog { /// {@macro attachmentWidgetCatalog} - const AttachmentWidgetCatalog({required this.builders}); + const AttachmentWidgetCatalog({ + required this.builders, + this.padding, + }); /// The list of builders to use to build the widget. /// @@ -28,25 +31,25 @@ class AttachmentWidgetCatalog { /// the message and attachments will be used to build the widget. final List builders; + /// The padding around the built attachment content. + final EdgeInsetsGeometry? padding; + /// Builds a widget for the given [message] and [attachments]. /// /// It iterates through the list of builders and uses the first builder /// that can handle the message and attachments. /// /// Throws an [Exception] if no builder is found for the message. - Widget build(BuildContext context, Message message) { + 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) { if (builder.canHandle(message, attachments)) { - return builder.build(context, message, attachments); + final child = builder.build(context, message, attachments); + if (child == null || padding == null) return child; + + return Padding(padding: padding!, child: child); } } @@ -57,8 +60,11 @@ class AttachmentWidgetCatalog { extension on List { /// Groups the attachments by their type. Map> get grouped { - return groupBy(where((it) { - return it.type != null; - }), (attachment) => attachment.type!); + return groupBy( + where((it) { + return it.type != null; + }), + (attachment) => attachment.type!, + ); } } 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..6e4122fbeb 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 @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; part 'fallback_attachment_builder.dart'; part 'file_attachment_builder.dart'; @@ -8,18 +8,19 @@ part 'gallery_attachment_builder.dart'; part 'giphy_attachment_builder.dart'; part 'image_attachment_builder.dart'; part 'mixed_attachment_builder.dart'; -part 'url_attachment_builder.dart'; +part 'link_preview_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. /// {@endtemplate} -typedef StreamAttachmentWidgetTapCallback = void Function( - Message message, - Attachment attachment, -); +typedef StreamAttachmentWidgetTapCallback = + void Function( + Message message, + Attachment attachment, + ); /// {@template attachmentWidgetBuilder} /// A builder which is used to build a widget for a given [Message] and @@ -43,6 +44,8 @@ abstract class StreamAttachmentWidgetBuilder { /// * [FileAttachmentBuilder] /// * [ImageAttachmentBuilder] /// * [VideoAttachmentBuilder] + /// * [VoiceRecordingAttachmentPlaylistBuilder] + /// * [PollAttachmentBuilder] /// * [UrlAttachmentBuilder] /// * [FallbackAttachmentBuilder] /// @@ -64,69 +67,54 @@ abstract class StreamAttachmentWidgetBuilder { /// widget. static List defaultBuilders({ required Message message, - ShapeBorder? shape, - EdgeInsetsGeometry padding = const EdgeInsets.all(4), StreamAttachmentWidgetTapCallback? onAttachmentTap, List? customAttachmentBuilders, }) { return [ ...?customAttachmentBuilders, - // Handles a mix of image, gif, video, url and file attachments. + // Handles poll attachments. + const PollAttachmentBuilder(), + + // Handles a mix of image, gif, video, url, file and voice recording + // attachments. MixedAttachmentBuilder( - padding: padding, onAttachmentTap: onAttachmentTap, ), // Handles a mix of image, gif, and video attachments. GalleryAttachmentBuilder( - shape: shape, - padding: padding, - runSpacing: padding.vertical / 2, - spacing: padding.horizontal / 2, onAttachmentTap: onAttachmentTap, ), // Handles file attachments. FileAttachmentBuilder( - shape: shape, - padding: padding, onAttachmentTap: onAttachmentTap, ), // Handles giphy attachments. GiphyAttachmentBuilder( - shape: shape, - padding: padding, onAttachmentTap: onAttachmentTap, ), // Handles image attachments. ImageAttachmentBuilder( - shape: shape, - padding: padding, onAttachmentTap: onAttachmentTap, ), // Handles video attachments. VideoAttachmentBuilder( - shape: shape, - padding: padding, onAttachmentTap: onAttachmentTap, ), // Handles voice recording attachments. VoiceRecordingAttachmentPlaylistBuilder( - shape: shape, - padding: padding, onAttachmentTap: onAttachmentTap, ), // We don't handle URL attachments if the message is a reply. if (message.quotedMessage == null) - UrlAttachmentBuilder( - shape: shape, - padding: padding, + LinkPreviewAttachmentBuilder( onAttachmentTap: onAttachmentTap, ), @@ -142,7 +130,7 @@ abstract class StreamAttachmentWidgetBuilder { /// Builds a widget for the given [message] and [attachments]. /// This will only be called if [canHandle] returns `true`. - Widget build( + Widget? build( BuildContext context, Message message, Map> attachments, diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/fallback_attachment_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/fallback_attachment_builder.dart index 3e80a7d9fe..a66beae897 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/fallback_attachment_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/fallback_attachment_builder.dart @@ -21,13 +21,12 @@ class FallbackAttachmentBuilder extends StreamAttachmentWidgetBuilder { } @override - Widget build( + Widget? build( BuildContext context, Message message, Map> attachments, ) { - // Returns an empty widget because this builder will be used as a fallback - // when no other builder can handle the attachments. - return const Empty(); + // No visual representation for unsupported attachment types. + return null; } } diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/file_attachment_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/file_attachment_builder.dart index 1b05939250..3c6a61d3af 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/file_attachment_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/file_attachment_builder.dart @@ -6,24 +6,19 @@ part of 'attachment_widget_builder.dart'; class FileAttachmentBuilder extends StreamAttachmentWidgetBuilder { /// {@macro fileAttachmentBuilder} const FileAttachmentBuilder({ - this.shape, - this.backgroundColor, - this.constraints = const BoxConstraints(), - this.padding = const EdgeInsets.all(4), + this.style, + this.constraints, this.onAttachmentTap, }); - /// The shape of the file attachment. - final ShapeBorder? shape; - - /// The background color of the file attachment. - final Color? backgroundColor; + /// The style of the file attachment container. + /// + /// When null, a default style with placement-aware background color and + /// a superellipse shape is used. + final StreamMessageAttachmentStyle? style; /// The constraints to apply to the file attachment widget. - final BoxConstraints constraints; - - /// The padding to apply to the file attachment widget. - final EdgeInsetsGeometry padding; + final BoxConstraints? constraints; /// The callback to call when the attachment is tapped. final StreamAttachmentWidgetTapCallback? onAttachmentTap; @@ -38,49 +33,45 @@ class FileAttachmentBuilder extends StreamAttachmentWidgetBuilder { } @override - Widget build( + Widget? build( BuildContext context, Message message, Map> attachments, ) { assert(debugAssertCanHandle(message, attachments), ''); + final spacing = context.streamSpacing; + final files = attachments[AttachmentType.file]!; Widget _buildFileAttachment(Attachment file) { - VoidCallback? onTap; - if (onAttachmentTap != null) { - onTap = () => onAttachmentTap!(message, file); - } + final onTap = switch (onAttachmentTap) { + final onTap? => () => onTap(message, file), + _ => null, + }; - return InkWell( - onTap: onTap, - child: StreamFileAttachment( - file: file, - message: message, - shape: shape, - constraints: constraints, - backgroundColor: backgroundColor, + return StreamMessageAttachment( + style: style, + child: InkWell( + onTap: onTap, + child: StreamFileAttachment( + file: file, + message: message, + constraints: constraints, + ), ), ); } - Widget child; if (files.length == 1) { - child = _buildFileAttachment(files.first); - } else { - child = Column( - // Add a small vertical padding between each attachment. - spacing: padding.vertical / 2, - children: [ - for (final file in files) _buildFileAttachment(file), - ], - ); + return _buildFileAttachment(files.first); } - return Padding( - padding: padding, - child: child, + return Column( + spacing: spacing.xs, + children: [ + for (final file in files) _buildFileAttachment(file), + ], ); } } diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/gallery_attachment_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/gallery_attachment_builder.dart index cd2bf22e92..fff72399a5 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/gallery_attachment_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/gallery_attachment_builder.dart @@ -1,10 +1,5 @@ part of 'attachment_widget_builder.dart'; -const _kDefaultGalleryConstraints = BoxConstraints.tightFor( - width: 256, - height: 195, -); - /// {@template galleryAttachmentBuilder} /// A widget builder for [AttachmentType.image], [AttachmentType.video] and /// [AttachmentType.giphy] attachment types. @@ -15,38 +10,37 @@ const _kDefaultGalleryConstraints = BoxConstraints.tightFor( class GalleryAttachmentBuilder extends StreamAttachmentWidgetBuilder { /// {@macro galleryAttachmentBuilder} const GalleryAttachmentBuilder({ - this.shape, - this.padding = const EdgeInsets.all(2), - this.spacing = 2, - this.runSpacing = 2, - this.constraints = _kDefaultGalleryConstraints, + this.style, + this.spacing, + this.runSpacing, + this.constraints, this.onAttachmentTap, }); - /// The shape of the gallery attachment. - final ShapeBorder? shape; + /// The style of the gallery attachment container. + /// + /// When null, a default style with a rounded rectangle shape and border + /// is used. + final StreamMessageAttachmentStyle? style; /// The constraints to apply to the gallery attachment widget. - final BoxConstraints constraints; - - /// The padding to apply to the gallery attachment widget. - final EdgeInsetsGeometry padding; + final BoxConstraints? constraints; /// How much space to place between children in a run in the main axis. /// /// For example, if [spacing] is 10.0, the children will be spaced at least /// 10.0 logical pixels apart in the main axis. /// - /// Defaults to 2.0. - final double spacing; + /// When null, defaults to [StreamSpacing.xxs]. + final double? spacing; /// How much space to place between the runs themselves in the cross axis. /// /// For example, if [runSpacing] is 10.0, the runs will be spaced at least /// 10.0 logical pixels apart in the cross axis. /// - /// Defaults to 2.0. - final double runSpacing; + /// When null, defaults to [StreamSpacing.xxs]. + final double? runSpacing; /// The callback to call when the attachment is tapped. final StreamAttachmentWidgetTapCallback? onAttachmentTap; @@ -73,19 +67,22 @@ class GalleryAttachmentBuilder extends StreamAttachmentWidgetBuilder { } @override - Widget build( + Widget? build( BuildContext context, Message message, Map> attachments, ) { assert(debugAssertCanHandle(message, attachments), ''); + final effectiveStyle = StreamMessageAttachmentStyle.from( + backgroundColor: StreamColors.transparent, + ).merge(style); + final galleryAttachments = [...attachments.values.expand((it) => it)]; - return Padding( - padding: padding, + return StreamMessageAttachment( + style: effectiveStyle, child: StreamGalleryAttachment( - shape: shape, message: message, spacing: spacing, runSpacing: runSpacing, @@ -99,24 +96,26 @@ class GalleryAttachmentBuilder extends StreamAttachmentWidgetBuilder { onTap = () => onAttachmentTap!(message, attachment); } - return InkWell( - onTap: onTap, - child: Stack( - children: [ - StreamMediaAttachmentThumbnail( - media: attachment, - width: constraints.maxWidth, - height: constraints.maxHeight, - fit: BoxFit.cover, - ), - Padding( - padding: const EdgeInsets.all(8), - child: StreamAttachmentUploadStateBuilder( - message: message, - attachment: attachment, + return StreamMessageAttachment( + style: StreamMessageAttachmentStyle.from(padding: EdgeInsets.zero), + child: InkWell( + onTap: onTap, + child: Stack( + fit: .expand, + children: [ + StreamMediaAttachmentThumbnail( + media: attachment, + fit: BoxFit.cover, + ), + Padding( + padding: const EdgeInsets.all(8), + child: StreamAttachmentUploadStateBuilder( + message: message, + attachment: attachment, + ), ), - ), - ], + ], + ), ), ); }, diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/giphy_attachment_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/giphy_attachment_builder.dart index 2220ae9240..8eed7cc485 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/giphy_attachment_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/giphy_attachment_builder.dart @@ -1,12 +1,5 @@ part of 'attachment_widget_builder.dart'; -const _kDefaultGiphyConstraints = BoxConstraints( - minWidth: 170, - maxWidth: 256, - minHeight: 100, - maxHeight: 300, -); - /// {@template giphyAttachmentBuilder} /// A widget builder for [AttachmentType.giphy] attachment type. /// @@ -15,20 +8,19 @@ const _kDefaultGiphyConstraints = BoxConstraints( class GiphyAttachmentBuilder extends StreamAttachmentWidgetBuilder { /// {@macro giphyAttachmentBuilder} const GiphyAttachmentBuilder({ - this.shape, - this.padding = const EdgeInsets.all(2), - this.constraints = _kDefaultGiphyConstraints, + this.style, + this.constraints, this.onAttachmentTap, }); - /// The shape of the giphy attachment. - final ShapeBorder? shape; + /// The style of the giphy attachment container. + /// + /// When null, a default style with a rounded rectangle shape and border + /// is used. + final StreamMessageAttachmentStyle? style; /// The constraints to apply to the giphy attachment widget. - final BoxConstraints constraints; - - /// The padding to apply to the giphy attachment widget. - final EdgeInsetsGeometry padding; + final BoxConstraints? constraints; /// The callback to call when the attachment is tapped. final StreamAttachmentWidgetTapCallback? onAttachmentTap; @@ -43,7 +35,7 @@ class GiphyAttachmentBuilder extends StreamAttachmentWidgetBuilder { } @override - Widget build( + Widget? build( BuildContext context, Message message, Map> attachments, @@ -57,15 +49,14 @@ class GiphyAttachmentBuilder extends StreamAttachmentWidgetBuilder { onTap = () => onAttachmentTap!(message, giphy); } - return Padding( - padding: padding, + return StreamMessageAttachment( + style: style, child: InkWell( onTap: onTap, child: StreamGiphyAttachment( message: message, constraints: constraints, giphy: giphy, - shape: shape, ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/image_attachment_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/image_attachment_builder.dart index 14aa7c687c..70cf7279df 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/image_attachment_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/image_attachment_builder.dart @@ -1,12 +1,5 @@ part of 'attachment_widget_builder.dart'; -const _kDefaultImageConstraints = BoxConstraints( - minWidth: 170, - maxWidth: 256, - minHeight: 100, - maxHeight: 300, -); - /// {@template imageAttachmentBuilder} /// A widget builder for [AttachmentType.image] attachment type. /// @@ -15,20 +8,19 @@ const _kDefaultImageConstraints = BoxConstraints( class ImageAttachmentBuilder extends StreamAttachmentWidgetBuilder { /// {@macro imageAttachmentBuilder} const ImageAttachmentBuilder({ - this.shape, - this.padding = const EdgeInsets.all(2), - this.constraints = _kDefaultImageConstraints, + this.style, + this.constraints, this.onAttachmentTap, }); - /// The shape of the image attachment. - final ShapeBorder? shape; + /// The style of the image attachment container. + /// + /// When null, a default style with a rounded rectangle shape and border + /// is used. + final StreamMessageAttachmentStyle? style; /// The constraints to apply to the image attachment widget. - final BoxConstraints constraints; - - /// The padding to apply to the image attachment widget. - final EdgeInsetsGeometry padding; + final BoxConstraints? constraints; /// The callback to call when the attachment is tapped. final StreamAttachmentWidgetTapCallback? onAttachmentTap; @@ -43,7 +35,7 @@ class ImageAttachmentBuilder extends StreamAttachmentWidgetBuilder { } @override - Widget build( + Widget? build( BuildContext context, Message message, Map> attachments, @@ -57,12 +49,11 @@ class ImageAttachmentBuilder extends StreamAttachmentWidgetBuilder { onTap = () => onAttachmentTap!(message, image); } - return Padding( - padding: padding, + return StreamMessageAttachment( + style: style, child: InkWell( onTap: onTap, child: StreamImageAttachment( - shape: shape, message: message, constraints: constraints, image: image, diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/link_preview_attachment_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/link_preview_attachment_builder.dart new file mode 100644 index 0000000000..b97ff89d41 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/link_preview_attachment_builder.dart @@ -0,0 +1,80 @@ +part of 'attachment_widget_builder.dart'; + +/// {@template urlAttachmentBuilder} +/// A widget builder for link preview attachment type. +/// +/// This is used to show url attachments with a preview. e.g. youtube, twitter, +/// etc. +/// {@endtemplate} +class LinkPreviewAttachmentBuilder extends StreamAttachmentWidgetBuilder { + /// {@macro urlAttachmentBuilder} + const LinkPreviewAttachmentBuilder({ + this.style, + this.constraints, + this.onAttachmentTap, + }); + + /// The style of the url attachment container. + /// + /// When null, a default style with a rounded rectangle shape, border, + /// and background color is used. + final StreamMessageAttachmentStyle? style; + + /// The constraints to apply to the url attachment widget. + final BoxConstraints? constraints; + + /// The callback to call when the attachment is tapped. + final StreamAttachmentWidgetTapCallback? onAttachmentTap; + + @override + bool canHandle( + Message message, + Map> attachments, + ) { + final urls = attachments[AttachmentType.urlPreview]; + return urls != null && urls.isNotEmpty; + } + + @override + Widget? build( + BuildContext context, + Message message, + Map> attachments, + ) { + assert(debugAssertCanHandle(message, attachments), ''); + + final spacing = context.streamSpacing; + + final urlPreviews = attachments[AttachmentType.urlPreview]!; + + Widget _buildUrlPreview(Attachment urlPreview) { + VoidCallback? onTap; + if (onAttachmentTap != null) { + onTap = () => onAttachmentTap!(message, urlPreview); + } + + return StreamMessageAttachment( + style: style, + child: InkWell( + onTap: onTap, + child: StreamLinkPreviewAttachment( + message: message, + urlAttachment: urlPreview, + constraints: constraints, + ), + ), + ); + } + + if (urlPreviews.length == 1) { + return _buildUrlPreview(urlPreviews.first); + } + + return Column( + spacing: spacing.xs, + children: [ + for (final urlPreview in urlPreviews) _buildUrlPreview(urlPreview), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/mixed_attachment_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/mixed_attachment_builder.dart index 121c78e707..dd38a64375 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/mixed_attachment_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/mixed_attachment_builder.dart @@ -12,49 +12,24 @@ part of 'attachment_widget_builder.dart'; class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder { /// {@macro mixedAttachmentBuilder} MixedAttachmentBuilder({ - this.padding = const EdgeInsets.all(4), StreamAttachmentWidgetTapCallback? onAttachmentTap, - }) : _imageAttachmentBuilder = ImageAttachmentBuilder( - padding: EdgeInsets.zero, - onAttachmentTap: onAttachmentTap, - ), - _videoAttachmentBuilder = VideoAttachmentBuilder( - padding: EdgeInsets.zero, - onAttachmentTap: onAttachmentTap, - ), - _giphyAttachmentBuilder = GiphyAttachmentBuilder( - padding: EdgeInsets.zero, - onAttachmentTap: onAttachmentTap, - ), - _galleryAttachmentBuilder = GalleryAttachmentBuilder( - padding: EdgeInsets.zero, - onAttachmentTap: onAttachmentTap, - ), - _fileAttachmentBuilder = FileAttachmentBuilder( - padding: EdgeInsets.zero, - onAttachmentTap: onAttachmentTap, - ), - _urlAttachmentBuilder = UrlAttachmentBuilder( - padding: EdgeInsets.zero, - onAttachmentTap: onAttachmentTap, - ), - _voiceRecordingAttachmentPlaylistBuilder = - VoiceRecordingAttachmentPlaylistBuilder( - padding: EdgeInsets.zero, - onAttachmentTap: onAttachmentTap, - ); - - /// The padding to apply to the mixed attachment widget. - final EdgeInsetsGeometry padding; + }) : _imageAttachmentBuilder = ImageAttachmentBuilder(onAttachmentTap: onAttachmentTap), + _videoAttachmentBuilder = VideoAttachmentBuilder(onAttachmentTap: onAttachmentTap), + _giphyAttachmentBuilder = GiphyAttachmentBuilder(onAttachmentTap: onAttachmentTap), + _galleryAttachmentBuilder = GalleryAttachmentBuilder(onAttachmentTap: onAttachmentTap), + _fileAttachmentBuilder = FileAttachmentBuilder(onAttachmentTap: onAttachmentTap), + _linkPreviewAttachmentBuilder = LinkPreviewAttachmentBuilder(onAttachmentTap: onAttachmentTap), + _voiceRecordingAttachmentPlaylistBuilder = VoiceRecordingAttachmentPlaylistBuilder( + onAttachmentTap: onAttachmentTap, + ); late final StreamAttachmentWidgetBuilder _imageAttachmentBuilder; late final StreamAttachmentWidgetBuilder _videoAttachmentBuilder; late final StreamAttachmentWidgetBuilder _giphyAttachmentBuilder; late final StreamAttachmentWidgetBuilder _galleryAttachmentBuilder; late final StreamAttachmentWidgetBuilder _fileAttachmentBuilder; - late final StreamAttachmentWidgetBuilder _urlAttachmentBuilder; - late final StreamAttachmentWidgetBuilder - _voiceRecordingAttachmentPlaylistBuilder; + late final StreamAttachmentWidgetBuilder _linkPreviewAttachmentBuilder; + late final StreamAttachmentWidgetBuilder _voiceRecordingAttachmentPlaylistBuilder; @override bool canHandle( @@ -83,7 +58,7 @@ class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder { } @override - Widget build( + Widget? build( BuildContext context, Message message, Map> attachments, @@ -99,44 +74,45 @@ class MixedAttachmentBuilder extends StreamAttachmentWidgetBuilder { final shouldBuildGallery = [...?images, ...?videos, ...?giphys].length > 1; - return Padding( - padding: padding, - child: Column( - spacing: padding.vertical / 2, - mainAxisSize: MainAxisSize.min, - children: [ - if (urls != null) - _urlAttachmentBuilder.build(context, message, { - AttachmentType.urlPreview: urls, - }), - if (files != null) - _fileAttachmentBuilder.build(context, message, { - AttachmentType.file: files, - }), - if (voiceRecordings != null) - _voiceRecordingAttachmentPlaylistBuilder.build(context, message, { - AttachmentType.voiceRecording: voiceRecordings, - }), - if (shouldBuildGallery) - _galleryAttachmentBuilder.build(context, message, { - if (images != null) AttachmentType.image: images, - if (videos != null) AttachmentType.video: videos, - if (giphys != null) AttachmentType.giphy: giphys, - }) - else if (images != null && images.length == 1) - _imageAttachmentBuilder.build(context, message, { - AttachmentType.image: images, - }) - else if (videos != null && videos.length == 1) - _videoAttachmentBuilder.build(context, message, { - AttachmentType.video: videos, - }) - else if (giphys != null && giphys.length == 1) - _giphyAttachmentBuilder.build(context, message, { - AttachmentType.giphy: giphys, - }), - ], - ), + final spacing = context.streamSpacing; + final crossAxisAlignment = StreamMessageLayout.crossAxisAlignmentOf(context); + + return Column( + spacing: spacing.xs, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: crossAxisAlignment, + children: [ + if (urls != null) + ?_linkPreviewAttachmentBuilder.build(context, message, { + AttachmentType.urlPreview: urls, + }), + if (shouldBuildGallery) + ?_galleryAttachmentBuilder.build(context, message, { + if (images != null) AttachmentType.image: images, + if (videos != null) AttachmentType.video: videos, + if (giphys != null) AttachmentType.giphy: giphys, + }) + else if (images != null && images.length == 1) + ?_imageAttachmentBuilder.build(context, message, { + AttachmentType.image: images, + }) + else if (videos != null && videos.length == 1) + ?_videoAttachmentBuilder.build(context, message, { + AttachmentType.video: videos, + }) + else if (giphys != null && giphys.length == 1) + ?_giphyAttachmentBuilder.build(context, message, { + AttachmentType.giphy: giphys, + }), + if (files != null) + ?_fileAttachmentBuilder.build(context, message, { + AttachmentType.file: files, + }), + if (voiceRecordings != null) + ?_voiceRecordingAttachmentPlaylistBuilder.build(context, message, { + AttachmentType.voiceRecording: voiceRecordings, + }), + ], ); } } 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..cae2623ef2 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/poll_attachment_builder.dart @@ -0,0 +1,53 @@ +part of 'attachment_widget_builder.dart'; + +/// {@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 pollAttachmentBuilder} + const PollAttachmentBuilder({ + this.style, + this.constraints, + }); + + /// The style of the poll attachment container. + /// + /// When null, a default style with a rounded rectangle shape and border + /// is used. + final StreamMessageAttachmentStyle? style; + + /// The constraints to apply to the poll attachment widget. + final BoxConstraints? constraints; + + @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), ''); + + final effectiveStyle = StreamMessageAttachmentStyle.from( + backgroundColor: StreamColors.transparent, + ).merge(style); + + return StreamMessageAttachment( + style: effectiveStyle, + child: StreamPollAttachment( + message: message, + constraints: constraints, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/url_attachment_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/url_attachment_builder.dart deleted file mode 100644 index ab464d9ed0..0000000000 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/url_attachment_builder.dart +++ /dev/null @@ -1,103 +0,0 @@ -part of 'attachment_widget_builder.dart'; - -const _kDefaultUrlAttachmentConstraints = BoxConstraints(maxWidth: 256); - -/// {@template urlAttachmentBuilder} -/// A widget builder for url attachment type. -/// -/// This is used to show url attachments with a preview. e.g. youtube, twitter, -/// etc. -/// {@endtemplate} -class UrlAttachmentBuilder extends StreamAttachmentWidgetBuilder { - /// {@macro urlAttachmentBuilder} - const UrlAttachmentBuilder({ - this.shape, - this.padding = const EdgeInsets.all(8), - this.constraints = _kDefaultUrlAttachmentConstraints, - this.onAttachmentTap, - }); - - /// The shape of the url attachment. - final ShapeBorder? shape; - - /// The constraints to apply to the url attachment widget. - final BoxConstraints constraints; - - /// The padding to apply to the url attachment widget. - final EdgeInsetsGeometry padding; - - /// The callback to call when the attachment is tapped. - final StreamAttachmentWidgetTapCallback? onAttachmentTap; - - @override - bool canHandle( - Message message, - Map> attachments, - ) { - final urls = attachments[AttachmentType.urlPreview]; - return urls != null && urls.isNotEmpty; - } - - @override - Widget build( - BuildContext context, - Message message, - Map> attachments, - ) { - assert(debugAssertCanHandle(message, attachments), ''); - - final urlPreviews = attachments[AttachmentType.urlPreview]!; - - final client = StreamChat.of(context).client; - final isMyMessage = message.user?.id == client.state.currentUser?.id; - - final streamChatTheme = StreamChatTheme.of(context); - final messageTheme = isMyMessage - ? streamChatTheme.ownMessageTheme - : streamChatTheme.otherMessageTheme; - - Widget _buildUrlPreview(Attachment urlPreview) { - VoidCallback? onTap; - if (onAttachmentTap != null) { - onTap = () => onAttachmentTap!(message, urlPreview); - } - - final host = Uri.parse(urlPreview.titleLink!).host; - final splitList = host.split('.'); - final hostName = splitList.length == 3 ? splitList[1] : splitList[0]; - final hostDisplayName = urlPreview.authorName?.capitalize() ?? - getWebsiteName(hostName.toLowerCase()) ?? - hostName.capitalize(); - - return InkWell( - onTap: onTap, - child: StreamUrlAttachment( - message: message, - urlAttachment: urlPreview, - hostDisplayName: hostDisplayName, - messageTheme: messageTheme, - constraints: constraints, - shape: shape, - ), - ); - } - - Widget child; - if (urlPreviews.length == 1) { - child = _buildUrlPreview(urlPreviews.first); - } else { - child = Column( - // Add a small vertical padding between each attachment. - spacing: padding.vertical / 2, - children: [ - for (final urlPreview in urlPreviews) _buildUrlPreview(urlPreview), - ], - ); - } - - return Padding( - padding: padding, - child: child, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/video_attachment_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/video_attachment_builder.dart index 45f92ef8b8..23d9e81062 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/video_attachment_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/video_attachment_builder.dart @@ -1,10 +1,5 @@ part of 'attachment_widget_builder.dart'; -const _kDefaultVideoConstraints = BoxConstraints.tightFor( - width: 256, - height: 195, -); - /// {@template videoAttachmentBuilder} /// A widget builder for [AttachmentType.video] attachment type. /// @@ -13,20 +8,19 @@ const _kDefaultVideoConstraints = BoxConstraints.tightFor( class VideoAttachmentBuilder extends StreamAttachmentWidgetBuilder { /// {@macro videoAttachmentBuilder} const VideoAttachmentBuilder({ - this.shape, - this.padding = const EdgeInsets.all(2), - this.constraints = _kDefaultVideoConstraints, + this.style, + this.constraints, this.onAttachmentTap, }); - /// The shape of the video attachment. - final ShapeBorder? shape; + /// The style of the video attachment container. + /// + /// When null, a default style with a rounded rectangle shape and border + /// is used. + final StreamMessageAttachmentStyle? style; /// The constraints to apply to the video attachment widget. - final BoxConstraints constraints; - - /// The padding to apply to the video attachment widget. - final EdgeInsetsGeometry padding; + final BoxConstraints? constraints; /// The callback to call when the attachment is tapped. final StreamAttachmentWidgetTapCallback? onAttachmentTap; @@ -43,7 +37,7 @@ class VideoAttachmentBuilder extends StreamAttachmentWidgetBuilder { } @override - Widget build( + Widget? build( BuildContext context, Message message, Map> attachments, @@ -57,8 +51,8 @@ class VideoAttachmentBuilder extends StreamAttachmentWidgetBuilder { onTap = () => onAttachmentTap!(message, video); } - return Padding( - padding: padding, + return StreamMessageAttachment( + style: style, child: InkWell( onTap: onTap, child: StreamVideoAttachment( 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/builder/voice_recording_attachment_playlist_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_playlist_builder.dart index f53fea8642..d50de626ff 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_playlist_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/voice_recording_attachment_playlist_builder.dart @@ -9,24 +9,22 @@ part of 'attachment_widget_builder.dart'; /// The widget is built when the message has at least one voice recording /// attachment. /// {@endtemplate} -class VoiceRecordingAttachmentPlaylistBuilder - extends StreamAttachmentWidgetBuilder { +class VoiceRecordingAttachmentPlaylistBuilder extends StreamAttachmentWidgetBuilder { /// {@macro voiceRecordingAttachmentPlaylistBuilder} const VoiceRecordingAttachmentPlaylistBuilder({ - this.shape, - this.padding = const EdgeInsets.all(16), - this.constraints = const BoxConstraints(), + this.style, + this.constraints, this.onAttachmentTap, }); - /// The shape of the video attachment. - final ShapeBorder? shape; - - /// The padding to apply to the video attachment widget. - final EdgeInsetsGeometry padding; + /// The style of the voice recording attachment container. + /// + /// When null, a default style with a rounded rectangle shape and border + /// is used. + final StreamMessageAttachmentStyle? style; /// The constraints to apply to the video attachment widget. - final BoxConstraints constraints; + final BoxConstraints? constraints; /// The callback to call when the attachment is tapped. final StreamAttachmentWidgetTapCallback? onAttachmentTap; @@ -41,7 +39,7 @@ class VoiceRecordingAttachmentPlaylistBuilder } @override - Widget build( + Widget? build( BuildContext context, Message message, Map> attachments, @@ -50,15 +48,37 @@ class VoiceRecordingAttachmentPlaylistBuilder final playlist = attachments[AttachmentType.voiceRecording]!; - return Padding( - padding: padding, + return StreamVoiceRecordingAttachmentTheme( + data: _StreamVoiceRecordingAttachmentDefaults(context), child: StreamVoiceRecordingAttachmentPlaylist( - shape: shape, message: message, voiceRecordings: playlist, constraints: constraints, - separatorBuilder: (_, __) => SizedBox(height: padding.vertical / 2), + itemDecorator: (context, index, child) { + return StreamMessageAttachment(style: style, child: child); + }, ), ); } } + +// Default values for [StreamVoiceRecordingAttachmentThemeData] backed by stream design tokens. +class _StreamVoiceRecordingAttachmentDefaults extends StreamVoiceRecordingAttachmentThemeData { + _StreamVoiceRecordingAttachmentDefaults(this._context); + + final BuildContext _context; + + late final _alignment = StreamMessageLayout.messageAlignmentOf(_context); + late final StreamColorScheme _colorScheme = _context.streamColorScheme; + + Color get _borderColor => switch (_alignment) { + .start => _colorScheme.borderStrong, + .end => _colorScheme.brand.shade300, + }; + + @override + StreamButtonThemeStyle get controlButtonStyle => .from(borderColor: _borderColor); + + @override + StreamPlaybackSpeedToggleStyle get speedToggleStyle => .from(borderColor: _borderColor); +} diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index a6cf3a04a0..3e46f69522 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -1,28 +1,79 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler.dart'; import 'package:stream_chat_flutter/src/attachment/thumbnail/file_attachment_thumbnail.dart'; -import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/components/stream_chat_component_builders.dart'; import 'package:stream_chat_flutter/src/indicators/upload_progress_indicator.dart'; import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; -/// {@template streamFileAttachment} -/// Displays file attachments that have been sent in a chat. +/// A file attachment component with file information and actions. /// -/// Used in [MessageWidget]. -/// {@endtemplate} +/// [StreamFileAttachment] presents a file attachment, including the file +/// name, size, and appropriate actions based on the message state. +/// +/// {@tool snippet} +/// +/// Basic usage: +/// +/// ```dart +/// StreamFileAttachment( +/// message: message, +/// file: fileAttachment, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamFileAttachmentProps], which configures this widget. +/// * [DefaultStreamFileAttachment], the default implementation. class StreamFileAttachment extends StatelessWidget { - /// {@macro streamFileAttachment} - const StreamFileAttachment({ + /// Creates a [StreamFileAttachment]. + StreamFileAttachment({ super.key, + required Message message, + required Attachment file, + BoxConstraints? constraints, + Widget? title, + Widget? trailing, + }) : props = .new( + message: message, + file: file, + constraints: constraints, + title: title, + trailing: trailing, + ); + + /// The properties that configure this attachment. + final StreamFileAttachmentProps props; + + @override + Widget build(BuildContext context) { + final builder = context.chatComponentBuilder(); + if (builder != null) return builder(context, props); + return DefaultStreamFileAttachment(props: props); + } +} + +/// Properties for configuring a [StreamFileAttachment]. +/// +/// This class holds all the configuration options for a file attachment, +/// allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamFileAttachment], which uses these properties. +/// * [DefaultStreamFileAttachment], the default implementation. +class StreamFileAttachmentProps { + /// Creates properties for a file attachment. + const StreamFileAttachmentProps({ required this.message, required this.file, this.title, this.trailing, - this.shape, - this.backgroundColor, - this.constraints = const BoxConstraints(), + this.constraints, }); /// The [Message] that the file is attached to. @@ -31,18 +82,8 @@ class StreamFileAttachment extends StatelessWidget { /// The [Attachment] object containing the file information. final Attachment file; - /// The shape of the attachment. - /// - /// Defaults to [RoundedRectangleBorder] with a radius of 12. - final ShapeBorder? shape; - - /// The background color of the attachment. - /// - /// Defaults to [StreamChatTheme.colorTheme.barsBg]. - final Color? backgroundColor; - /// The constraints to use when displaying the file. - final BoxConstraints constraints; + final BoxConstraints? constraints; /// Widget for displaying the title of the attachment. /// (usually the file name) @@ -51,65 +92,75 @@ class StreamFileAttachment extends StatelessWidget { /// Widget for displaying at the end of the attachment. /// (such as a download button) final Widget? trailing; +} + +const _kDefaultConstraints = BoxConstraints( + minWidth: 256, + maxWidth: 256, + minHeight: 64, +); + +/// The default implementation of [StreamFileAttachment]. +/// +/// Renders the file information with download and upload controls. +/// +/// See also: +/// +/// * [StreamFileAttachment], the public API widget. +/// * [StreamFileAttachmentProps], which configures this widget. +class DefaultStreamFileAttachment extends StatelessWidget { + /// Creates a default Stream file attachment. + const DefaultStreamFileAttachment({ + super.key, + required this.props, + }); + + /// The properties that configure this attachment. + final StreamFileAttachmentProps props; @override Widget build(BuildContext context) { - final chatTheme = StreamChatTheme.of(context); - final textTheme = chatTheme.textTheme; - final colorTheme = chatTheme.colorTheme; - - final backgroundColor = this.backgroundColor ?? colorTheme.barsBg; - final shape = this.shape ?? - RoundedRectangleBorder( - side: BorderSide( - color: colorTheme.borders, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: BorderRadius.circular(12), - ); + final file = props.file; + + final spacing = context.streamSpacing; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + final constraints = props.constraints ?? _kDefaultConstraints; - return Container( + return ConstrainedBox( constraints: constraints, - clipBehavior: Clip.hardEdge, - decoration: ShapeDecoration( - shape: shape, - color: backgroundColor, - ), - child: Row( - children: [ - Container( - width: 34, - height: 40, - margin: const EdgeInsets.all(8), - child: _FileTypeImage(file: file), - ), - const SizedBox(width: 8), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - file.title ?? context.translations.fileText, - maxLines: 1, - style: textTheme.bodyBold, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 3), - _FileAttachmentSubtitle(attachment: file), - ], + child: Padding( + padding: .all(spacing.sm), + child: Row( + spacing: spacing.sm, + children: [ + SizedBox( + width: 32, + height: 40, + child: _FileTypeImage(file: file), ), - ), - const SizedBox(width: 8), - Material( - type: MaterialType.transparency, - child: trailing ?? - _Trailing( - attachment: file, - message: message, - ), - ), - ], + Expanded( + child: Column( + spacing: spacing.xxs, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + maxLines: 2, + file.title ?? context.translations.fileText, + style: textTheme.captionEmphasis.copyWith(color: colorScheme.textPrimary), + overflow: TextOverflow.ellipsis, + ), + _FileAttachmentSubtitle(attachment: file), + ], + ), + ), + Material( + type: MaterialType.transparency, + child: props.trailing ?? _Trailing(attachment: file, message: props.message), + ), + ], + ), ), ); } @@ -171,12 +222,9 @@ class _Trailing extends StatelessWidget { if (message.state.isCompleted) { return IconButton( - icon: StreamSvgIcon( - icon: StreamSvgIcons.cloudDownload, - color: theme.colorTheme.textHighEmphasis, - ), + icon: Icon(context.streamIcons.arrowDown20), + color: theme.colorTheme.textHighEmphasis, visualDensity: VisualDensity.compact, - splashRadius: 16, onPressed: () async { final assetUrl = attachment.assetUrl; if (assetUrl != null) { @@ -194,8 +242,8 @@ class _Trailing extends StatelessWidget { preparing: () => Padding( padding: const EdgeInsets.all(8), child: _TrailingButton( - icon: StreamSvgIcon( - icon: StreamSvgIcons.close, + icon: Icon( + context.streamIcons.xmark20, color: theme.colorTheme.barsBg, ), fillColor: theme.colorTheme.overlayDark, @@ -205,8 +253,8 @@ class _Trailing extends StatelessWidget { inProgress: (_, __) => Padding( padding: const EdgeInsets.all(8), child: _TrailingButton( - icon: StreamSvgIcon( - icon: StreamSvgIcons.close, + icon: Icon( + context.streamIcons.xmark20, color: theme.colorTheme.barsBg, ), fillColor: theme.colorTheme.overlayDark, @@ -218,8 +266,8 @@ class _Trailing extends StatelessWidget { child: CircleAvatar( backgroundColor: theme.colorTheme.accentPrimary, maxRadius: 12, - child: StreamSvgIcon( - icon: StreamSvgIcons.check, + child: Icon( + context.streamIcons.checkmark20, color: theme.colorTheme.barsBg, ), ), @@ -227,8 +275,8 @@ class _Trailing extends StatelessWidget { failed: (_) => Padding( padding: const EdgeInsets.all(8), child: _TrailingButton( - icon: StreamSvgIcon( - icon: StreamSvgIcons.retry, + icon: Icon( + context.streamIcons.retry20, color: theme.colorTheme.barsBg, ), fillColor: theme.colorTheme.overlayDark, @@ -281,11 +329,11 @@ class _FileAttachmentSubtitle extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + final size = attachment.file?.size ?? attachment.extraData['file_size']; - final textStyle = theme.textTheme.footnote.copyWith( - color: theme.colorTheme.textLowEmphasis, - ); + final textStyle = textTheme.metadataDefault.copyWith(color: colorScheme.textPrimary); return attachment.uploadState.when( preparing: () => Text(fileSize(size), style: textStyle), inProgress: (sent, total) => StreamUploadProgressIndicator( @@ -293,13 +341,10 @@ class _FileAttachmentSubtitle extends StatelessWidget { total: total, showBackground: false, textStyle: textStyle, - progressIndicatorColor: theme.colorTheme.accentPrimary, + progressIndicatorColor: colorScheme.accentPrimary, ), success: () => Text(fileSize(size), style: textStyle), - failed: (_) => Text( - context.translations.uploadErrorLabel, - style: textStyle, - ), + failed: (_) => Text(context.translations.uploadErrorLabel, style: textStyle), ); } } diff --git a/packages/stream_chat_flutter/lib/src/attachment/gallery_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/gallery_attachment.dart index 22cbf84265..5d19b83d08 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/gallery_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/gallery_attachment.dart @@ -2,123 +2,176 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/misc/flex_grid.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@template streamGalleryAttachment} -/// Constructs a gallery of images, videos, and gifs from a list of attachments. +/// A responsive grid layout for multiple media attachments. /// -/// This widget uses a [FlexGrid] to display the attachments in a grid format. -/// The grid will automatically resize based on the size of the attachment. -/// {@endtemplate} +/// [StreamGalleryAttachment] arranges two or more image, video, or GIF +/// attachments in a responsive grid layout. +/// +/// {@tool snippet} +/// +/// Basic usage: +/// +/// ```dart +/// StreamGalleryAttachment( +/// message: message, +/// attachments: mediaAttachments, +/// itemBuilder: (context, index) => MyMediaThumbnail( +/// attachment: mediaAttachments[index], +/// ), +/// ) +/// ``` +/// {@end-tool} /// /// See also: /// -/// * [StreamImageAttachmentThumbnail], which is used to display the image -/// thumbnails. -/// * [StreamVideoAttachmentThumbnail], which is used to display the video -/// thumbnails. -/// * [StreamGiphyAttachmentThumbnail], which is used to display the gif -/// thumbnails. +/// * [StreamGalleryAttachmentProps], which configures this widget. +/// * [DefaultStreamGalleryAttachment], the default implementation. class StreamGalleryAttachment extends StatelessWidget { - /// {@macro streamGalleryAttachment} - const StreamGalleryAttachment({ + /// Creates a [StreamGalleryAttachment]. + StreamGalleryAttachment({ super.key, - required this.attachments, + required Message message, + required List attachments, + BoxConstraints? constraints, + double? spacing, + double? runSpacing, + required IndexedWidgetBuilder itemBuilder, + }) : props = .new( + message: message, + attachments: attachments, + constraints: constraints, + spacing: spacing, + runSpacing: runSpacing, + itemBuilder: itemBuilder, + ); + + /// The properties that configure this attachment. + final StreamGalleryAttachmentProps props; + + @override + Widget build(BuildContext context) { + final builder = context.chatComponentBuilder(); + if (builder != null) return builder(context, props); + return DefaultStreamGalleryAttachment(props: props); + } +} + +/// Properties for configuring a [StreamGalleryAttachment]. +/// +/// This class holds all the configuration options for a gallery attachment, +/// allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamGalleryAttachment], which uses these properties. +/// * [DefaultStreamGalleryAttachment], the default implementation. +class StreamGalleryAttachmentProps { + /// Creates properties for a gallery attachment. + const StreamGalleryAttachmentProps({ required this.message, - this.shape, - this.constraints = const BoxConstraints(), - this.spacing = 2.0, - this.runSpacing = 2.0, + required this.attachments, + this.constraints, + this.spacing, + this.runSpacing, required this.itemBuilder, }); - /// List of attachments to show - final List attachments; - - /// The [Message] that the images are attached to + /// The [Message] that the images are attached to. final Message message; - /// The shape of the attachment. - /// - /// Defaults to [RoundedRectangleBorder] with a radius of 14. - final ShapeBorder? shape; + /// The list of media attachments to display in the grid. + final List attachments; - /// The constraints of the [attachments] - final BoxConstraints constraints; + /// The constraints to use when displaying the gallery. + final BoxConstraints? constraints; /// How much space to place between children in a run in the main axis. /// /// For example, if [spacing] is 10.0, the children will be spaced at least /// 10.0 logical pixels apart in the main axis. /// - /// Defaults to 2.0. - final double spacing; + /// When null, defaults to [StreamSpacing.xxs]. + final double? spacing; /// How much space to place between the runs themselves in the cross axis. /// /// For example, if [runSpacing] is 10.0, the runs will be spaced at least /// 10.0 logical pixels apart in the cross axis. /// - /// Defaults to 2.0. - final double runSpacing; + /// When null, defaults to [StreamSpacing.xxs]. + final double? runSpacing; /// Item builder for the gallery. final IndexedWidgetBuilder itemBuilder; +} + +const _kDefaultConstraints = BoxConstraints.tightFor(width: 256, height: 195); + +/// The default implementation of [StreamGalleryAttachment]. +/// +/// Renders a responsive grid of media attachment thumbnails. +/// +/// See also: +/// +/// * [StreamGalleryAttachment], the public API widget. +/// * [StreamGalleryAttachmentProps], which configures this widget. +class DefaultStreamGalleryAttachment extends StatelessWidget { + /// Creates a default Stream gallery attachment. + const DefaultStreamGalleryAttachment({ + super.key, + required this.props, + }); + + /// The properties that configure this attachment. + final StreamGalleryAttachmentProps props; @override Widget build(BuildContext context) { + final attachments = props.attachments; assert( attachments.length >= 2, 'Gallery should have at least 2 attachments, found ${attachments.length}', ); - final chatTheme = StreamChatTheme.of(context); - final colorTheme = chatTheme.colorTheme; - final shape = this.shape ?? - RoundedRectangleBorder( - side: BorderSide( - color: colorTheme.borders, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: BorderRadius.circular(14), - ); + final streamSpacing = context.streamSpacing; + final constraints = props.constraints ?? _kDefaultConstraints; + + final spacing = props.spacing ?? streamSpacing.xxs; + final runSpacing = props.runSpacing ?? streamSpacing.xxs; - return Container( + return ConstrainedBox( constraints: constraints, - clipBehavior: Clip.hardEdge, - decoration: ShapeDecoration(shape: shape), - // Added a builder just for the sake of calculating the image count - // and building the appropriate layout based on the image count. child: Builder( - builder: (context) { - final attachmentCount = attachments.length; - if (attachmentCount == 2) { - return _buildForTwo(context, attachments); - } - - if (attachmentCount == 3) { - return _buildForThree(context, attachments); - } - - return _buildForFourOrMore(context, attachments); + builder: (context) => switch (attachments.length) { + 2 => _buildForTwo(context, attachments, props.itemBuilder, spacing: spacing, runSpacing: runSpacing), + 3 => _buildForThree(context, attachments, props.itemBuilder, spacing: spacing, runSpacing: runSpacing), + _ => _buildForFourOrMore(context, attachments, props.itemBuilder, spacing: spacing, runSpacing: runSpacing), }, ), ); } - Widget _buildForTwo(BuildContext context, List attachments) { + Widget _buildForTwo( + BuildContext context, + List attachments, + IndexedWidgetBuilder itemBuilder, { + required double spacing, + required double runSpacing, + }) { final aspectRatio1 = attachments[0].originalSize?.aspectRatio; final aspectRatio2 = attachments[1].originalSize?.aspectRatio; - // check if one image is landscape and other is portrait or vice versa + // Check if one image is landscape and other is portrait or vice versa. final isLandscape1 = aspectRatio1 != null && aspectRatio1 > 1; final isLandscape2 = aspectRatio2 != null && aspectRatio2 > 1; // Both the images are landscape. + // ---------- + // | | + // ---------- + // | | + // ---------- if (isLandscape1 && isLandscape2) { - // ---------- - // | | - // ---------- - // | | - // ---------- return FlexGrid( pattern: const [ [1], @@ -134,12 +187,12 @@ class StreamGalleryAttachment extends StatelessWidget { } // Both the images are portrait. + // ----------- + // | | | + // | | | + // | | | + // ----------- if (!isLandscape1 && !isLandscape2) { - // ----------- - // | | | - // | | | - // | | | - // ----------- return FlexGrid( pattern: const [ [1, 1], @@ -180,7 +233,13 @@ class StreamGalleryAttachment extends StatelessWidget { ); } - Widget _buildForThree(BuildContext context, List attachments) { + Widget _buildForThree( + BuildContext context, + List attachments, + IndexedWidgetBuilder itemBuilder, { + required double spacing, + required double runSpacing, + }) { final aspectRatio1 = attachments[0].originalSize?.aspectRatio; final isLandscape1 = aspectRatio1 != null && aspectRatio1 > 1; @@ -219,7 +278,12 @@ class StreamGalleryAttachment extends StatelessWidget { } Widget _buildForFourOrMore( - BuildContext context, List attachments) { + BuildContext context, + List attachments, + IndexedWidgetBuilder itemBuilder, { + required double spacing, + required double runSpacing, + }) { final pattern = >[]; final children = []; @@ -233,6 +297,10 @@ class StreamGalleryAttachment extends StatelessWidget { children.add(itemBuilder(context, i)); } + final radius = context.streamRadius; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + // ----------- // | | | // | | | @@ -248,15 +316,15 @@ class StreamGalleryAttachment extends StatelessWidget { children: children, overlayBuilder: (context, remaining) { return IgnorePointer( - child: ColoredBox( - color: Colors.black38, + child: Material( + clipBehavior: .hardEdge, + color: colorScheme.backgroundOverlayDark, + shape: RoundedSuperellipseBorder(borderRadius: .all(radius.md)), child: Center( child: Text( '+$remaining', - style: const TextStyle( - fontSize: 26, - color: Colors.white, - fontWeight: FontWeight.bold, + style: textTheme.headingLg.copyWith( + color: colorScheme.textOnAccent, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index e370cd00bf..6d5dba2192 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -1,19 +1,69 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/misc/giphy_chip.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@template streamGiphyAttachment} -/// Shows a GIF attachment in a [StreamMessageWidget]. -/// {@endtemplate} +/// A Giphy GIF attachment component with automatic sizing. +/// +/// [StreamGiphyAttachment] displays a Giphy GIF attachment, automatically +/// sized based on the GIF's metadata dimensions. +/// +/// {@tool snippet} +/// +/// Basic usage: +/// +/// ```dart +/// StreamGiphyAttachment( +/// message: message, +/// giphy: giphyAttachment, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamGiphyAttachmentProps], which configures this widget. +/// * [DefaultStreamGiphyAttachment], the default implementation. class StreamGiphyAttachment extends StatelessWidget { - /// {@macro streamGiphyAttachment} - const StreamGiphyAttachment({ + /// Creates a [StreamGiphyAttachment]. + StreamGiphyAttachment({ super.key, + required Message message, + required Attachment giphy, + GiphyInfoType type = GiphyInfoType.original, + BoxConstraints? constraints, + }) : props = .new( + message: message, + giphy: giphy, + type: type, + constraints: constraints, + ); + + /// The properties that configure this attachment. + final StreamGiphyAttachmentProps props; + + @override + Widget build(BuildContext context) { + final builder = context.chatComponentBuilder(); + if (builder != null) return builder(context, props); + return DefaultStreamGiphyAttachment(props: props); + } +} + +/// Properties for configuring a [StreamGiphyAttachment]. +/// +/// This class holds all the configuration options for a giphy attachment, +/// allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamGiphyAttachment], which uses these properties. +/// * [DefaultStreamGiphyAttachment], the default implementation. +class StreamGiphyAttachmentProps { + /// Creates properties for a giphy attachment. + const StreamGiphyAttachmentProps({ required this.message, required this.giphy, this.type = GiphyInfoType.original, - this.shape, - this.constraints = const BoxConstraints(), + this.constraints, }); /// The [Message] that the giphy is attached to. @@ -24,21 +74,42 @@ class StreamGiphyAttachment extends StatelessWidget { /// The type of giphy to display. /// - /// Defaults to [GiphyInfoType.fixedHeight]. + /// Defaults to [GiphyInfoType.original]. final GiphyInfoType type; - /// The shape of the attachment. - /// - /// Defaults to [RoundedRectangleBorder] with a radius of 14. - final ShapeBorder? shape; - /// The constraints to use when displaying the giphy. - final BoxConstraints constraints; + final BoxConstraints? constraints; +} + +const _kDefaultConstraints = BoxConstraints( + minWidth: 170, + maxWidth: 256, + minHeight: 100, + maxHeight: 300, +); + +/// The default implementation of [StreamGiphyAttachment]. +/// +/// Renders the GIF thumbnail with upload progress indication. +/// +/// See also: +/// +/// * [StreamGiphyAttachment], the public API widget. +/// * [StreamGiphyAttachmentProps], which configures this widget. +class DefaultStreamGiphyAttachment extends StatelessWidget { + /// Creates a default Stream giphy attachment. + const DefaultStreamGiphyAttachment({ + super.key, + required this.props, + }); + + /// The properties that configure this attachment. + final StreamGiphyAttachmentProps props; @override Widget build(BuildContext context) { BoxFit? fit; - final giphyInfo = giphy.giphyInfo(type); + final giphyInfo = props.giphy.giphyInfo(props.type); Size? giphySize; if (giphyInfo != null) { @@ -47,7 +118,7 @@ class StreamGiphyAttachment extends StatelessWidget { // If attachment size is available, we will tighten the constraints max // size to the attachment size. - var constraints = this.constraints; + var constraints = props.constraints ?? _kDefaultConstraints; if (giphySize != null) { constraints = constraints.tightenMaxSize(giphySize); } else { @@ -56,45 +127,31 @@ class StreamGiphyAttachment extends StatelessWidget { fit = BoxFit.cover; } - final chatTheme = StreamChatTheme.of(context); - final colorTheme = chatTheme.colorTheme; - final shape = this.shape ?? - RoundedRectangleBorder( - side: BorderSide( - color: colorTheme.borders, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: BorderRadius.circular(14), - ); - - return Container( + return ConstrainedBox( constraints: constraints, - clipBehavior: Clip.hardEdge, - decoration: ShapeDecoration(shape: shape), child: AspectRatio( aspectRatio: giphySize?.aspectRatio ?? 1, child: Stack( + fit: .expand, alignment: Alignment.center, children: [ StreamGiphyAttachmentThumbnail( - type: type, - giphy: giphy, + type: props.type, + giphy: props.giphy, fit: fit, - width: double.infinity, - height: double.infinity, ), - if (giphy.uploadState.isSuccess) - const Positioned( + if (props.giphy.uploadState.isSuccess) + PositionedDirectional( bottom: 8, - left: 8, - child: GiphyChip(), + start: 8, + child: StreamImageSourceBadge.giphy, ) else Padding( padding: const EdgeInsets.all(8), child: StreamAttachmentUploadStateBuilder( - message: message, - attachment: giphy, + message: props.message, + attachment: props.giphy, ), ), ], diff --git a/packages/stream_chat_flutter/lib/src/attachment/handler/common.dart b/packages/stream_chat_flutter/lib/src/attachment/handler/common.dart index 8c58fe2dc3..256c8ac6da 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/handler/common.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/handler/common.dart @@ -78,8 +78,7 @@ Future downloadAttachmentData( queryParameters: queryParameters, cancelToken: cancelToken, // set responseType to `bytes` - options: options?.copyWith(responseType: ResponseType.bytes) ?? - Options(responseType: ResponseType.bytes), + options: options?.copyWith(responseType: ResponseType.bytes) ?? Options(responseType: ResponseType.bytes), ); final bytes = Uint8List.fromList(response.data!); 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..ef3568b165 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 @@ -11,8 +11,7 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { /// Returns the singleton instance of [StreamAttachmentHandler]. // ignore: prefer_constructors_over_static_methods - static StreamAttachmentHandler get instance => - _instance ??= StreamAttachmentHandler._(); + static StreamAttachmentHandler get instance => _instance ??= StreamAttachmentHandler._(); late final _filePicker = FilePicker.platform; @@ -23,8 +22,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 02d051f72c..f6e67e64f1 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 @@ -8,6 +8,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:stream_chat_flutter/src/attachment/handler/common.dart'; import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler_base.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:video_player/video_player.dart'; /// StreamAttachmentHandler implementation for desktop. class StreamAttachmentHandlerDesktop extends StreamAttachmentHandler { @@ -64,8 +65,7 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { /// Returns the singleton instance of [StreamAttachmentHandler]. // ignore: prefer_constructors_over_static_methods - static StreamAttachmentHandler get instance => - _instance ??= StreamAttachmentHandler._(); + static StreamAttachmentHandler get instance => _instance ??= StreamAttachmentHandler._(); late final _imagePicker = ImagePicker(); late final _filePicker = FilePicker.platform; @@ -101,7 +101,26 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { maxDuration: maxDuration, ); - return video?.toAttachment(type: 'video'); + if (video == null) return null; + + final attachment = await video.toAttachment(type: 'video'); + + final videoController = VideoPlayerController.file(File(video.path)); + try { + await videoController.initialize(); + final duration = videoController.value.duration; + if (duration.inSeconds > 0) { + return attachment.copyWith( + extraData: {...attachment.extraData, 'duration': duration.inSeconds}, + ); + } + } catch (_) { + // If duration extraction fails, return the attachment without it. + } finally { + await videoController.dispose(); + } + + return attachment; } @override @@ -111,8 +130,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, @@ -142,9 +159,7 @@ class StreamAttachmentHandler extends StreamAttachmentHandlerBase { final tempDir = await getTemporaryDirectory(); final tempPath = Uri.file(tempDir.path, windows: CurrentPlatform.isWindows); - final tempFilePath = tempPath - .resolve(fileName!) - .toFilePath(windows: CurrentPlatform.isWindows); + final tempFilePath = tempPath.resolve(fileName!).toFilePath(windows: CurrentPlatform.isWindows); final attachmentFileBytes = attachmentFile.bytes; if (attachmentFileBytes == null) { diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index 5a3837b487..c0c5afdd83 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -1,20 +1,69 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@template streamImageAttachment} -/// Shows an image attachment in a [StreamMessageWidget]. -/// {@endtemplate} +/// An image attachment component with automatic sizing. +/// +/// [StreamImageAttachment] displays an image attachment, automatically +/// sized based on the image's original dimensions. +/// +/// {@tool snippet} +/// +/// Basic usage: +/// +/// ```dart +/// StreamImageAttachment( +/// message: message, +/// image: imageAttachment, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamImageAttachmentProps], which configures this widget. +/// * [DefaultStreamImageAttachment], the default implementation. class StreamImageAttachment extends StatelessWidget { - /// {@macro streamImageAttachment} - const StreamImageAttachment({ + /// Creates a [StreamImageAttachment]. + StreamImageAttachment({ super.key, + required Message message, + required Attachment image, + BoxConstraints? constraints, + ImageResize? resize, + }) : props = .new( + message: message, + image: image, + constraints: constraints, + resize: resize, + ); + + /// The properties that configure this attachment. + final StreamImageAttachmentProps props; + + @override + Widget build(BuildContext context) { + final builder = context.chatComponentBuilder(); + if (builder != null) return builder(context, props); + return DefaultStreamImageAttachment(props: props); + } +} + +/// Properties for configuring a [StreamImageAttachment]. +/// +/// This class holds all the configuration options for an image attachment, +/// allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamImageAttachment], which uses these properties. +/// * [DefaultStreamImageAttachment], the default implementation. +class StreamImageAttachmentProps { + /// Creates properties for an image attachment. + const StreamImageAttachmentProps({ required this.message, required this.image, - this.shape, - this.constraints = const BoxConstraints(), - this.imageThumbnailSize, - this.imageThumbnailResizeType = 'clip', - this.imageThumbnailCropType = 'center', + this.constraints, + this.resize, }); /// The [Message] that the image is attached to. @@ -23,35 +72,52 @@ class StreamImageAttachment extends StatelessWidget { /// The [Attachment] object containing the image information. final Attachment image; - /// The shape of the attachment. - /// - /// Defaults to [RoundedRectangleBorder] with a radius of 14. - final ShapeBorder? shape; - /// The constraints to use when displaying the image. - final BoxConstraints constraints; + final BoxConstraints? constraints; - /// Size of the attachment image thumbnail. - final Size? imageThumbnailSize; - - /// Resize type of the image attachment thumbnail. + /// The resize configuration for the image attachment thumbnail. /// - /// Defaults to [crop] - final String /*clip|crop|scale|fill*/ imageThumbnailResizeType; - - /// Crop type of the image attachment thumbnail. + /// When provided, its [ImageResize.width] and [ImageResize.height] are used + /// directly as the CDN resize dimensions. /// - /// Defaults to [center] - final String /*center|top|bottom|left|right*/ imageThumbnailCropType; + /// When null, the size is auto-calculated from the layout constraints + /// and defaults to [ResizeMode.clip] and [CropMode.center]. + final ImageResize? resize; +} + +const _kDefaultConstraints = BoxConstraints( + minWidth: 170, + maxWidth: 256, + minHeight: 100, + maxHeight: 300, +); + +/// The default implementation of [StreamImageAttachment]. +/// +/// Renders the image thumbnail with upload progress indication. +/// +/// See also: +/// +/// * [StreamImageAttachment], the public API widget. +/// * [StreamImageAttachmentProps], which configures this widget. +class DefaultStreamImageAttachment extends StatelessWidget { + /// Creates a default Stream image attachment. + const DefaultStreamImageAttachment({ + super.key, + required this.props, + }); + + /// The properties that configure this attachment. + final StreamImageAttachmentProps props; @override Widget build(BuildContext context) { BoxFit? fit; - final imageSize = image.originalSize; + final imageSize = props.image.originalSize; // If attachment size is available, we will tighten the constraints max // size to the attachment size. - var constraints = this.constraints; + var constraints = props.constraints ?? _kDefaultConstraints; if (imageSize != null) { constraints = constraints.tightenMaxSize(imageSize); } else { @@ -60,40 +126,24 @@ class StreamImageAttachment extends StatelessWidget { fit = BoxFit.cover; } - final chatTheme = StreamChatTheme.of(context); - final colorTheme = chatTheme.colorTheme; - final shape = this.shape ?? - RoundedRectangleBorder( - side: BorderSide( - color: colorTheme.borders, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: BorderRadius.circular(14), - ); - - return Container( + return ConstrainedBox( constraints: constraints, - clipBehavior: Clip.hardEdge, - decoration: ShapeDecoration(shape: shape), child: AspectRatio( aspectRatio: imageSize?.aspectRatio ?? 1, child: Stack( + fit: .expand, alignment: Alignment.center, children: [ StreamImageAttachmentThumbnail( - image: image, + image: props.image, fit: fit, - width: double.infinity, - height: double.infinity, - thumbnailSize: imageThumbnailSize, - thumbnailResizeType: imageThumbnailResizeType, - thumbnailCropType: imageThumbnailCropType, + resize: props.resize, ), Padding( padding: const EdgeInsets.all(8), child: StreamAttachmentUploadStateBuilder( - message: message, - attachment: image, + message: props.message, + attachment: props.image, ), ), ], diff --git a/packages/stream_chat_flutter/lib/src/attachment/link_preview_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/link_preview_attachment.dart new file mode 100644 index 0000000000..9a0e9a311e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/attachment/link_preview_attachment.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A link preview attachment with Open Graph metadata. +/// +/// [StreamLinkPreviewAttachment] presents a link preview, showing the +/// page's image, title, and description. +/// +/// {@tool snippet} +/// +/// Basic usage: +/// +/// ```dart +/// StreamLinkPreviewAttachment( +/// message: message, +/// urlAttachment: urlAttachment, +/// hostDisplayName: 'GitHub', +/// messageTheme: messageTheme, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamLinkPreviewAttachmentProps], which configures this widget. +/// * [DefaultStreamLinkPreviewAttachment], the default implementation. +class StreamLinkPreviewAttachment extends StatelessWidget { + /// Creates a [StreamLinkPreviewAttachment]. + StreamLinkPreviewAttachment({ + super.key, + required Message message, + required Attachment urlAttachment, + BoxConstraints? constraints, + }) : props = .new( + message: message, + urlAttachment: urlAttachment, + constraints: constraints, + ); + + /// The properties that configure this attachment. + final StreamLinkPreviewAttachmentProps props; + + @override + Widget build(BuildContext context) { + final builder = context.chatComponentBuilder(); + if (builder != null) return builder(context, props); + return DefaultStreamLinkPreviewAttachment(props: props); + } +} + +/// Properties for configuring a [StreamLinkPreviewAttachment]. +/// +/// This class holds all the configuration options for a link preview +/// attachment, allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamLinkPreviewAttachment], which uses these properties. +/// * [DefaultStreamLinkPreviewAttachment], the default implementation. +class StreamLinkPreviewAttachmentProps { + /// Creates properties for a link preview attachment. + const StreamLinkPreviewAttachmentProps({ + required this.message, + required this.urlAttachment, + this.constraints, + }); + + /// The [Message] that the image is attached to. + final Message message; + + /// Attachment to be displayed. + final Attachment urlAttachment; + + /// The constraints to use when displaying the link preview. + final BoxConstraints? constraints; +} + +const _kDefaultConstraints = BoxConstraints(maxWidth: 256); + +/// The default implementation of [StreamLinkPreviewAttachment]. +/// +/// Renders the Open Graph preview with host name, title, and description. +/// +/// See also: +/// +/// * [StreamLinkPreviewAttachment], the public API widget. +/// * [StreamLinkPreviewAttachmentProps], which configures this widget. +class DefaultStreamLinkPreviewAttachment extends StatelessWidget { + /// Creates a default Stream link preview attachment. + const DefaultStreamLinkPreviewAttachment({ + super.key, + required this.props, + }); + + /// The properties that configure this attachment. + final StreamLinkPreviewAttachmentProps props; + + @override + Widget build(BuildContext context) { + final urlAttachment = props.urlAttachment; + + final icons = context.streamIcons; + final spacing = context.streamSpacing; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + + final constraints = props.constraints ?? _kDefaultConstraints; + + return ConstrainedBox( + constraints: constraints, + child: Column( + mainAxisSize: .min, + children: [ + AspectRatio( + // Default aspect ratio for Open Graph images. + // https://www.kapwing.com/resources/what-is-an-og-image-make-and-format-og-images-for-your-blog-or-webpage + aspectRatio: 1.91 / 1, + child: StreamImageAttachmentThumbnail( + image: urlAttachment, + fit: BoxFit.cover, + ), + ), + Padding( + padding: .all(spacing.sm), + child: Column( + spacing: spacing.xxs, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (urlAttachment.title case final title?) + Text( + title.trim(), + maxLines: 1, + overflow: .ellipsis, + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + ), + ), + if (urlAttachment.text case final text?) + Text( + text, + maxLines: 3, + overflow: .ellipsis, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textPrimary, + ), + ), + if (urlAttachment.titleLink case final titleLink?) + Row( + mainAxisSize: .min, + spacing: spacing.xxs, + children: [ + Icon(icons.link12, size: 12), + Expanded( + child: Text( + titleLink, + maxLines: 1, + overflow: .ellipsis, + style: textTheme.metadataDefault.copyWith( + color: colorScheme.textPrimary, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} 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 55% 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..51eedaa113 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/poll_message.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/poll_attachment.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/components/stream_chat_component_builders.dart'; import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; import 'package:stream_chat_flutter/src/poll/interactor/poll_add_comment_dialog.dart'; import 'package:stream_chat_flutter/src/poll/interactor/poll_end_vote_dialog.dart'; @@ -9,42 +10,108 @@ 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_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; -const _maxVisibleOptionCount = 10; +/// An interactive poll attachment with voting and results. +/// +/// [StreamPollAttachment] presents an interactive poll, supporting +/// voting, comments, and results viewing. +/// +/// {@tool snippet} +/// +/// Basic usage: +/// +/// ```dart +/// StreamPollAttachment( +/// message: message, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamPollAttachmentProps], which configures this widget. +/// * [DefaultStreamPollAttachment], the default implementation. +class StreamPollAttachment extends StatelessWidget { + /// Creates a [StreamPollAttachment]. + StreamPollAttachment({ + super.key, + required Message message, + BoxConstraints? constraints, + }) : props = .new( + message: message, + constraints: constraints, + ); -const _kDefaultPollMessageConstraints = BoxConstraints( - maxWidth: 270, -); + /// The properties that configure this attachment. + final StreamPollAttachmentProps props; -/// {@template pollMessage} -/// A widget that displays a poll message. + @override + Widget build(BuildContext context) { + final builder = context.chatComponentBuilder(); + if (builder != null) return builder(context, props); + return DefaultStreamPollAttachment(props: props); + } +} + +/// Properties for configuring a [StreamPollAttachment]. /// -/// Used in [MessageCard] to display a poll message. -/// {@endtemplate} -class PollMessage extends StatefulWidget { - /// {@macro pollMessage} - const PollMessage({ - super.key, +/// This class holds all the configuration options for a poll attachment, +/// allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamPollAttachment], which uses these properties. +/// * [DefaultStreamPollAttachment], the default implementation. +class StreamPollAttachmentProps { + /// Creates properties for a poll attachment. + const StreamPollAttachmentProps({ required this.message, + this.constraints, }); - /// The message with the poll to display. + /// The message containing the poll. final Message message; + /// The constraints to use when displaying the poll. + final BoxConstraints? constraints; +} + +const _maxVisibleOptionCount = 10; +const _kDefaultConstraints = BoxConstraints(maxWidth: 270); + +/// The default implementation of [StreamPollAttachment]. +/// +/// Renders an interactive poll with voting and result controls. +/// +/// See also: +/// +/// * [StreamPollAttachment], the public API widget. +/// * [StreamPollAttachmentProps], which configures this widget. +class DefaultStreamPollAttachment extends StatefulWidget { + /// Creates a default Stream poll attachment. + const DefaultStreamPollAttachment({ + super.key, + required this.props, + }); + + /// The properties that configure this attachment. + final StreamPollAttachmentProps props; + @override - State createState() => _PollMessageState(); + State createState() => _DefaultStreamPollAttachmentState(); } -class _PollMessageState extends State { - late final _messageNotifier = ValueNotifier(widget.message); +class _DefaultStreamPollAttachmentState extends State { + late final _messageNotifier = ValueNotifier(widget.props.message); @override - void didUpdateWidget(covariant PollMessage oldWidget) { + void didUpdateWidget(covariant DefaultStreamPollAttachment oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.message != widget.message) { - // If the message changes, schedule an update for the next frame + if (oldWidget.props.message != widget.props.message) { + // If the message changes, schedule an update for the next frame. WidgetsBinding.instance.addPostFrameCallback((_) { - _messageNotifier.value = widget.message; + _messageNotifier.value = widget.props.message; }); } } @@ -96,8 +163,10 @@ class _PollMessageState extends State { channel.createPollOption(poll, PollOption(text: optionText)); } + final constraints = widget.props.constraints ?? _kDefaultConstraints; + return ConstrainedBox( - constraints: _kDefaultPollMessageConstraints, + constraints: constraints, child: StreamPollInteractor( poll: poll, currentUser: currentUser, diff --git a/packages/stream_chat_flutter/lib/src/attachment/thumbnail/file_attachment_thumbnail.dart b/packages/stream_chat_flutter/lib/src/attachment/thumbnail/file_attachment_thumbnail.dart index b6f14b291c..1e5bb53dae 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/thumbnail/file_attachment_thumbnail.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/thumbnail/file_attachment_thumbnail.dart @@ -20,7 +20,7 @@ class StreamFileAttachmentThumbnail extends StatelessWidget { this.width, this.height, this.fit, - this.errorBuilder = _defaultErrorBuilder, + this.errorBuilder, }); /// The file attachment to build the thumbnail for. @@ -36,17 +36,9 @@ class StreamFileAttachmentThumbnail extends StatelessWidget { final BoxFit? fit; /// Builder used when the thumbnail fails to load. - final ThumbnailErrorBuilder errorBuilder; - - // Default error builder for file attachment thumbnail. - static Widget _defaultErrorBuilder( - BuildContext context, - Object error, - StackTrace? stackTrace, - ) { - // Return a generic file type icon. - return getFileTypeImage(); - } + /// + /// If null, default error handling is used. + final ThumbnailErrorBuilder? errorBuilder; @override Widget build(BuildContext context) { @@ -54,17 +46,17 @@ class StreamFileAttachmentThumbnail extends StatelessWidget { return switch (mediaType?.type) { AttachmentType.image => StreamImageAttachmentThumbnail( - image: file, - width: width, - height: height, - fit: fit, - ), + image: file, + width: width, + height: height, + fit: fit, + ), AttachmentType.video => StreamVideoAttachmentThumbnail( - video: file, - width: width, - height: height, - fit: fit, - ), + video: file, + width: width, + height: height, + fit: fit, + ), // Return a generic file type icon. _ => getFileTypeImage(mediaType?.mimeType), }; diff --git a/packages/stream_chat_flutter/lib/src/attachment/thumbnail/giphy_attachment_thumbnail.dart b/packages/stream_chat_flutter/lib/src/attachment/thumbnail/giphy_attachment_thumbnail.dart index 952b0f04e4..ffe5b7d576 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/thumbnail/giphy_attachment_thumbnail.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/thumbnail/giphy_attachment_thumbnail.dart @@ -17,7 +17,7 @@ class StreamGiphyAttachmentThumbnail extends StatelessWidget { this.width, this.height, this.fit, - this.errorBuilder = _defaultErrorBuilder, + this.errorBuilder, }); /// The giphy attachment to build the thumbnail for. @@ -36,22 +36,9 @@ class StreamGiphyAttachmentThumbnail extends StatelessWidget { final BoxFit? fit; /// Builder used when the thumbnail fails to load. - final ThumbnailErrorBuilder errorBuilder; - - // Default error builder for image attachment thumbnail. - static Widget _defaultErrorBuilder( - BuildContext context, - Object error, - StackTrace? stackTrace, - ) { - return ThumbnailError( - error: error, - stackTrace: stackTrace, - height: double.infinity, - width: double.infinity, - fit: BoxFit.cover, - ); - } + /// + /// If null, default error handling is used. + final ThumbnailErrorBuilder? errorBuilder; @override Widget build(BuildContext context) { diff --git a/packages/stream_chat_flutter/lib/src/attachment/thumbnail/image_attachment_thumbnail.dart b/packages/stream_chat_flutter/lib/src/attachment/thumbnail/image_attachment_thumbnail.dart index 55474beae4..78cec4d8d9 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/thumbnail/image_attachment_thumbnail.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/thumbnail/image_attachment_thumbnail.dart @@ -1,13 +1,9 @@ import 'dart:io' show File; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:shimmer/shimmer.dart'; -import 'package:stream_chat_flutter/src/attachment/thumbnail/thumbnail_error.dart'; import 'package:stream_chat_flutter/src/attachment/thumbnail/thumbnail_size_calculator.dart'; -import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/utils/utils.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template imageAttachmentThumbnail} /// Widget for building image attachment thumbnail. @@ -22,10 +18,8 @@ class StreamImageAttachmentThumbnail extends StatelessWidget { this.width, this.height, this.fit, - this.thumbnailSize, - this.thumbnailResizeType = 'clip', - this.thumbnailCropType = 'center', - this.errorBuilder = _defaultErrorBuilder, + this.resize, + this.errorBuilder, }); /// The image attachment to show. @@ -40,70 +34,52 @@ class StreamImageAttachmentThumbnail extends StatelessWidget { /// Fit of the attachment image thumbnail. final BoxFit? fit; - /// Size of the attachment image thumbnail. - final Size? thumbnailSize; - - /// Resize type of the image attachment thumbnail. + /// The resize configuration for the image attachment thumbnail. /// - /// Defaults to [crop] - final String /*clip|crop|scale|fill*/ thumbnailResizeType; - - /// Crop type of the image attachment thumbnail. + /// When provided, its [ImageResize.width] and [ImageResize.height] are used + /// directly as the CDN resize dimensions. /// - /// Defaults to [center] - final String /*center|top|bottom|left|right*/ thumbnailCropType; + /// When null, the size is auto-calculated from the layout constraints + /// and defaults to [ResizeMode.clip] and [CropMode.center]. + final ImageResize? resize; /// Builder used when the thumbnail fails to load. - final ThumbnailErrorBuilder errorBuilder; - - // Default error builder for image attachment thumbnail. - static Widget _defaultErrorBuilder( - BuildContext context, - Object error, - StackTrace? stackTrace, - ) { - return ThumbnailError( - error: error, - stackTrace: stackTrace, - height: double.infinity, - width: double.infinity, - fit: BoxFit.cover, - ); - } + /// + /// If null, the default error handling of the underlying image widget is + /// used. For remote images, [StreamNetworkImage] provides a tap-to-retry + /// error placeholder. For local images, a static + /// [StreamImageErrorPlaceholder] is shown. + final ThumbnailErrorBuilder? errorBuilder; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { - // Calculate optimal thumbnail size once for all paths - final effectiveThumbnailSize = switch (thumbnailSize) { - final thumbnailSize? => thumbnailSize, - _ => ThumbnailSizeCalculator.calculate( - targetSize: constraints.biggest, - originalSize: image.originalSize, - pixelRatio: MediaQuery.devicePixelRatioOf(context), - ), - }; - - final cacheWidth = effectiveThumbnailSize?.width.round(); - final cacheHeight = effectiveThumbnailSize?.height.round(); + var effectiveResize = resize; + if (effectiveResize == null) { + final size = ThumbnailSizeCalculator.calculate( + targetSize: constraints.biggest, + originalSize: image.originalSize, + pixelRatio: MediaQuery.devicePixelRatioOf(context), + ); + + if (size != null) effectiveResize = .new(width: size.width, height: size.height); + } + + final cacheWidth = effectiveResize?.width.round(); + final cacheHeight = effectiveResize?.height.round(); // If the remote image URL is available, we can directly show it using // the _RemoteImageAttachment widget. final imageUrl = image.thumbUrl ?? image.imageUrl ?? image.assetUrl; if (imageUrl case final imageUrl?) { - var resizedImageUrl = imageUrl; - if (effectiveThumbnailSize case final thumbnailSize?) { - resizedImageUrl = imageUrl.getResizedImageUrl( - crop: thumbnailCropType, - resize: thumbnailResizeType, - width: thumbnailSize.width, - height: thumbnailSize.height, - ); - } + final imageCDN = StreamChatConfiguration.maybeOf(context)?.imageCDN ?? const StreamImageCDN(); + final resolvedUrl = imageCDN.resolveUrl(imageUrl, resize: effectiveResize); + final resolvedCacheKey = imageCDN.cacheKey(resolvedUrl); return _RemoteImageAttachment( - url: resizedImageUrl, + url: resolvedUrl, + cacheKey: resolvedCacheKey, width: width, height: height, fit: fit, @@ -126,11 +102,11 @@ class StreamImageAttachmentThumbnail extends StatelessWidget { ); } - return errorBuilder( - context, - 'Image attachment is not valid', - StackTrace.current, - ); + if (errorBuilder case final builder?) { + return builder(context, 'Image attachment is not valid', null); + } + + return StreamImageErrorPlaceholder(width: width, height: height); }, ); } @@ -139,7 +115,7 @@ class StreamImageAttachmentThumbnail extends StatelessWidget { class _LocalImageAttachment extends StatelessWidget { const _LocalImageAttachment({ required this.file, - required this.errorBuilder, + this.errorBuilder, this.width, this.height, this.cacheWidth, @@ -153,7 +129,17 @@ class _LocalImageAttachment extends StatelessWidget { final int? cacheWidth; final int? cacheHeight; final BoxFit? fit; - final ThumbnailErrorBuilder errorBuilder; + final ThumbnailErrorBuilder? errorBuilder; + + // Default error builder for local attachment thumbnail. + Widget _defaultErrorBuilder( + BuildContext context, + Object error, + StackTrace? stackTrace, + ) { + if (errorBuilder case final builder?) return builder(context, error, null); + return StreamImageErrorPlaceholder(width: width, height: height); + } @override Widget build(BuildContext context) { @@ -166,7 +152,7 @@ class _LocalImageAttachment extends StatelessWidget { cacheWidth: cacheWidth, cacheHeight: cacheHeight, fit: fit, - errorBuilder: errorBuilder, + errorBuilder: _defaultErrorBuilder, ); } @@ -179,12 +165,12 @@ class _LocalImageAttachment extends StatelessWidget { cacheWidth: cacheWidth, cacheHeight: cacheHeight, fit: fit, - errorBuilder: errorBuilder, + errorBuilder: _defaultErrorBuilder, ); } // Return error widget if no image is found. - return errorBuilder( + return _defaultErrorBuilder( context, 'Image attachment is not valid', StackTrace.current, @@ -195,54 +181,35 @@ class _LocalImageAttachment extends StatelessWidget { class _RemoteImageAttachment extends StatelessWidget { const _RemoteImageAttachment({ required this.url, - required this.errorBuilder, + this.cacheKey, this.width, this.height, this.cacheWidth, this.cacheHeight, this.fit, + this.errorBuilder, }); final String url; + final String? cacheKey; final double? width; final double? height; final int? cacheWidth; final int? cacheHeight; final BoxFit? fit; - final ThumbnailErrorBuilder errorBuilder; + final ThumbnailErrorBuilder? errorBuilder; @override Widget build(BuildContext context) { - return CachedNetworkImage( - imageUrl: url, + return StreamNetworkImage( + url, + cacheKey: cacheKey, width: width, height: height, - memCacheWidth: cacheWidth, - memCacheHeight: cacheHeight, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, fit: fit, - placeholder: (context, __) { - final image = Image.asset( - 'lib/assets/images/placeholder.png', - width: width, - height: height, - fit: BoxFit.cover, - package: 'stream_chat_flutter', - ); - - final colorTheme = StreamChatTheme.of(context).colorTheme; - return Shimmer.fromColors( - baseColor: colorTheme.disabled, - highlightColor: colorTheme.inputBg, - child: image, - ); - }, - errorWidget: (context, url, error) { - return errorBuilder( - context, - error, - StackTrace.current, - ); - }, + errorBuilder: errorBuilder, ); } } diff --git a/packages/stream_chat_flutter/lib/src/attachment/thumbnail/media_attachment_thumbnail.dart b/packages/stream_chat_flutter/lib/src/attachment/thumbnail/media_attachment_thumbnail.dart index ab8c60c571..f3962ac38e 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/thumbnail/media_attachment_thumbnail.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/thumbnail/media_attachment_thumbnail.dart @@ -3,7 +3,9 @@ import 'package:stream_chat_flutter/src/attachment/thumbnail/giphy_attachment_th import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart'; import 'package:stream_chat_flutter/src/attachment/thumbnail/thumbnail_error.dart'; import 'package:stream_chat_flutter/src/attachment/thumbnail/video_attachment_thumbnail.dart'; +import 'package:stream_chat_flutter/src/utils/stream_image_cdn.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template mediaAttachmentThumbnail} /// Widget for building media attachment thumbnail. @@ -24,11 +26,9 @@ class StreamMediaAttachmentThumbnail extends StatelessWidget { this.width, this.height, this.fit, - this.thumbnailSize, - this.thumbnailResizeType = 'clip', - this.thumbnailCropType = 'center', + this.resize, this.gifInfoType = GiphyInfoType.original, - this.errorBuilder = _defaultErrorBuilder, + this.errorBuilder, }); /// The giphy attachment to build the thumbnail for. @@ -44,45 +44,34 @@ class StreamMediaAttachmentThumbnail extends StatelessWidget { final BoxFit? fit; /// Builder used when the thumbnail fails to load. - final ThumbnailErrorBuilder errorBuilder; - - /// Size of the attachment image thumbnail. /// - /// Ignored if the [Attachment.type] is not [AttachmentType.image]. - final Size? thumbnailSize; + /// If null, default error handling is used. + final ThumbnailErrorBuilder? errorBuilder; - /// Resize type of the image attachment thumbnail. - /// - /// Defaults to [crop] + /// The resize configuration for the image attachment thumbnail. /// - /// Ignored if the [Attachment.type] is not [AttachmentType.image]. - final String /*clip|crop|scale|fill*/ thumbnailResizeType; - - /// Crop type of the image attachment thumbnail. + /// When provided, its [ImageResize.width] and [ImageResize.height] are used + /// directly as the CDN resize dimensions. /// - /// Defaults to [center] + /// When null, the size is auto-calculated from the layout constraints + /// and defaults to [ResizeMode.clip] and [CropMode.center]. /// /// Ignored if the [Attachment.type] is not [AttachmentType.image]. - final String /*center|top|bottom|left|right*/ thumbnailCropType; + final ImageResize? resize; /// The type of giphy thumbnail to build. /// /// Ignored if the [Attachment.type] is not [AttachmentType.giphy]. final GiphyInfoType gifInfoType; - // Default error builder for image attachment thumbnail. - static Widget _defaultErrorBuilder( + // Default error builder for media attachment thumbnail. + Widget _defaultErrorBuilder( BuildContext context, Object error, StackTrace? stackTrace, ) { - return ThumbnailError( - error: error, - stackTrace: stackTrace, - height: double.infinity, - width: double.infinity, - fit: BoxFit.cover, - ); + if (errorBuilder case final builder?) return builder(context, error, null); + return StreamImageErrorPlaceholder(width: width, height: height); } @override @@ -94,9 +83,7 @@ class StreamMediaAttachmentThumbnail extends StatelessWidget { width: width, height: height, fit: fit, - thumbnailSize: thumbnailSize, - thumbnailResizeType: thumbnailResizeType, - thumbnailCropType: thumbnailCropType, + resize: resize, errorBuilder: errorBuilder, ); } @@ -122,7 +109,7 @@ class StreamMediaAttachmentThumbnail extends StatelessWidget { ); } - return errorBuilder( + return _defaultErrorBuilder( context, 'Unsupported attachment type: $type', StackTrace.current, diff --git a/packages/stream_chat_flutter/lib/src/attachment/thumbnail/thumbnail_error.dart b/packages/stream_chat_flutter/lib/src/attachment/thumbnail/thumbnail_error.dart index d5f5a4ae56..f1db5b5433 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/thumbnail/thumbnail_error.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/thumbnail/thumbnail_error.dart @@ -1,54 +1,8 @@ import 'package:flutter/material.dart'; -/// {@template thumbnailErrorBuilder} -/// Signature for the builder callback used by [ThumbnailError.builder]. +/// Signature for a function that builds an error widget when an attachment +/// thumbnail fails to load. /// -/// The parameters represent the [BuildContext], [error] and [stackTrace] of the -/// error that triggered this callback. -/// {@endtemplate} -typedef ThumbnailErrorBuilder = Widget Function( - BuildContext context, - Object error, - StackTrace? stackTrace, -); - -/// {@template thumbnailError} -/// A widget that shows an error state when a thumbnail fails to load. -/// {@endtemplate} -class ThumbnailError extends StatelessWidget { - /// {@macro thumbnailError} - const ThumbnailError({ - super.key, - required this.error, - this.stackTrace, - this.width, - this.height, - this.fit, - }); - - /// The width of the thumbnail. - final double? width; - - /// The height of the thumbnail. - final double? height; - - /// How to inscribe the thumbnail into the space allocated during layout. - final BoxFit? fit; - - /// The error that triggered this error widget. - final Object error; - - /// The stack trace of the error that triggered this error widget. - final StackTrace? stackTrace; - - @override - Widget build(BuildContext context) { - return Image.asset( - 'lib/assets/images/placeholder.png', - width: width, - height: height, - fit: fit, - package: 'stream_chat_flutter', - ); - } -} +/// When [retry] is non-null, it can be invoked to retry loading the image +/// (e.g. to show a tap-to-reload button). +typedef ThumbnailErrorBuilder = Widget Function(BuildContext context, Object error, VoidCallback? retry); diff --git a/packages/stream_chat_flutter/lib/src/attachment/thumbnail/video_attachment_thumbnail.dart b/packages/stream_chat_flutter/lib/src/attachment/thumbnail/video_attachment_thumbnail.dart index 946023f6ec..d56479976c 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/thumbnail/video_attachment_thumbnail.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/thumbnail/video_attachment_thumbnail.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/attachment/thumbnail/image_attachment_thumbnail.dart'; import 'package:stream_chat_flutter/src/attachment/thumbnail/thumbnail_error.dart'; -import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template videoAttachmentThumbnail} /// Widget for building video attachment thumbnail. @@ -19,7 +18,7 @@ class StreamVideoAttachmentThumbnail extends StatelessWidget { this.width, this.height, this.fit, - this.errorBuilder = _defaultErrorBuilder, + this.errorBuilder, }); /// The video attachment to build the thumbnail for. @@ -35,21 +34,18 @@ class StreamVideoAttachmentThumbnail extends StatelessWidget { final BoxFit? fit; /// Builder used when the thumbnail fails to load. - final ThumbnailErrorBuilder errorBuilder; + /// + /// If null, default error handling is used. + final ThumbnailErrorBuilder? errorBuilder; - // Default error builder for image attachment thumbnail. - static Widget _defaultErrorBuilder( + // Default error builder for video attachment thumbnail. + Widget _defaultErrorBuilder( BuildContext context, Object error, StackTrace? stackTrace, ) { - return ThumbnailError( - error: error, - stackTrace: stackTrace, - height: double.infinity, - width: double.infinity, - fit: BoxFit.cover, - ); + if (errorBuilder case final builder?) return builder(context, error, null); + return StreamImageErrorPlaceholder(width: width, height: height); } @override @@ -76,31 +72,14 @@ class StreamVideoAttachmentThumbnail extends StatelessWidget { height: height, fit: fit, frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { - if (frame != null || wasSynchronouslyLoaded) { - return child; - } - - final image = Image.asset( - 'lib/assets/images/placeholder.png', - width: width, - height: height, - fit: BoxFit.cover, - package: 'stream_chat_flutter', - ); - - final colorTheme = StreamChatTheme.of(context).colorTheme; - return Shimmer.fromColors( - baseColor: colorTheme.disabled, - highlightColor: colorTheme.inputBg, - child: image, - ); + if (frame != null || wasSynchronouslyLoaded) return child; + return StreamImageLoadingPlaceholder(height: height, width: width); }, - errorBuilder: errorBuilder, + errorBuilder: _defaultErrorBuilder, ); } - // Return error widget if no thumbnail is found. - return errorBuilder( + return _defaultErrorBuilder( context, 'Video attachment is not valid', StackTrace.current, diff --git a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart deleted file mode 100644 index 6d8629ea4c..0000000000 --- a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template streamUrlAttachment} -/// Displays a URL attachment in a [StreamMessageWidget]. -/// {@endtemplate} -class StreamUrlAttachment extends StatelessWidget { - /// {@macro streamUrlAttachment} - const StreamUrlAttachment({ - super.key, - required this.message, - required this.urlAttachment, - required this.hostDisplayName, - required this.messageTheme, - this.shape, - this.constraints = const BoxConstraints(), - }); - - /// The [Message] that the image is attached to. - final Message message; - - /// Attachment to be displayed - final Attachment urlAttachment; - - /// The shape of the attachment. - /// - /// Defaults to [RoundedRectangleBorder] with a radius of 14. - final ShapeBorder? shape; - - /// The constraints to use when displaying the file. - final BoxConstraints constraints; - - /// Host name - final String hostDisplayName; - - /// The [StreamMessageThemeData] to use for the image title - final StreamMessageThemeData messageTheme; - - @override - Widget build(BuildContext context) { - final chatTheme = StreamChatTheme.of(context); - final colorTheme = chatTheme.colorTheme; - final shape = this.shape ?? - RoundedRectangleBorder( - side: BorderSide( - color: colorTheme.borders, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: BorderRadius.circular(8), - ); - - final backgroundColor = messageTheme.urlAttachmentBackgroundColor; - - return Container( - constraints: constraints, - clipBehavior: Clip.hardEdge, - decoration: ShapeDecoration( - shape: shape, - color: backgroundColor, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - children: [ - AspectRatio( - // Default aspect ratio for Open Graph images. - // https://www.kapwing.com/resources/what-is-an-og-image-make-and-format-og-images-for-your-blog-or-webpage - aspectRatio: 1.91 / 1, - child: StreamImageAttachmentThumbnail( - image: urlAttachment, - fit: BoxFit.cover, - ), - ), - Positioned( - left: 0, - bottom: 0, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topRight: Radius.circular(16), - ), - color: backgroundColor, - ), - child: Padding( - padding: const EdgeInsets.only( - top: 8, - left: 8, - right: 12, - bottom: 4, - ), - child: Text( - hostDisplayName, - style: messageTheme.urlAttachmentHostStyle, - ), - ), - ), - ), - ], - ), - Padding( - padding: const EdgeInsets.all(8), - child: Column( - spacing: 4, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (urlAttachment.title != null) - Builder(builder: (context) { - final maxLines = messageTheme.urlAttachmentTitleMaxLine; - - TextOverflow? overflow; - if (maxLines != null && maxLines > 0) { - overflow = TextOverflow.ellipsis; - } - - return Text( - urlAttachment.title!.trim(), - maxLines: maxLines, - overflow: overflow, - style: messageTheme.urlAttachmentTitleStyle, - ); - }), - if (urlAttachment.text != null) - Builder(builder: (context) { - final maxLines = messageTheme.urlAttachmentTextMaxLine; - - TextOverflow? overflow; - if (maxLines != null && maxLines > 0) { - overflow = TextOverflow.ellipsis; - } - - return Text( - urlAttachment.text!, - maxLines: maxLines, - overflow: overflow, - style: messageTheme.urlAttachmentTextStyle, - ); - }), - ], - ), - ), - ], - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart index a79d44c77f..4868dcafee 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -1,17 +1,67 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; -/// {@template streamVideoAttachment} -/// Shows a video attachment in a [StreamMessageWidget]. -/// {@endtemplate} +/// A video attachment component with a play indicator. +/// +/// [StreamVideoAttachment] displays a video attachment with a visual +/// indicator that it can be played. +/// +/// {@tool snippet} +/// +/// Basic usage: +/// +/// ```dart +/// StreamVideoAttachment( +/// message: message, +/// video: videoAttachment, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamVideoAttachmentProps], which configures this widget. +/// * [DefaultStreamVideoAttachment], the default implementation. class StreamVideoAttachment extends StatelessWidget { - /// {@macro streamVideoAttachment} - const StreamVideoAttachment({ + /// Creates a [StreamVideoAttachment]. + StreamVideoAttachment({ super.key, + required Message message, + required Attachment video, + BoxConstraints? constraints, + }) : props = StreamVideoAttachmentProps( + message: message, + video: video, + constraints: constraints, + ); + + /// The properties that configure this attachment. + final StreamVideoAttachmentProps props; + + @override + Widget build(BuildContext context) { + final builder = context.chatComponentBuilder(); + if (builder != null) return builder(context, props); + return DefaultStreamVideoAttachment(props: props); + } +} + +/// Properties for configuring a [StreamVideoAttachment]. +/// +/// This class holds all the configuration options for a video attachment, +/// allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamVideoAttachment], which uses these properties. +/// * [DefaultStreamVideoAttachment], the default implementation. +class StreamVideoAttachmentProps { + /// Creates properties for a video attachment. + const StreamVideoAttachmentProps({ required this.message, required this.video, - this.shape, - this.constraints = const BoxConstraints(), + this.constraints, }); /// The [Message] that the video is attached to. @@ -20,52 +70,65 @@ class StreamVideoAttachment extends StatelessWidget { /// The [Attachment] object containing the video information. final Attachment video; - /// The shape of the attachment. - /// - /// Defaults to [RoundedRectangleBorder] with a radius of 14. - final ShapeBorder? shape; - /// The constraints to use when displaying the video. - final BoxConstraints constraints; + final BoxConstraints? constraints; +} + +const _kDefaultConstraints = BoxConstraints.tightFor( + width: 256, + height: 195, +); + +/// The default implementation of [StreamVideoAttachment]. +/// +/// Renders the video thumbnail with upload progress indication. +/// +/// See also: +/// +/// * [StreamVideoAttachment], the public API widget. +/// * [StreamVideoAttachmentProps], which configures this widget. +class DefaultStreamVideoAttachment extends StatelessWidget { + /// Creates a default Stream video attachment. + const DefaultStreamVideoAttachment({ + super.key, + required this.props, + }); + + /// The properties that configure this attachment. + final StreamVideoAttachmentProps props; @override Widget build(BuildContext context) { - final chatTheme = StreamChatTheme.of(context); - final colorTheme = chatTheme.colorTheme; - final shape = this.shape ?? - RoundedRectangleBorder( - side: BorderSide( - color: colorTheme.borders, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: BorderRadius.circular(14), - ); + final constraints = props.constraints ?? _kDefaultConstraints; - return Container( + return ConstrainedBox( constraints: constraints, - clipBehavior: Clip.hardEdge, - decoration: ShapeDecoration(shape: shape), child: Stack( + fit: .expand, alignment: Alignment.center, children: [ StreamVideoAttachmentThumbnail( - video: video, - width: double.infinity, - height: double.infinity, + video: props.video, fit: BoxFit.cover, ), - const Material( - shape: CircleBorder(), - child: Padding( - padding: EdgeInsets.all(16), - child: Icon(Icons.play_arrow), + Center( + child: Material( + color: StreamColors.black75, + shape: const CircleBorder(), + child: Padding( + padding: const EdgeInsets.all(12), + child: Icon( + context.streamIcons.playFill20, + color: context.streamColorScheme.textOnAccent, + ), + ), ), ), Padding( padding: const EdgeInsets.all(8), child: StreamAttachmentUploadStateBuilder( - message: message, - attachment: video, + message: props.message, + attachment: props.video, ), ), ], diff --git a/packages/stream_chat_flutter/lib/src/attachment/voice_recording_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/voice_recording_attachment.dart index 3681d30939..e2dd8d5370 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/voice_recording_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/voice_recording_attachment.dart @@ -1,39 +1,88 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/audio/audio_playlist_state.dart'; import 'package:stream_chat_flutter/src/audio/audio_sampling.dart' as sampling; -import 'package:stream_chat_flutter/src/misc/audio_waveform.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart' hide StreamTextTheme; +import 'package:stream_core_flutter/stream_core_flutter.dart'; const _kDefaultWaveformLimit = 35; -const _kDefaultWaveformHeight = 28.0; +const _kDefaultWaveformHeight = 24.0; -/// Signature for building trailing widgets in voice recording attachments. +/// An inline audio player for a voice recording attachment. /// -/// Provides a flexible way to customize the trailing section of the -/// voice recording player based on the current track and playback state. -typedef StreamVoiceRecordingAttachmentTrailingWidgetBuilder = Widget Function( - BuildContext context, - PlaylistTrack track, - PlaybackSpeed speed, - ValueChanged? onChangeSpeed, -); - -/// {@template streamVoiceRecordingAttachment} -/// An embedded audio player for voice recordings with comprehensive playback -/// controls. +/// [StreamVoiceRecordingAttachment] displays a single voice recording with +/// playback controls, waveform visualization, and speed adjustment. /// -/// Provides a rich audio message player with features including: -/// - Play/pause controls -/// - Waveform visualization -/// - Playback speed adjustment -/// - Optional title display +/// {@tool snippet} /// -/// Supports customizable appearance and interaction through various parameters. -/// {@endtemplate} +/// Basic usage: +/// +/// ```dart +/// StreamVoiceRecordingAttachment( +/// track: track, +/// speed: playbackSpeed, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamVoiceRecordingAttachmentProps], which configures this widget. +/// * [DefaultStreamVoiceRecordingAttachment], the default implementation. class StreamVoiceRecordingAttachment extends StatelessWidget { - /// {@macro streamVoiceRecordingAttachment} - const StreamVoiceRecordingAttachment({ + /// Creates a [StreamVoiceRecordingAttachment]. + StreamVoiceRecordingAttachment({ super.key, + required PlaylistTrack track, + required StreamPlaybackSpeed speed, + VoidCallback? onTrackPause, + VoidCallback? onTrackPlay, + VoidCallback? onTrackReplay, + ValueChanged? onTrackSeekStart, + ValueChanged? onTrackSeekChanged, + ValueChanged? onTrackSeekEnd, + ValueChanged? onChangeSpeed, + BoxConstraints constraints = const BoxConstraints(), + bool showTitle = false, + String? title, + }) : props = .new( + track: track, + speed: speed, + onTrackPause: onTrackPause, + onTrackPlay: onTrackPlay, + onTrackReplay: onTrackReplay, + onTrackSeekStart: onTrackSeekStart, + onTrackSeekChanged: onTrackSeekChanged, + onTrackSeekEnd: onTrackSeekEnd, + onChangeSpeed: onChangeSpeed, + constraints: constraints, + showTitle: showTitle, + title: title, + ); + + /// The properties that configure this attachment. + final StreamVoiceRecordingAttachmentProps props; + + @override + Widget build(BuildContext context) { + final builder = context.chatComponentBuilder(); + if (builder != null) return builder(context, props); + return DefaultStreamVoiceRecordingAttachment(props: props); + } +} + +/// Properties for configuring a [StreamVoiceRecordingAttachment]. +/// +/// This class holds all the configuration options for a voice recording +/// attachment, allowing them to be passed through the +/// [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamVoiceRecordingAttachment], which uses these properties. +/// * [DefaultStreamVoiceRecordingAttachment], the default implementation. +class StreamVoiceRecordingAttachmentProps { + /// Creates properties for a voice recording attachment. + const StreamVoiceRecordingAttachmentProps({ required this.track, required this.speed, this.onTrackPause, @@ -43,17 +92,16 @@ class StreamVoiceRecordingAttachment extends StatelessWidget { this.onTrackSeekChanged, this.onTrackSeekEnd, this.onChangeSpeed, - this.shape, this.constraints = const BoxConstraints(), this.showTitle = false, - this.trailingBuilder = _defaultTrailingBuilder, + this.title, }); /// The audio track to display. final PlaylistTrack track; /// The current playback speed of the audio track. - final PlaybackSpeed speed; + final StreamPlaybackSpeed speed; /// Callback when the track is paused. final VoidCallback? onTrackPause; @@ -74,115 +122,102 @@ class StreamVoiceRecordingAttachment extends StatelessWidget { final ValueChanged? onTrackSeekEnd; /// Callback when the playback speed is changed. - final ValueChanged? onChangeSpeed; - - /// The shape of the attachment. - /// - /// Defaults to [RoundedRectangleBorder] with a radius of 14. - final ShapeBorder? shape; + final ValueChanged? onChangeSpeed; /// The constraints to use when displaying the voice recording. final BoxConstraints constraints; + /// The title of the audio message to display when [showTitle] is `true`. + /// If not provided, the [track] title will be used. + final String? title; + /// Whether to show the title of the audio message. /// /// Defaults to `false`. final bool showTitle; +} - /// The builder to use for the trailing widget. - final StreamVoiceRecordingAttachmentTrailingWidgetBuilder trailingBuilder; - - static Widget _defaultTrailingBuilder( - BuildContext context, - PlaylistTrack track, - PlaybackSpeed speed, - ValueChanged? onChangeSpeed, - ) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: switch (track.state.isPlaying) { - true => SpeedControlButton( - speed: speed, - onChangeSpeed: onChangeSpeed, - ), - false => getFileTypeImage(track.title?.mediaType?.mimeType), - }, - ); - } +/// The default implementation of [StreamVoiceRecordingAttachment]. +/// +/// Renders an inline audio player with playback controls, waveform +/// visualization, and playback speed adjustment. +/// +/// See also: +/// +/// * [StreamVoiceRecordingAttachment], the public API widget. +/// * [StreamVoiceRecordingAttachmentProps], which configures this widget. +class DefaultStreamVoiceRecordingAttachment extends StatelessWidget { + /// Creates a default Stream voice recording attachment. + const DefaultStreamVoiceRecordingAttachment({ + super.key, + required this.props, + }); + + /// The properties that configure this attachment. + final StreamVoiceRecordingAttachmentProps props; @override Widget build(BuildContext context) { + final spacing = context.streamSpacing; + final textTheme = context.streamTextTheme; + final theme = StreamVoiceRecordingAttachmentTheme.of(context); - final waveformSliderTheme = theme.audioWaveformSliderTheme; - final waveformTheme = waveformSliderTheme?.audioWaveformTheme; - - final shape = this.shape ?? - RoundedRectangleBorder( - side: BorderSide( - color: StreamChatTheme.of(context).colorTheme.borders, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: BorderRadius.circular(14), - ); - - return Container( - constraints: constraints, - clipBehavior: Clip.hardEdge, - padding: const EdgeInsets.all(8), - decoration: ShapeDecoration( - shape: shape, - color: theme.backgroundColor, - ), + final defaults = _StreamVoiceRecordingAttachmentDefaults(context); + + final isActive = props.track.state != TrackState.idle; + final isPlaying = props.track.state == TrackState.playing; + + final effectiveDurationTextStyle = theme.durationTextStyle ?? defaults.durationTextStyle; + final effectiveActiveDurationTextStyle = theme.activeDurationTextStyle ?? defaults.activeDurationTextStyle; + final effectiveSpeedToggleStyle = theme.speedToggleStyle ?? defaults.speedToggleStyle; + final effectiveControlButtonStyle = theme.controlButtonStyle ?? defaults.controlButtonStyle; + + return Padding( + padding: .all(spacing.xs), child: Row( + spacing: spacing.xs, + crossAxisAlignment: .center, children: [ AudioControlButton( - state: track.state, - onPlay: onTrackPlay, - onPause: onTrackPause, - onReplay: onTrackReplay, + state: props.track.state, + onPlay: props.onTrackPlay, + onPause: props.onTrackPause, + onReplay: props.onTrackReplay, + themeStyle: effectiveControlButtonStyle, ), - const SizedBox(width: 14), Expanded( child: Column( + spacing: spacing.xxxs, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (track.title case final title? when showTitle) ...[ + if (props.title ?? props.track.title case final title? when props.showTitle) AudioTitleText( title: title, - style: theme.titleTextStyle, + style: theme.titleTextStyle ?? textTheme.metadataEmphasis, ), - const SizedBox(height: 6), - ], Row( + spacing: spacing.sm, children: [ AudioDurationText( - duration: track.duration, - position: track.position, - style: theme.durationTextStyle, + duration: props.track.duration, + position: props.track.position, + style: isPlaying ? effectiveActiveDurationTextStyle : effectiveDurationTextStyle, ), - const SizedBox(width: 8), Expanded( child: SizedBox( height: _kDefaultWaveformHeight, child: StreamAudioWaveformSlider( + isActive: isActive, limit: _kDefaultWaveformLimit, waveform: sampling.resampleWaveformData( - track.waveform, + props.track.waveform, _kDefaultWaveformLimit, ), - progress: track.progress, - onChangeStart: onTrackSeekStart, - onChanged: onTrackSeekChanged, - onChangeEnd: onTrackSeekEnd, - color: waveformTheme?.color, - progressColor: waveformTheme?.progressColor, - minBarHeight: waveformTheme?.minBarHeight, - spacingRatio: waveformTheme?.spacingRatio, - heightScale: waveformTheme?.heightScale, - thumbColor: waveformSliderTheme?.thumbColor, - thumbBorderColor: - waveformSliderTheme?.thumbBorderColor, + progress: props.track.progress, + onChangeStart: props.onTrackSeekStart, + onChanged: props.onTrackSeekChanged, + onChangeEnd: props.onTrackSeekEnd, ), ), ), @@ -191,8 +226,11 @@ class StreamVoiceRecordingAttachment extends StatelessWidget { ], ), ), - const SizedBox(width: 14), - trailingBuilder(context, track, speed, onChangeSpeed), + StreamPlaybackSpeedToggle( + value: props.speed, + onChanged: props.onChangeSpeed, + style: effectiveSpeedToggleStyle, + ), ], ), ); @@ -284,6 +322,10 @@ class AudioControlButton extends StatelessWidget { this.onPlay, this.onPause, this.onReplay, + this.style = .secondary, + this.type = .outline, + this.size = .medium, + this.themeStyle, }); /// The current state of the audio track. @@ -298,58 +340,58 @@ class AudioControlButton extends StatelessWidget { /// Callback when the track is replayed. final VoidCallback? onReplay; + /// The style of the button. + final StreamButtonStyle style; + + /// The type of the button. + final StreamButtonType type; + + /// The size of the button. + final StreamButtonSize size; + + /// The optional style override for the button. + final StreamButtonThemeStyle? themeStyle; + @override Widget build(BuildContext context) { - final theme = StreamVoiceRecordingAttachmentTheme.of(context); + final icons = context.streamIcons; - return ElevatedButton( - style: theme.audioControlButtonStyle, - onPressed: switch (state) { + return StreamButton.icon( + style: style, + type: type, + size: size, + themeStyle: themeStyle, + icon: switch (state) { + TrackState.loading => icons.playFill20, + TrackState.idle => icons.playFill20, + TrackState.playing => icons.pauseFill20, + TrackState.paused => icons.playFill20, + }, + onTap: switch (state) { TrackState.loading => null, TrackState.idle => onPlay, TrackState.playing => onPause, TrackState.paused => onPlay, }, - child: switch (state) { - TrackState.loading => theme.loadingIndicator, - TrackState.idle => theme.playIcon, - TrackState.playing => theme.pauseIcon, - TrackState.paused => theme.playIcon, - }, ); } } -/// {@template speedControlButton} -/// A button for controlling audio playback speed. -/// -/// Allows cycling through predefined playback speeds when pressed. -/// {@endtemplate} -class SpeedControlButton extends StatelessWidget { - /// {@macro speedControlButton} - const SpeedControlButton({ - super.key, - required this.speed, - this.onChangeSpeed, - }); +// Default values for [StreamVoiceRecordingAttachmentThemeData] backed by stream design tokens. +class _StreamVoiceRecordingAttachmentDefaults extends StreamVoiceRecordingAttachmentThemeData { + _StreamVoiceRecordingAttachmentDefaults(this._context); - /// The current playback speed of the audio track. - final PlaybackSpeed speed; + final BuildContext _context; - /// Callback when the playback speed is changed. - final ValueChanged? onChangeSpeed; + late final StreamColorScheme _colorScheme = _context.streamColorScheme; + late final StreamTextTheme _textTheme = _context.streamTextTheme; @override - Widget build(BuildContext context) { - final theme = StreamVoiceRecordingAttachmentTheme.of(context); + TextStyle get titleTextStyle => _textTheme.captionEmphasis.copyWith(color: _colorScheme.textPrimary); - return ElevatedButton( - style: theme.speedControlButtonStyle, - onPressed: switch (onChangeSpeed) { - final it? => () => it(speed.next), - _ => null, - }, - child: Text('x${speed.speed}'), - ); - } + @override + TextStyle get durationTextStyle => _textTheme.metadataEmphasis.copyWith(color: _colorScheme.textPrimary); + + @override + TextStyle get activeDurationTextStyle => _textTheme.metadataEmphasis.copyWith(color: _colorScheme.accentPrimary); } 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..22395ed328 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,68 +1,123 @@ 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'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// {@template streamVoiceRecordingAttachmentPlaylist} -/// Shows a voice recording attachment in a [StreamMessageWidget]. -/// {@endtemplate} +/// Signature for decorating each voice recording item in a playlist. +/// +/// The [child] is the default [StreamVoiceRecordingAttachment] widget built +/// by the playlist. Return a widget that wraps [child] with the desired +/// container. +/// +/// See also: +/// +/// * [StreamVoiceRecordingAttachmentPlaylist.itemDecorator], which uses this +/// typedef. +typedef StreamVoiceRecordingItemDecorator = Widget Function(BuildContext context, int index, Widget child); + +/// A playlist container for multiple voice recording attachments. +/// +/// [StreamVoiceRecordingAttachmentPlaylist] manages audio playback across +/// multiple voice recordings using a shared controller, ensuring only one +/// recording plays at a time. +/// +/// {@tool snippet} +/// +/// Basic usage: +/// +/// ```dart +/// StreamVoiceRecordingAttachmentPlaylist( +/// message: message, +/// voiceRecordings: voiceRecordingAttachments, +/// ) +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// +/// With a decorator to wrap each item in a message attachment container: +/// +/// ```dart +/// StreamVoiceRecordingAttachmentPlaylist( +/// message: message, +/// voiceRecordings: voiceRecordingAttachments, +/// itemDecorator: (context, index, child) { +/// return StreamMessageAttachment(style: style, child: child); +/// }, +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamVoiceRecordingAttachment], the individual voice recording widget +/// used for each item in the playlist. class StreamVoiceRecordingAttachmentPlaylist extends StatefulWidget { - /// {@macro streamVoiceRecordingAttachmentPlaylist} + /// Creates a [StreamVoiceRecordingAttachmentPlaylist]. const StreamVoiceRecordingAttachmentPlaylist({ super.key, - this.shape, required this.message, required this.voiceRecordings, this.padding, this.itemBuilder, + this.itemDecorator, this.separatorBuilder = _defaultVoiceRecordingPlaylistSeparatorBuilder, - this.constraints = const BoxConstraints(), + this.constraints, + this.voiceRecordingTitle, }); - /// The shape of the attachment. - /// - /// Defaults to [RoundedRectangleBorder] with a radius of 14. - final ShapeBorder? shape; - - /// The [Message] that the voice recording is attached to. + /// The [Message] that the voice recordings are attached to. final Message message; - /// The list of [Attachment] object containing the voice recording + /// The list of [Attachment] objects containing the voice recording /// information. final List voiceRecordings; - /// The constraints to use when displaying the voice recording. - final BoxConstraints constraints; + /// The constraints to use when displaying each voice recording. + final BoxConstraints? constraints; /// The amount of space by which to inset the children. final EdgeInsetsGeometry? padding; /// The builder to use for each voice recording. /// - /// If not provided, a default implementation will be used. + /// If not provided, a default implementation using + /// [StreamVoiceRecordingAttachment] will be used. + /// + /// When provided, [itemDecorator] is ignored since the builder has full + /// control over the item widget. final IndexedWidgetBuilder? itemBuilder; + /// Optional decorator that wraps each default voice recording item. + /// + /// Use this to provide context-specific containers around each + /// [StreamVoiceRecordingAttachment] without replacing the default + /// item building logic. + /// + /// Ignored when [itemBuilder] is provided. + final StreamVoiceRecordingItemDecorator? itemDecorator; + /// The separator to use between the voice recordings. final IndexedWidgetBuilder separatorBuilder; + /// The title to use for each voice recording. + final String? voiceRecordingTitle; + // Default separator builder for the voice recording playlist. static Widget _defaultVoiceRecordingPlaylistSeparatorBuilder( BuildContext context, int index, ) { - return const Empty(); + final spacing = context.streamSpacing; + return SizedBox(height: spacing.xxs); } @override - State createState() => - _StreamVoiceRecordingAttachmentPlaylistState(); + State createState() => _StreamVoiceRecordingAttachmentPlaylistState(); } -class _StreamVoiceRecordingAttachmentPlaylistState - extends State { +class _StreamVoiceRecordingAttachmentPlaylistState extends State { late final _controller = StreamAudioPlaylistController( widget.voiceRecordings.toPlaylist(), ); @@ -114,12 +169,13 @@ class _StreamVoiceRecordingAttachmentPlaylistState } final track = state.tracks[index]; - return StreamVoiceRecordingAttachment( + + final child = StreamVoiceRecordingAttachment( track: track, speed: state.speed, - showTitle: true, - shape: widget.shape, - constraints: widget.constraints, + showTitle: false, + title: widget.voiceRecordingTitle, + constraints: widget.constraints ?? const BoxConstraints(), onTrackPause: _controller.pause, onChangeSpeed: _controller.setSpeed, onTrackPlay: () async { @@ -134,10 +190,6 @@ class _StreamVoiceRecordingAttachmentPlaylistState if (state.currentIndex != index) return; return _controller.pause(); }, - onTrackSeekEnd: (_) async { - if (state.currentIndex != index) return; - return _controller.play(); - }, onTrackSeekChanged: (progress) async { if (state.currentIndex != index) return; @@ -145,9 +197,15 @@ class _StreamVoiceRecordingAttachmentPlaylistState final seekPosition = (duration * progress).toInt(); final seekDuration = Duration(microseconds: seekPosition); - return _controller.seek(seekDuration); + await _controller.seek(seekDuration); }, ); + + if (widget.itemDecorator case final decorator?) { + return decorator(context, index, child); + } + + return child; }, ), ); diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal/attachment_actions_modal.dart index f11e4e127e..65fe52481b 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal/attachment_actions_modal.dart @@ -105,148 +105,149 @@ class AttachmentActionsModal extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min, - children: [ - if (showReply) - _buildButton( - context, - context.translations.replyLabel, - StreamSvgIcon( - size: 24, - icon: StreamSvgIcons.reply, - color: theme.colorTheme.textLowEmphasis, - ), - onReply, - ), - if (showShowInChat) - _buildButton( - context, - context.translations.showInChatLabel, - StreamSvgIcon( - size: 24, - icon: StreamSvgIcons.eye, - color: theme.colorTheme.textHighEmphasis, - ), - onShowMessage, - ), - if (showSave) - _buildButton( - context, - attachment.type == AttachmentType.video - ? context.translations.saveVideoLabel - : context.translations.saveImageLabel, - StreamSvgIcon( - size: 24, - icon: StreamSvgIcons.save, - color: theme.colorTheme.textLowEmphasis, - ), - () { - // Closing attachment actions modal before opening - // attachment download dialog - Navigator.of(context).pop(); - - final downloader = attachmentDownloader ?? - StreamAttachmentHandler.instance.downloadAttachment; - - // No need to show progress dialog in case of - // web or desktop. - if (isDesktopDeviceOrWeb) { - downloader(attachment); - return; - } - - final progressNotifier = - ValueNotifier<_DownloadProgress?>( - _DownloadProgress.initial(), - ); - - final downloadedPathNotifier = - ValueNotifier(null); - - downloader( - attachment, - onReceiveProgress: (received, total) { - progressNotifier.value = _DownloadProgress( - total, - received, - ); - }, - ).then((path) { - downloadedPathNotifier.value = path; - }).catchError((e, stk) { - print(e); - print(stk); - progressNotifier.value = null; - }); - - showDialog( - barrierDismissible: false, - context: context, - barrierColor: theme.colorTheme.overlay, - builder: (context) => _buildDownloadProgressDialog( - context, - progressNotifier, - downloadedPathNotifier, + children: + [ + if (showReply) + _buildButton( + context, + context.translations.replyLabel, + Icon( + context.streamIcons.reply20, + size: 24, + color: theme.colorTheme.textLowEmphasis, + ), + onReply, + ), + if (showShowInChat) + _buildButton( + context, + context.translations.showInChatLabel, + Icon( + context.streamIcons.eyeFill20, + size: 24, + color: theme.colorTheme.textHighEmphasis, + ), + onShowMessage, + ), + if (showSave) + _buildButton( + context, + attachment.type == AttachmentType.video + ? context.translations.saveVideoLabel + : context.translations.saveImageLabel, + Icon( + context.streamIcons.save20, + size: 24, + color: theme.colorTheme.textLowEmphasis, + ), + () { + // Closing attachment actions modal before opening + // attachment download dialog + Navigator.of(context).pop(); + + final downloader = + attachmentDownloader ?? StreamAttachmentHandler.instance.downloadAttachment; + + // No need to show progress dialog in case of + // web or desktop. + if (isDesktopDeviceOrWeb) { + downloader(attachment); + return; + } + + final progressNotifier = ValueNotifier<_DownloadProgress?>( + _DownloadProgress.initial(), + ); + + final downloadedPathNotifier = ValueNotifier(null); + + downloader( + attachment, + onReceiveProgress: (received, total) { + progressNotifier.value = _DownloadProgress( + total, + received, + ); + }, + ) + .then((path) { + downloadedPathNotifier.value = path; + }) + .catchError((e, stk) { + print(e); + print(stk); + progressNotifier.value = null; + }); + + showDialog( + barrierDismissible: false, + context: context, + barrierColor: theme.colorTheme.overlay, + builder: (context) => _buildDownloadProgressDialog( + context, + progressNotifier, + downloadedPathNotifier, + ), + ); + }, + ), + if (StreamChat.of(context).currentUser?.id == message.user?.id && showDelete) + _buildButton( + context, + context.translations.deleteLabel.sentenceCase, + Icon( + context.streamIcons.delete20, + size: 24, + color: theme.colorTheme.accentError, + ), + () { + final channel = StreamChannel.of(context).channel; + if (message.attachments.length > 1 || message.text?.isNotEmpty == true) { + final currentAttachmentIndex = message.attachments.indexWhere( + (element) => element.id == attachment.id, + ); + final remainingAttachments = [...message.attachments] + ..removeAt(currentAttachmentIndex); + channel.updateMessage( + message.copyWith( + attachments: remainingAttachments, + ), + ); + Navigator.of(context) + ..pop() + ..maybePop(); + } else { + channel.deleteMessage(message); + Navigator.of(context) + ..pop() + ..maybePop(); + } + }, + color: theme.colorTheme.accentError, + ), + ...customActions + .map( + (e) => _buildButton( + context, + e.actionTitle, + e.icon, + e.onTap, + ), + ) + .toList(), + ] + .map( + (e) => Align( + alignment: Alignment.centerRight, + child: e, + ), + ) + .insertBetween( + Container( + height: 1, + color: theme.colorTheme.borders, ), - ); - }, - ), - if (StreamChat.of(context).currentUser?.id == - message.user?.id && - showDelete) - _buildButton( - context, - context.translations.deleteLabel.capitalize(), - StreamSvgIcon( - size: 24, - icon: StreamSvgIcons.delete, - color: theme.colorTheme.accentError, - ), - () { - final channel = StreamChannel.of(context).channel; - if (message.attachments.length > 1 || - message.text?.isNotEmpty == true) { - final currentAttachmentIndex = - message.attachments.indexWhere( - (element) => element.id == attachment.id, - ); - final remainingAttachments = [...message.attachments] - ..removeAt(currentAttachmentIndex); - channel.updateMessage(message.copyWith( - attachments: remainingAttachments, - )); - Navigator.of(context) - ..pop() - ..maybePop(); - } else { - channel.deleteMessage(message); - Navigator.of(context) - ..pop() - ..maybePop(); - } - }, - color: theme.colorTheme.accentError, - ), - ...customActions - .map( - (e) => _buildButton( - context, - e.actionTitle, - e.icon, - e.onTap, ), - ) - .toList(), - ] - .map((e) => Align( - alignment: Alignment.centerRight, - child: e, - )) - .insertBetween( - Container( - height: 1, - color: theme.colorTheme.borders, - ), - ), ), ), ), @@ -276,10 +277,7 @@ class AttachmentActionsModal extends StatelessWidget { const SizedBox(width: 16), Text( title, - style: StreamChatTheme.of(context) - .textTheme - .body - .copyWith(color: color), + style: StreamChatTheme.of(context).textTheme.body.copyWith(color: color), ), ], ), @@ -330,46 +328,44 @@ class AttachmentActionsModal extends StatelessWidget { ? SizedBox( height: 100, width: 100, - child: StreamSvgIcon( - icon: StreamSvgIcons.error, + child: Icon( + context.streamIcons.exclamationCircleFill20, color: theme.colorTheme.disabled, ), ) : _downloadComplete - ? SizedBox( - key: const Key('completedIcon'), - height: 160, - width: 160, - child: StreamSvgIcon( - icon: StreamSvgIcons.check, - color: theme.colorTheme.disabled, + ? SizedBox( + key: const Key('completedIcon'), + height: 160, + width: 160, + child: Icon( + context.streamIcons.checkmark20, + color: theme.colorTheme.disabled, + ), + ) + : SizedBox( + height: 100, + width: 100, + child: Stack( + fit: StackFit.expand, + children: [ + CircularProgressIndicator.adaptive( + strokeWidth: 8, + valueColor: AlwaysStoppedAnimation( + theme.colorTheme.accentPrimary, + ), ), - ) - : SizedBox( - height: 100, - width: 100, - child: Stack( - fit: StackFit.expand, - children: [ - CircularProgressIndicator.adaptive( - strokeWidth: 8, - valueColor: AlwaysStoppedAnimation( - theme.colorTheme.accentPrimary, - ), - ), - Center( - child: Text( - '${progress.receivedValueInMB} MB', - style: - theme.textTheme.headline.copyWith( - color: - theme.colorTheme.textLowEmphasis, - ), - ), + Center( + child: Text( + '${progress.receivedValueInMB} MB', + style: theme.textTheme.headline.copyWith( + color: theme.colorTheme.textLowEmphasis, ), - ], + ), ), - ), + ], + ), + ), ), ), ), @@ -384,8 +380,7 @@ class AttachmentActionsModal extends StatelessWidget { class _DownloadProgress { const _DownloadProgress(this.total, this.received); - factory _DownloadProgress.initial() => - _DownloadProgress(double.maxFinite.toInt(), 0); + factory _DownloadProgress.initial() => _DownloadProgress(double.maxFinite.toInt(), 0); final int total; final int received; diff --git a/packages/stream_chat_flutter/lib/src/audio/audio_playlist_controller.dart b/packages/stream_chat_flutter/lib/src/audio/audio_playlist_controller.dart index 013aadde1e..c481c5386d 100644 --- a/packages/stream_chat_flutter/lib/src/audio/audio_playlist_controller.dart +++ b/packages/stream_chat_flutter/lib/src/audio/audio_playlist_controller.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:just_audio/just_audio.dart'; import 'package:stream_chat_flutter/src/audio/audio_playlist_state.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template streamAudioPlaylistController} /// A controller for managing an audio playlist. @@ -21,8 +22,8 @@ class StreamAudioPlaylistController extends ValueNotifier { StreamAudioPlaylistController.raw({ AudioPlayer? player, AudioPlaylistState state = const AudioPlaylistState(tracks: []), - }) : _player = player ?? AudioPlayer(), - super(state); + }) : _player = player ?? AudioPlayer(), + super(state); final AudioPlayer _player; @@ -45,21 +46,22 @@ class StreamAudioPlaylistController extends ValueNotifier { final tracks = [ ...value.tracks.mapIndexed((index, track) { final trackState = switch (index == currentIndex) { - true => state.playing - ? TrackState.playing - : switch (state.processingState) { - ProcessingState.idle => TrackState.idle, - ProcessingState.loading => TrackState.loading, - _ => TrackState.paused, - }, + true => + state.playing + ? TrackState.playing + : switch (state.processingState) { + ProcessingState.idle => TrackState.idle, + ProcessingState.loading => TrackState.loading, + _ => TrackState.paused, + }, false => switch (track.state) { - TrackState.idle => TrackState.idle, - _ => TrackState.paused, - }, + TrackState.idle => TrackState.idle, + _ => TrackState.paused, + }, }; return track.copyWith(state: trackState); - }) + }), ]; value = value.copyWith(tracks: tracks); @@ -74,7 +76,7 @@ class StreamAudioPlaylistController extends ValueNotifier { ...value.tracks.mapIndexed((index, track) { if (index != currentIndex) return track; return track.copyWith(position: position); - }) + }), ]; value = value.copyWith(tracks: tracks); @@ -82,7 +84,11 @@ class StreamAudioPlaylistController extends ValueNotifier { // Listen to speed changes _speedSubscription = _player.speedStream.listen((speed) { - value = value.copyWith(speed: PlaybackSpeed.fromValue(speed)); + final playbackSpeed = StreamPlaybackSpeed.values.firstWhere( + (e) => e.speed == speed, + orElse: () => StreamPlaybackSpeed.x1, + ); + value = value.copyWith(speed: playbackSpeed); }); } @@ -116,7 +122,7 @@ class StreamAudioPlaylistController extends ValueNotifier { Future stop() => _player.stop(); /// Sets the speed of the current track. - Future setSpeed(PlaybackSpeed speed) => _player.setSpeed(speed.speed); + Future setSpeed(StreamPlaybackSpeed speed) => _player.setSpeed(speed.speed); /// Seeks to the given position in the current track. Future seek(Duration position) => _player.seek(position); diff --git a/packages/stream_chat_flutter/lib/src/audio/audio_playlist_state.dart b/packages/stream_chat_flutter/lib/src/audio/audio_playlist_state.dart index 36a3078037..8685dec59b 100644 --- a/packages/stream_chat_flutter/lib/src/audio/audio_playlist_state.dart +++ b/packages/stream_chat_flutter/lib/src/audio/audio_playlist_state.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template playlistLoopMode} /// Represents the loop mode of a playlist. @@ -24,7 +25,7 @@ class AudioPlaylistState { const AudioPlaylistState({ required this.tracks, this.currentIndex, - this.speed = PlaybackSpeed.regular, + this.speed = StreamPlaybackSpeed.x1, this.loopMode = PlaylistLoopMode.off, }); @@ -37,7 +38,7 @@ class AudioPlaylistState { final int? currentIndex; /// The current playback speed of the playlist. - final PlaybackSpeed speed; + final StreamPlaybackSpeed speed; /// The current loop mode of the playlist. final PlaylistLoopMode loopMode; @@ -47,7 +48,7 @@ class AudioPlaylistState { AudioPlaylistState copyWith({ List? tracks, int? currentIndex, - PlaybackSpeed? speed, + StreamPlaybackSpeed? speed, PlaylistLoopMode? loopMode, }) { return AudioPlaylistState( @@ -86,7 +87,8 @@ enum TrackState { playing, /// The track is currently paused. - paused; + paused + ; /// Returns `true` if the track is currently idle. bool get isIdle => this == TrackState.idle; @@ -101,45 +103,6 @@ enum TrackState { bool get isPaused => this == TrackState.paused; } -/// {@template playbackSpeed} -/// Represents the speed of a track. -/// {@endtemplate} -enum PlaybackSpeed { - /// The regular speed of the playback (1x). - regular._(1), - - /// A faster speed of the playback (1.5x). - faster._(1.5), - - /// The fastest speed of the playback (2x). - fastest._(2); - - const PlaybackSpeed._(this.speed); - - /// Creates a [PlaybackSpeed] from the given value. - factory PlaybackSpeed.fromValue(double speed) { - return PlaybackSpeed.values.firstWhere( - (it) => it.speed == speed, - orElse: () => PlaybackSpeed.regular, - ); - } - - /// The speed of the playback. - final double speed; -} - -/// Helper extension for [PlaybackSpeed]. -extension StreamAudioPlayerExtension on PlaybackSpeed { - /// Returns the next [PlaybackSpeed] value. - PlaybackSpeed get next { - return switch (this) { - PlaybackSpeed.regular => PlaybackSpeed.faster, - PlaybackSpeed.faster => PlaybackSpeed.fastest, - PlaybackSpeed.fastest => PlaybackSpeed.regular, - }; - } -} - /// {@template playlistTrack} /// Represents a track in a playlist. /// {@endtemplate} @@ -147,6 +110,7 @@ class PlaylistTrack { /// {@macro playlistTrack} const PlaylistTrack({ required this.uri, + this.key, this.title, this.waveform = const [], this.duration = Duration.zero, @@ -154,6 +118,9 @@ class PlaylistTrack { this.state = TrackState.idle, }); + /// The key to identify the track. + final Object? key; + /// The uri of the track. final Uri uri; @@ -200,6 +167,7 @@ class PlaylistTrack { TrackState? state, }) { return PlaylistTrack( + key: key, uri: uri ?? this.uri, title: title ?? this.title, duration: duration ?? this.duration, diff --git a/packages/stream_chat_flutter/lib/src/audio/audio_sampling.dart b/packages/stream_chat_flutter/lib/src/audio/audio_sampling.dart index 0381d273eb..943032510d 100644 --- a/packages/stream_chat_flutter/lib/src/audio/audio_sampling.dart +++ b/packages/stream_chat_flutter/lib/src/audio/audio_sampling.dart @@ -36,23 +36,21 @@ List downSample(List data, int targetOutputSize) { final previousBucketRefPoint = data[lastSelectedPointIndex]; final nextBucketMean = _getNextBucketMean(data, bucketIndex, bucketSize); - final currentBucketStartIndex = - ((bucketIndex - 1) * bucketSize).floor() + 1; + final currentBucketStartIndex = ((bucketIndex - 1) * bucketSize).floor() + 1; final nextBucketStartIndex = (bucketIndex * bucketSize).floor() + 1; - final countUnitsBetweenAtoC = - 1 + nextBucketStartIndex - currentBucketStartIndex; + final countUnitsBetweenAtoC = 1 + nextBucketStartIndex - currentBucketStartIndex; var maxArea = -1.0; var triangleArea = -1.0; double? maxAreaPoint; - for (var currentPointIndex = currentBucketStartIndex; - currentPointIndex < nextBucketStartIndex; - currentPointIndex++) { - final countUnitsBetweenAtoB = - (currentPointIndex - currentBucketStartIndex).abs() + 1; - final countUnitsBetweenBtoC = - countUnitsBetweenAtoC - countUnitsBetweenAtoB; + for ( + var currentPointIndex = currentBucketStartIndex; + currentPointIndex < nextBucketStartIndex; + currentPointIndex++ + ) { + final countUnitsBetweenAtoB = (currentPointIndex - currentBucketStartIndex).abs() + 1; + final countUnitsBetweenBtoC = countUnitsBetweenAtoC - countUnitsBetweenAtoB; final currentPointValue = data[currentPointIndex]; triangleArea = _triangleAreaHeron( @@ -107,11 +105,8 @@ double _getNextBucketMean( double bucketSize, ) { final nextBucketStartIndex = (currentBucketIndex * bucketSize).floor() + 1; - var nextNextBucketStartIndex = - ((currentBucketIndex + 1) * bucketSize).floor() + 1; - nextNextBucketStartIndex = nextNextBucketStartIndex < data.length - ? nextNextBucketStartIndex - : data.length; + var nextNextBucketStartIndex = ((currentBucketIndex + 1) * bucketSize).floor() + 1; + nextNextBucketStartIndex = nextNextBucketStartIndex < data.length ? nextNextBucketStartIndex : data.length; return _mean(data.sublist(nextBucketStartIndex, nextNextBucketStartIndex)); } diff --git a/packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart b/packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart index 98487690c2..c452b10e9c 100644 --- a/packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart +++ b/packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart @@ -19,7 +19,8 @@ enum OptionsAlignment { /// The options are displayed above the field. /// /// This is the default. - above; + above + ; Anchor _toAnchor() { switch (this) { @@ -45,11 +46,12 @@ enum OptionsAlignment { /// See also: /// /// * [StreamAutocomplete.fieldViewBuilder], which is of this type. -typedef StreamAutocompleteFieldViewBuilder = Widget Function( - BuildContext context, - StreamMessageEditingController messageEditingController, - FocusNode focusNode, -); +typedef StreamAutocompleteFieldViewBuilder = + Widget Function( + BuildContext context, + StreamMessageEditingController messageEditingController, + FocusNode focusNode, + ); /// The type of the [StreamAutocompleteTrigger] callback which returns a /// [Widget] that displays the specified [options]. @@ -57,11 +59,12 @@ typedef StreamAutocompleteFieldViewBuilder = Widget Function( /// See also: /// /// * [StreamAutocompleteTrigger.optionsViewBuilder], which is of this type. -typedef StreamAutocompleteOptionsViewBuilder = Widget Function( - BuildContext context, - StreamAutocompleteQuery autocompleteQuery, - StreamMessageEditingController messageEditingController, -); +typedef StreamAutocompleteOptionsViewBuilder = + Widget Function( + BuildContext context, + StreamAutocompleteQuery autocompleteQuery, + StreamMessageEditingController messageEditingController, + ); /// The query to determine the autocomplete options. class StreamAutocompleteQuery { @@ -148,8 +151,7 @@ class StreamAutocompleteTrigger { final cursorPosition = textEditingValue.selection.baseOffset; // Find the first [trigger] location before the input cursor. - final firstTriggerIndexBeforeCursor = - text.substring(0, cursorPosition).lastIndexOf(trigger); + final firstTriggerIndexBeforeCursor = text.substring(0, cursorPosition).lastIndexOf(trigger); // If the [trigger] is not found before the cursor, then it's not a trigger. if (firstTriggerIndexBeforeCursor == -1) return null; @@ -164,9 +166,7 @@ class StreamAutocompleteTrigger { // valid examples: "@user", "Hello @user" // invalid examples: "Hello@user" final textBeforeTrigger = text.substring(0, firstTriggerIndexBeforeCursor); - if (triggerOnlyAfterSpace && - textBeforeTrigger.isNotEmpty && - !textBeforeTrigger.endsWith(' ')) { + if (triggerOnlyAfterSpace && textBeforeTrigger.isNotEmpty && !textBeforeTrigger.endsWith(' ')) { return null; } @@ -287,10 +287,7 @@ class _StreamAutocompleteState extends State { // True if the state indicates that the options should be visible. bool get _shouldShowOptions { - return !_hideOptions && - _focusNode.hasFocus && - _currentQuery != null && - _currentTrigger != null; + return !_hideOptions && _focusNode.hasFocus && _currentQuery != null && _currentTrigger != null; } /// Accepts and replaces the current query with the given [option] and closes @@ -467,8 +464,7 @@ class _StreamAutocompleteState extends State { @override void initState() { super.initState(); - _messageEditingController = - widget.messageEditingController ?? StreamMessageEditingController(); + _messageEditingController = widget.messageEditingController ?? StreamMessageEditingController(); _messageEditingController.addListener(_onChangedField); _focusNode = widget.focusNode ?? FocusNode(); _focusNode.addListener(_onChangedFocus); @@ -495,7 +491,8 @@ class _StreamAutocompleteState extends State { _focusNode.dispose(); } _onChangedField.cancel(); - closeSuggestions(); + _currentQuery = null; + _currentTrigger = null; super.dispose(); } @@ -555,6 +552,72 @@ const _kDefaultStreamAutocompleteOptionsShape = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(8)), ); +/// Defines the visual style of autocomplete options overlay. +enum AutocompleteOptionsStyle { + /// Flat overlay with no elevation or margin. + /// + /// Used for overlays that appear directly above the composer (default). + fixed, + + /// Floating card with elevation and rounded corners. + /// + /// Used for overlays that appear in open space away from the composer. + floating, +} + +/// Resolves visual parameters for a [StreamAutocompleteOptions] widget based +/// on [AutocompleteOptionsStyle]. +extension AutocompleteOptionsStyleX on AutocompleteOptionsStyle { + /// Returns the elevation, margin, and shape for [StreamAutocompleteOptions]. + /// + /// [borderColor] is used for the top border (fixed) or outline (floating). + ({double elevation, EdgeInsetsGeometry margin, ShapeBorder shape}) resolve( + Color borderColor, + ) { + return switch (this) { + AutocompleteOptionsStyle.fixed => ( + elevation: 0.0, + margin: EdgeInsets.zero, + shape: _TopBorderShape(BorderSide(color: borderColor)), + ), + AutocompleteOptionsStyle.floating => ( + elevation: 4.0, + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(24)), + side: BorderSide(color: borderColor), + ), + ), + }; + } +} + +/// A [ShapeBorder] that paints only a top border, with no rounding or sides. +class _TopBorderShape extends ShapeBorder { + const _TopBorderShape(this.top); + + final BorderSide top; + + @override + EdgeInsetsGeometry get dimensions => EdgeInsets.only(top: top.width); + + @override + Path getInnerPath(Rect rect, {TextDirection? textDirection}) => Path()..addRect(rect); + + @override + Path getOuterPath(Rect rect, {TextDirection? textDirection}) => Path()..addRect(rect); + + @override + void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { + final paint = top.toPaint()..strokeCap = StrokeCap.square; + final y = rect.top + top.width / 2; + canvas.drawLine(Offset(rect.left, y), Offset(rect.right, y), paint); + } + + @override + ShapeBorder scale(double t) => _TopBorderShape(top.scale(t)); +} + /// A helper widget used to show the options of a [StreamAutocomplete]. class StreamAutocompleteOptions extends StatelessWidget { /// Creates a [StreamAutocompleteOptions] widget. @@ -621,10 +684,7 @@ class StreamAutocompleteOptions extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (headerBuilder != null) ...[ - headerBuilder!(context), - Divider(height: 0, color: colorTheme.borders), - ], + if (headerBuilder != null) headerBuilder!(context), LimitedBox( maxHeight: maxHeight ?? height * 0.5, child: ListView.builder( diff --git a/packages/stream_chat_flutter/lib/src/autocomplete/stream_command_autocomplete_options.dart b/packages/stream_chat_flutter/lib/src/autocomplete/stream_command_autocomplete_options.dart index fddd4abc72..dd92cdb9bf 100644 --- a/packages/stream_chat_flutter/lib/src/autocomplete/stream_command_autocomplete_options.dart +++ b/packages/stream_chat_flutter/lib/src/autocomplete/stream_command_autocomplete_options.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_picker/options/stream_command_icon.dart'; import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; @@ -12,6 +13,7 @@ class StreamCommandAutocompleteOptions extends StatelessWidget { required this.query, required this.channel, this.onCommandSelected, + this.style = AutocompleteOptionsStyle.fixed, super.key, }); @@ -24,6 +26,11 @@ class StreamCommandAutocompleteOptions extends StatelessWidget { /// Callback called when a command is selected. final ValueSetter? onCommandSelected; + /// The visual style of the autocomplete options overlay. + /// + /// Defaults to [AutocompleteOptionsStyle.fixed]. + final AutocompleteOptionsStyle style; + @override Widget build(BuildContext context) { final commands = channel.config?.commands.where((it) { @@ -34,25 +41,29 @@ class StreamCommandAutocompleteOptions extends StatelessWidget { if (commands == null || commands.isEmpty) return const Empty(); - final streamChatTheme = StreamChatTheme.of(context); - final colorTheme = streamChatTheme.colorTheme; - final textTheme = streamChatTheme.textTheme; + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + + final (:elevation, :margin, :shape) = style.resolve(colorScheme.borderDefault); return StreamAutocompleteOptions( options: commands, + elevation: elevation, + margin: margin, + shape: shape, headerBuilder: (context) { - return ListTile( - dense: true, - horizontalTitleGap: 0, - leading: StreamSvgIcon( - icon: StreamSvgIcons.lightning, - color: colorTheme.accentPrimary, - size: 28, + return Padding( + padding: EdgeInsets.only( + left: context.streamSpacing.sm, + right: context.streamSpacing.sm, + top: context.streamSpacing.md, + bottom: context.streamSpacing.xs, ), - title: Text( - context.translations.instantCommandsLabel, - style: textTheme.body.copyWith( - color: colorTheme.textLowEmphasis, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text( + context.translations.instantCommandsLabel, + style: textTheme.headingXs.copyWith(color: colorScheme.textTertiary), ), ), ); @@ -60,122 +71,28 @@ class StreamCommandAutocompleteOptions extends StatelessWidget { optionBuilder: (context, command) { return ListTile( dense: true, - horizontalTitleGap: 8, - leading: _CommandIcon(command: command), - title: Row( + horizontalTitleGap: context.streamSpacing.sm, + leading: StreamCommandIcon(command: command), + title: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - command.name.capitalize(), - style: textTheme.bodyBold.copyWith( - color: colorTheme.textHighEmphasis, - ), + command.name.sentenceCase, + style: textTheme.bodyDefault, ), - const SizedBox(width: 8), + SizedBox(height: context.streamSpacing.xxs), Text( - '/${command.name} ${command.args}', - style: textTheme.body.copyWith( - color: colorTheme.textLowEmphasis, + command.description, + style: textTheme.captionDefault.copyWith( + color: colorScheme.textTertiary, ), ), ], ), - onTap: onCommandSelected == null - ? null - : () => onCommandSelected!(command), + onTap: onCommandSelected == null ? null : () => onCommandSelected!(command), ); }, ); } } - -class _CommandIcon extends StatelessWidget { - const _CommandIcon({required this.command}); - - final Command command; - - @override - Widget build(BuildContext context) { - final _streamChatTheme = StreamChatTheme.of(context); - switch (command.name) { - case 'giphy': - return const CircleAvatar( - radius: 12, - child: StreamSvgIcon( - size: 24, - icon: StreamSvgIcons.giphy, - ), - ); - case 'ban': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: const StreamSvgIcon( - size: 16, - color: Colors.white, - icon: StreamSvgIcons.userRemove, - ), - ); - case 'flag': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: const StreamSvgIcon( - size: 14, - color: Colors.white, - icon: StreamSvgIcons.flag, - ), - ); - case 'imgur': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: const ClipOval( - child: StreamSvgIcon( - size: 24, - icon: StreamSvgIcons.imgur, - ), - ), - ); - case 'mute': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: const StreamSvgIcon( - size: 16, - color: Colors.white, - icon: StreamSvgIcons.mute, - ), - ); - case 'unban': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: const StreamSvgIcon( - size: 16, - color: Colors.white, - icon: StreamSvgIcons.userAdd, - ), - ); - case 'unmute': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: const StreamSvgIcon( - size: 16, - color: Colors.white, - icon: StreamSvgIcons.volumeUp, - ), - ); - default: - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: const StreamSvgIcon( - size: 16, - color: Colors.white, - icon: StreamSvgIcons.lightning, - ), - ); - } - } -} diff --git a/packages/stream_chat_flutter/lib/src/autocomplete/stream_mention_autocomplete_options.dart b/packages/stream_chat_flutter/lib/src/autocomplete/stream_mention_autocomplete_options.dart index 955e02ae34..5fa238458a 100644 --- a/packages/stream_chat_flutter/lib/src/autocomplete/stream_mention_autocomplete_options.dart +++ b/packages/stream_chat_flutter/lib/src/autocomplete/stream_mention_autocomplete_options.dart @@ -16,14 +16,15 @@ class StreamMentionAutocompleteOptions extends StatefulWidget { this.mentionAllAppUsers = false, this.mentionsTileBuilder, this.onMentionUserTap, - }) : assert( - channel.state != null, - 'Channel ${channel.cid} is not yet initialized', - ), - assert( - !mentionAllAppUsers || (mentionAllAppUsers && client != null), - 'StreamChatClient is required in order to use mentionAllAppUsers', - ); + this.style = AutocompleteOptionsStyle.fixed, + }) : assert( + channel.state != null, + 'Channel ${channel.cid} is not yet initialized', + ), + assert( + !mentionAllAppUsers || (mentionAllAppUsers && client != null), + 'StreamChatClient is required in order to use mentionAllAppUsers', + ); /// Query for searching users. final String query; @@ -48,13 +49,16 @@ class StreamMentionAutocompleteOptions extends StatefulWidget { /// Callback called when a user is selected. final ValueSetter? onMentionUserTap; + /// The visual style of the autocomplete options overlay. + /// + /// Defaults to [AutocompleteOptionsStyle.fixed]. + final AutocompleteOptionsStyle style; + @override - _StreamMentionAutocompleteOptionsState createState() => - _StreamMentionAutocompleteOptionsState(); + _StreamMentionAutocompleteOptionsState createState() => _StreamMentionAutocompleteOptionsState(); } -class _StreamMentionAutocompleteOptionsState - extends State { +class _StreamMentionAutocompleteOptionsState extends State { late Future> userMentionsFuture; @override @@ -83,18 +87,48 @@ class _StreamMentionAutocompleteOptionsState if (!snapshot.hasData) return const Empty(); final users = snapshot.data!; + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; + final (:elevation, :margin, :shape) = widget.style.resolve(colorScheme.borderDefault); + return StreamAutocompleteOptions( options: users, + elevation: elevation, + margin: margin, + shape: shape, optionBuilder: (context, user) { - final colorTheme = StreamChatTheme.of(context).colorTheme; - return Material( - color: colorTheme.barsBg, + final mentionsTileBuilder = widget.mentionsTileBuilder; + if (mentionsTileBuilder != null) { + final colorTheme = StreamChatTheme.of(context).colorTheme; + return Material( + color: colorTheme.barsBg, + child: InkWell( + onTap: widget.onMentionUserTap == null ? null : () => widget.onMentionUserTap!(user), + child: mentionsTileBuilder(context, user), + ), + ); + } + + return Padding( + padding: EdgeInsets.symmetric(horizontal: spacing.xxs), child: InkWell( - onTap: widget.onMentionUserTap == null - ? null - : () => widget.onMentionUserTap!(user), - child: widget.mentionsTileBuilder?.call(context, user) ?? - StreamUserMentionTile(user), + borderRadius: BorderRadius.circular(12), + onTap: widget.onMentionUserTap == null ? null : () => widget.onMentionUserTap!(user), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: spacing.sm, vertical: spacing.xs), + child: Row( + spacing: spacing.sm, + children: [ + StreamUserAvatar(size: .md, user: user), + Text( + user.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.streamTextTheme.bodyDefault, + ), + ], + ), + ), ), ); }, @@ -131,18 +165,13 @@ class _StreamMentionAutocompleteOptionsState } final result = await _queryMembers(query); - return result - .map((it) => it.user) - .whereType() - .toList(growable: false); + return result.map((it) => it.user).whereType().toList(growable: false); } Future> _queryMembers(String query) async { final response = await widget.channel.queryMembers( pagination: PaginationParams(limit: widget.limit), - filter: query.isEmpty - ? const Filter.empty() - : Filter.autoComplete('name', query), + filter: query.isEmpty ? const Filter.empty() : Filter.autoComplete('name', query), ); return response.members; } diff --git a/packages/stream_chat_flutter/lib/src/avatars/group_avatar.dart b/packages/stream_chat_flutter/lib/src/avatars/group_avatar.dart deleted file mode 100644 index 146a664318..0000000000 --- a/packages/stream_chat_flutter/lib/src/avatars/group_avatar.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// WidgetBuilder for [StreamGroupAvatar]. -typedef StreamGroupAvatarBuilder = Widget Function( - BuildContext context, - List members, - // ignore: avoid_positional_boolean_parameters - bool isSelected, -); - -/// {@template streamGroupAvatar} -/// Widget for constructing a group of images -/// {@endtemplate} -class StreamGroupAvatar extends StatelessWidget { - /// {@macro streamGroupAvatar} - const StreamGroupAvatar({ - super.key, - this.channel, - required this.members, - this.constraints, - this.onTap, - this.borderRadius, - this.selected = false, - this.selectionColor, - this.selectionThickness = 4, - }); - - /// The channel of the avatar - final Channel? channel; - - /// The list of members in the group whose avatars should be displayed. - final List members; - - /// Constraints on the widget - final BoxConstraints? constraints; - - /// The action to perform when the widget is tapped - final VoidCallback? onTap; - - /// If `true`, this widget should be highlighted. - /// - /// Defaults to `false`. - final bool selected; - - /// [BorderRadius] to pass to the widget - final BorderRadius? borderRadius; - - /// The color to highlight the widget with if [selected] is `true` - final Color? selectionColor; - - /// The value to use for the border thickness and padding of the - /// selected image - final double selectionThickness; - - @override - Widget build(BuildContext context) { - final channel = this.channel ?? StreamChannel.of(context).channel; - - assert(channel.state != null, 'Channel ${channel.id} is not initialized'); - - final streamChatTheme = StreamChatTheme.of(context); - final colorTheme = streamChatTheme.colorTheme; - final previewTheme = streamChatTheme.channelPreviewTheme.avatarTheme; - - Widget avatar = GestureDetector( - onTap: onTap, - child: ClipRRect( - borderRadius: - borderRadius ?? previewTheme?.borderRadius ?? BorderRadius.zero, - child: Container( - constraints: constraints ?? previewTheme?.constraints, - decoration: BoxDecoration(color: colorTheme.accentPrimary), - child: Flex( - direction: Axis.vertical, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Flexible( - fit: FlexFit.tight, - child: Flex( - direction: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: members - .take(2) - .map( - (member) => Flexible( - fit: FlexFit.tight, - child: FittedBox( - fit: BoxFit.cover, - clipBehavior: Clip.antiAlias, - child: Transform.scale( - scale: 1.2, - child: BetterStreamBuilder( - stream: channel.state!.membersStream.map( - (members) => members.firstWhere( - (it) => it.userId == member.userId, - orElse: () => member, - ), - ), - initialData: member, - builder: (context, member) => StreamUserAvatar( - showOnlineStatus: false, - user: member.user!, - borderRadius: BorderRadius.zero, - ), - ), - ), - ), - ), - ) - .toList(), - ), - ), - if (members.length > 2) - Flexible( - fit: FlexFit.tight, - child: Flex( - direction: Axis.horizontal, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: members - .skip(2) - .take(2) - .map( - (member) => Flexible( - fit: FlexFit.tight, - child: FittedBox( - fit: BoxFit.cover, - clipBehavior: Clip.antiAlias, - child: Transform.scale( - scale: 1.2, - child: BetterStreamBuilder( - stream: channel.state!.membersStream.map( - (members) => members.firstWhere( - (it) => it.userId == member.userId, - orElse: () => member, - ), - ), - initialData: member, - builder: (context, member) => - StreamUserAvatar( - showOnlineStatus: false, - user: member.user!, - borderRadius: BorderRadius.zero, - ), - ), - ), - ), - ), - ) - .toList(), - ), - ), - ], - ), - ), - ), - ); - - if (selected) { - avatar = ClipRRect( - borderRadius: BorderRadius.circular(selectionThickness) + - (borderRadius ?? previewTheme?.borderRadius ?? BorderRadius.zero), - child: Container( - constraints: constraints ?? previewTheme?.constraints, - color: selectionColor ?? colorTheme.accentPrimary, - child: Padding( - padding: EdgeInsets.all(selectionThickness), - child: avatar, - ), - ), - ); - } - - return avatar; - } -} diff --git a/packages/stream_chat_flutter/lib/src/avatars/user_avatar.dart b/packages/stream_chat_flutter/lib/src/avatars/user_avatar.dart deleted file mode 100644 index f62e40bcff..0000000000 --- a/packages/stream_chat_flutter/lib/src/avatars/user_avatar.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// WidgetBuilder for [StreamUserAvatar]. -typedef StreamUserAvatarBuilder = Widget Function( - BuildContext context, - User user, - // ignore: avoid_positional_boolean_parameters - bool isSelected, -); - -/// {@template streamUserAvatar} -/// Displays a user's avatar. -/// {@endtemplate} -class StreamUserAvatar extends StatelessWidget { - /// {@macro streamUserAvatar} - const StreamUserAvatar({ - super.key, - required this.user, - this.constraints, - this.onlineIndicatorConstraints, - this.onTap, - this.onLongPress, - this.showOnlineStatus = true, - this.borderRadius, - this.onlineIndicatorAlignment = Alignment.topRight, - this.selected = false, - this.selectionColor, - this.selectionThickness = 4, - this.placeholder, - }); - - /// User whose avatar is to be displayed - final User user; - - /// Alignment of the online indicator - /// - /// Defaults to `Alignment.topRight` - final Alignment onlineIndicatorAlignment; - - /// Sizing constraints of the avatar - final BoxConstraints? constraints; - - /// [BorderRadius] of the image - final BorderRadius? borderRadius; - - /// Sizing constraints of the online indicator - final BoxConstraints? onlineIndicatorConstraints; - - /// {@macro onUserAvatarTap} - final OnUserAvatarPress? onTap; - - /// {@macro onUserAvatarTap} - final OnUserAvatarPress? onLongPress; - - /// Flag for showing online status - /// - /// Defaults to `true` - final bool showOnlineStatus; - - /// Flag for if avatar is selected - /// - /// Defaults to `false` - final bool selected; - - /// Color of selection - final Color? selectionColor; - - /// Selection thickness around the avatar - /// - /// Defaults to `4` - final double selectionThickness; - - /// {@macro placeholderUserImage} - final PlaceholderUserImage? placeholder; - - @override - Widget build(BuildContext context) { - final streamChatTheme = StreamChatTheme.of(context); - final colorTheme = streamChatTheme.colorTheme; - final avatarTheme = streamChatTheme.ownMessageTheme.avatarTheme; - final streamChatConfig = StreamChatConfiguration.of(context); - - final effectivePlaceholder = switch (placeholder) { - final placeholder? => placeholder, - _ => streamChatConfig.placeholderUserImage, - }; - - final effectiveBorderRadius = borderRadius ?? avatarTheme?.borderRadius; - - final backupGradientAvatar = ClipRRect( - borderRadius: effectiveBorderRadius ?? BorderRadius.zero, - child: streamChatConfig.defaultUserImage(context, user), - ); - - Widget avatar = FittedBox( - fit: BoxFit.cover, - child: Container( - constraints: constraints ?? avatarTheme?.constraints, - child: LayoutBuilder( - builder: (context, constraints) { - final imageUrl = user.image; - if (imageUrl == null || imageUrl.isEmpty) { - return backupGradientAvatar; - } - - // Calculate optimal thumbnail size for the avatar - final devicePixelRatio = MediaQuery.devicePixelRatioOf(context); - final thumbnailSize = constraints.biggest * devicePixelRatio; - - int? cacheWidth, cacheHeight; - if (thumbnailSize.isFinite && !thumbnailSize.isEmpty) { - cacheWidth = thumbnailSize.width.round(); - cacheHeight = thumbnailSize.height.round(); - } - - return CachedNetworkImage( - fit: BoxFit.cover, - filterQuality: FilterQuality.high, - imageUrl: imageUrl, - errorWidget: (_, __, ___) => backupGradientAvatar, - placeholder: switch (effectivePlaceholder) { - final holder? => (context, __) => holder(context, user), - _ => null, - }, - imageBuilder: (context, imageProvider) => DecoratedBox( - decoration: BoxDecoration( - borderRadius: effectiveBorderRadius, - image: DecorationImage( - fit: BoxFit.cover, - image: ResizeImage( - imageProvider, - width: cacheWidth, - height: cacheHeight, - ), - ), - ), - ), - ); - }, - ), - ), - ); - - if (selected) { - avatar = ClipRRect( - borderRadius: (effectiveBorderRadius ?? BorderRadius.zero) + - BorderRadius.circular(selectionThickness), - child: Container( - constraints: constraints ?? avatarTheme?.constraints, - color: selectionColor ?? colorTheme.accentPrimary, - child: Padding( - padding: EdgeInsets.all(selectionThickness), - child: avatar, - ), - ), - ); - } - return GestureDetector( - onTap: onTap != null ? () => onTap!(user) : null, - onLongPress: onLongPress != null ? () => onLongPress!(user) : null, - child: Stack( - children: [ - avatar, - if (showOnlineStatus && user.online) - Positioned.fill( - child: Align( - alignment: onlineIndicatorAlignment, - child: Material( - type: MaterialType.circle, - color: colorTheme.barsBg, - child: Container( - margin: const EdgeInsets.all(2), - constraints: onlineIndicatorConstraints ?? - const BoxConstraints.tightFor( - width: 8, - height: 8, - ), - child: Material( - shape: const CircleBorder(), - color: colorTheme.accentInfo, - ), - ), - ), - ), - ), - ], - ), - ); - } -} 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..51773694d1 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 @@ -89,8 +89,8 @@ class _EditMessageSheetState extends State { children: [ Padding( padding: const EdgeInsets.all(8), - child: StreamSvgIcon( - icon: StreamSvgIcons.edit, + child: Icon( + context.streamIcons.edit20, color: streamChatThemeData.colorTheme.disabled, ), ), @@ -100,8 +100,8 @@ class _EditMessageSheetState extends State { ), IconButton( visualDensity: VisualDensity.compact, - icon: StreamSvgIcon( - icon: StreamSvgIcons.closeSmall, + icon: Icon( + context.streamIcons.xmark16, color: streamChatThemeData.colorTheme.textLowEmphasis, ), onPressed: Navigator.of(context).pop, @@ -113,12 +113,7 @@ class _EditMessageSheetState extends State { widget.editMessageInputBuilder!(context, widget.message) else 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/bottom_sheets/error_alert_sheet.dart b/packages/stream_chat_flutter/lib/src/bottom_sheets/error_alert_sheet.dart index 744664dd8f..49e83139ca 100644 --- a/packages/stream_chat_flutter/lib/src/bottom_sheets/error_alert_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/bottom_sheets/error_alert_sheet.dart @@ -25,8 +25,8 @@ class ErrorAlertSheet extends StatelessWidget { const SizedBox( height: 26, ), - StreamSvgIcon( - icon: StreamSvgIcons.error, + Icon( + context.streamIcons.exclamationCircleFill20, color: _streamChatTheme.colorTheme.accentError, size: 24, ), diff --git a/packages/stream_chat_flutter/lib/src/bottom_sheets/stream_channel_info_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/bottom_sheets/stream_channel_info_bottom_sheet.dart index c1cb4ad060..18e4c11cd2 100644 --- a/packages/stream_chat_flutter/lib/src/bottom_sheets/stream_channel_info_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/bottom_sheets/stream_channel_info_bottom_sheet.dart @@ -13,9 +13,9 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { this.onDeleteConversationTap, this.onCancelTap, }) : assert( - channel.state != null, - 'Channel ${channel.id} is not initialized', - ); + channel.state != null, + 'Channel ${channel.id} is not initialized', + ); /// The [Channel] to show information about. final Channel channel; @@ -94,19 +94,15 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - StreamUserAvatar( - user: user, - constraints: const BoxConstraints.tightFor( - height: 64, - width: 64, + GestureDetector( + onTap: switch (onMemberTap) { + final onTap? => () => onTap(member), + _ => null, + }, + child: StreamUserAvatar( + size: .xl, + user: user, ), - borderRadius: BorderRadius.circular(32), - onlineIndicatorConstraints: BoxConstraints.tight( - const Size(12, 12), - ), - onTap: onMemberTap != null - ? (_) => onMemberTap!(member) - : null, ), const SizedBox(height: 6), Text( @@ -124,8 +120,8 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamSvgIcon( - icon: StreamSvgIcons.user, + child: Icon( + context.streamIcons.user20, color: colorTheme.textLowEmphasis, ), ), @@ -137,8 +133,8 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { title: context.translations.leaveGroupLabel, leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamSvgIcon( - icon: StreamSvgIcons.userRemove, + child: Icon( + context.streamIcons.userRemove20, color: colorTheme.textLowEmphasis, ), ), @@ -148,8 +144,8 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamSvgIcon( - icon: StreamSvgIcons.delete, + child: Icon( + context.streamIcons.delete20, color: colorTheme.accentError, ), ), @@ -160,8 +156,8 @@ class StreamChannelInfoBottomSheet extends StatelessWidget { StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamSvgIcon( - icon: StreamSvgIcons.closeSmall, + child: Icon( + context.streamIcons.xmark20, color: colorTheme.textLowEmphasis, ), ), @@ -259,30 +255,29 @@ Future showChannelInfoModalBottomSheet({ VoidCallback? onLeaveChannelTap, VoidCallback? onDeleteConversationTap, VoidCallback? onCancelTap, -}) => - showModalBottomSheet( - context: context, - backgroundColor: backgroundColor, - elevation: elevation, - shape: shape, - clipBehavior: clipBehavior, - constraints: constraints, - barrierColor: barrierColor, - isScrollControlled: isScrollControlled, - useRootNavigator: useRootNavigator, - isDismissible: isDismissible, - enableDrag: enableDrag, - routeSettings: routeSettings, - transitionAnimationController: transitionAnimationController, - builder: (BuildContext context) => StreamChannelInfoBottomSheet( - channel: channel, - onMemberTap: onMemberTap, - onViewInfoTap: onViewInfoTap, - onLeaveChannelTap: onLeaveChannelTap, - onDeleteConversationTap: onDeleteConversationTap, - onCancelTap: onCancelTap, - ), - ); +}) => showModalBottomSheet( + context: context, + backgroundColor: backgroundColor, + elevation: elevation, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + barrierColor: barrierColor, + isScrollControlled: isScrollControlled, + useRootNavigator: useRootNavigator, + isDismissible: isDismissible, + enableDrag: enableDrag, + routeSettings: routeSettings, + transitionAnimationController: transitionAnimationController, + builder: (BuildContext context) => StreamChannelInfoBottomSheet( + channel: channel, + onMemberTap: onMemberTap, + onViewInfoTap: onViewInfoTap, + onLeaveChannelTap: onLeaveChannelTap, + onDeleteConversationTap: onDeleteConversationTap, + onCancelTap: onCancelTap, + ), +); /// Shows a material design bottom sheet in the nearest [Scaffold] ancestor. If /// you wish to show a persistent bottom sheet, use [Scaffold.bottomSheet]. @@ -339,21 +334,20 @@ PersistentBottomSheetController showChannelInfoBottomSheet({ VoidCallback? onLeaveChannelTap, VoidCallback? onDeleteConversationTap, VoidCallback? onCancelTap, -}) => - showBottomSheet( - context: context, - backgroundColor: backgroundColor, - elevation: elevation, - shape: shape, - clipBehavior: clipBehavior, - constraints: constraints, - transitionAnimationController: transitionAnimationController, - builder: (BuildContext context) => StreamChannelInfoBottomSheet( - channel: channel, - onMemberTap: onMemberTap, - onViewInfoTap: onViewInfoTap, - onLeaveChannelTap: onLeaveChannelTap, - onDeleteConversationTap: onDeleteConversationTap, - onCancelTap: onCancelTap, - ), - ); +}) => showBottomSheet( + context: context, + backgroundColor: backgroundColor, + elevation: elevation, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + transitionAnimationController: transitionAnimationController, + builder: (BuildContext context) => StreamChannelInfoBottomSheet( + channel: channel, + onMemberTap: onMemberTap, + onViewInfoTap: onViewInfoTap, + onLeaveChannelTap: onLeaveChannelTap, + onDeleteConversationTap: onDeleteConversationTap, + onCancelTap: onCancelTap, + ), +); diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart index 20dd2d5ae3..813930abdb 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_header.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template streamChannelHeader} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header.png) @@ -50,8 +51,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// and the [StreamChatThemeData.channelHeaderTheme] property. Modify it to /// change the widget's appearance. /// {@endtemplate} -class StreamChannelHeader extends StatelessWidget - implements PreferredSizeWidget { +class StreamChannelHeader extends StatelessWidget implements PreferredSizeWidget { /// {@macro streamChannelHeader} const StreamChannelHeader({ super.key, @@ -63,12 +63,13 @@ class StreamChannelHeader extends StatelessWidget this.showConnectionStateTile = false, this.title, this.subtitle, - this.centerTitle, + this.centerTitle = true, this.leading, this.actions, this.bottom, this.backgroundColor, - this.elevation = 1, + this.elevation = 0, + this.scrolledUnderElevation = 0, this.bottomOpacity = 1, }); @@ -103,7 +104,7 @@ class StreamChannelHeader extends StatelessWidget final Widget? subtitle; /// Whether the title should be centered - final bool? centerTitle; + final bool centerTitle; /// Leading widget final Widget? leading; @@ -122,6 +123,9 @@ class StreamChannelHeader extends StatelessWidget /// The elevation for this [StreamChannelHeader]. final double elevation; + /// The scrolled under elevation for this [StreamChannelHeader]. + final double scrolledUnderElevation; + /// The opacity of the bottom widget. final double bottomOpacity; @@ -141,7 +145,8 @@ class StreamChannelHeader extends StatelessWidget final channel = StreamChannel.of(context).channel; final channelHeaderTheme = StreamChannelHeaderTheme.of(context); - final leadingWidget = leading ?? + final leadingWidget = + leading ?? (showBackButton ? StreamBackButton( onPressed: onBackPressed, @@ -149,86 +154,113 @@ class StreamChannelHeader extends StatelessWidget ) : const SizedBox()); - return StreamConnectionStatusBuilder( - statusBuilder: (context, status) { - var statusString = ''; - var showStatus = true; - - switch (status) { - case ConnectionStatus.connected: - statusString = context.translations.connectedLabel; - showStatus = false; - break; - case ConnectionStatus.connecting: - statusString = context.translations.reconnectingLabel; - break; - case ConnectionStatus.disconnected: - statusString = context.translations.disconnectedLabel; - break; - } - - final theme = Theme.of(context); - - return StreamInfoTile( - showMessage: showConnectionStateTile && showStatus, - message: statusString, - child: AppBar( - toolbarTextStyle: theme.textTheme.bodyMedium, - titleTextStyle: theme.textTheme.titleLarge, - systemOverlayStyle: theme.brightness == Brightness.dark - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark, - elevation: elevation, - leading: leadingWidget, - bottom: bottom, - bottomOpacity: bottomOpacity, - backgroundColor: backgroundColor ?? channelHeaderTheme.color, - actions: actions ?? - [ - Padding( - padding: const EdgeInsets.only(right: 10), - child: Center( + return Portal( + child: StreamConnectionStatusBuilder( + statusBuilder: (context, status) { + var statusString = ''; + var showStatus = true; + + switch (status) { + case ConnectionStatus.connected: + statusString = context.translations.connectedLabel; + showStatus = false; + break; + case ConnectionStatus.connecting: + statusString = context.translations.reconnectingLabel; + break; + case ConnectionStatus.disconnected: + statusString = context.translations.disconnectedLabel; + break; + } + + return StreamInfoTile( + showMessage: showConnectionStateTile && showStatus, + message: statusString, + child: StreamAppBar( + titleTextStyle: Theme.of(context).textTheme.titleLarge, + elevation: elevation, + scrolledUnderElevation: scrolledUnderElevation, + leading: Padding( + padding: .directional(start: context.streamSpacing.sm), + child: leadingWidget, + ), + leadingWidth: StreamAvatarSize.lg.value, + titleSpacing: context.streamSpacing.sm, + bottom: bottom, + bottomOpacity: bottomOpacity, + backgroundColor: backgroundColor ?? channelHeaderTheme.color, + actions: + actions ?? + [ + if (effectiveCenterTitle) + Padding( + padding: .directional(end: context.streamSpacing.sm), + child: Center( + child: GestureDetector( + onTap: onImageTap, + child: StreamChannelAvatar( + size: .lg, + channel: channel, + ), + ), + ), + ), + ], + centerTitle: centerTitle, + title: Row( + spacing: context.streamSpacing.sm, + children: [ + if (!effectiveCenterTitle) ...[ + GestureDetector( + onTap: onImageTap, child: StreamChannelAvatar( + size: .lg, channel: channel, - borderRadius: - channelHeaderTheme.avatarTheme?.borderRadius, - constraints: - channelHeaderTheme.avatarTheme?.constraints, - onTap: onImageTap, + ), + ), + ], + Expanded( + child: InkWell( + onTap: onTitleTap, + child: SizedBox( + height: preferredSize.height, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: effectiveCenterTitle + ? CrossAxisAlignment.center + : CrossAxisAlignment.stretch, + children: [ + title ?? + StreamChannelName( + channel: channel, + textStyle: + channelHeaderTheme.titleStyle ?? + context.streamTextTheme.headingSm.copyWith( + color: context.streamColorScheme.textPrimary, + ), + ), + const SizedBox(height: 2), + subtitle ?? + StreamChannelInfo( + showTypingIndicator: showTypingIndicator, + channel: channel, + textStyle: + channelHeaderTheme.subtitleStyle ?? + context.streamTextTheme.captionDefault.copyWith( + color: context.streamColorScheme.textSecondary, + ), + ), + ], + ), ), ), ), ], - centerTitle: centerTitle, - title: InkWell( - onTap: onTitleTap, - child: SizedBox( - height: preferredSize.height, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: effectiveCenterTitle - ? CrossAxisAlignment.center - : CrossAxisAlignment.stretch, - children: [ - title ?? - StreamChannelName( - channel: channel, - textStyle: channelHeaderTheme.titleStyle, - ), - const SizedBox(height: 2), - subtitle ?? - StreamChannelInfo( - showTypingIndicator: showTypingIndicator, - channel: channel, - textStyle: channelHeaderTheme.subtitleStyle, - ), - ], - ), ), ), - ), - ); - }, + ); + }, + ), ); } } diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel/channel_info.dart index d74d26673b..a5c973d1a0 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_info.dart @@ -33,6 +33,12 @@ class StreamChannelInfo extends StatelessWidget { @override Widget build(BuildContext context) { final client = StreamChat.of(context).client; + final effectiveTextStyle = + textStyle ?? + context.streamTextTheme.captionDefault.copyWith( + color: context.streamColorScheme.textSecondary, + ); + return BetterStreamBuilder>( stream: channel.state!.membersStream, initialData: channel.state!.members, @@ -43,16 +49,16 @@ class StreamChannelInfo extends StatelessWidget { return _ConnectedTitleState( channel: channel, showTypingIndicator: showTypingIndicator, - textStyle: textStyle, + textStyle: effectiveTextStyle, members: data, parentId: parentId, ); case ConnectionStatus.connecting: - return _ConnectingTitleState(textStyle: textStyle); + return _ConnectingTitleState(textStyle: effectiveTextStyle); case ConnectionStatus.disconnected: return _DisconnectedTitleState( client: client, - textStyle: textStyle, + textStyle: effectiveTextStyle, ); } }, @@ -83,15 +89,11 @@ class _ConnectedTitleState extends StatelessWidget { final memberCount = channel.memberCount; if (memberCount != null && memberCount > 2) { var text = context.translations.membersCountText(memberCount); - final onlineCount = - members?.where((m) => m.user?.online == true).length ?? 0; + final onlineCount = members?.where((m) => m.user?.online == true).length ?? 0; if (onlineCount > 0) { text += ', ${context.translations.watchersCountText(onlineCount)}'; } - alternativeWidget = Text( - text, - style: StreamChannelHeaderTheme.of(context).subtitleStyle, - ); + alternativeWidget = Text(text, style: textStyle); } else { final userId = StreamChat.of(context).currentUser?.id; final otherMember = members?.firstWhereOrNull( diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart index 2c3338425e..bc9acc7c2e 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_list_header.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template streamChannelListHeader} /// Shows the current [StreamChatClient] status. @@ -37,8 +38,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// the [StreamChannelListHeaderThemeData] property. Modify it to change the /// widget's appearance. /// {@endtemplate} -class StreamChannelListHeader extends StatelessWidget - implements PreferredSizeWidget { +class StreamChannelListHeader extends StatelessWidget implements PreferredSizeWidget { /// {@macro streamChannelListHeader} const StreamChannelListHeader({ super.key, @@ -49,11 +49,12 @@ class StreamChannelListHeader extends StatelessWidget this.showConnectionStateTile = false, this.preNavigationCallback, this.subtitle, - this.centerTitle, + this.centerTitle = true, this.leading, this.actions, this.backgroundColor, - this.elevation = 1, + this.elevation = 0, + this.scrolledUnderElevation = 0, }); /// Use this if you don't have a [StreamChatClient] in your widget tree. @@ -80,7 +81,7 @@ class StreamChannelListHeader extends StatelessWidget final Widget? subtitle; /// Whether the title should be centered - final bool? centerTitle; + final bool centerTitle; /// Leading widget /// @@ -98,6 +99,9 @@ class StreamChannelListHeader extends StatelessWidget /// The elevation for this [StreamChannelListHeader]. final double elevation; + /// The scrolled under elevation for this [StreamChannelListHeader]. + final double scrolledUnderElevation; + @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); @@ -105,106 +109,101 @@ class StreamChannelListHeader extends StatelessWidget Widget build(BuildContext context) { final _client = client ?? StreamChat.of(context).client; final user = _client.state.currentUser; - return StreamConnectionStatusBuilder( - statusBuilder: (context, status) { - var statusString = ''; - var showStatus = true; + return Portal( + child: StreamConnectionStatusBuilder( + statusBuilder: (context, status) { + var statusString = ''; + var showStatus = true; - switch (status) { - case ConnectionStatus.connected: - statusString = context.translations.connectedLabel; - showStatus = false; - break; - case ConnectionStatus.connecting: - statusString = context.translations.reconnectingLabel; - break; - case ConnectionStatus.disconnected: - statusString = context.translations.disconnectedLabel; - break; - } + switch (status) { + case ConnectionStatus.connected: + statusString = context.translations.connectedLabel; + showStatus = false; + break; + case ConnectionStatus.connecting: + statusString = context.translations.reconnectingLabel; + break; + case ConnectionStatus.disconnected: + statusString = context.translations.disconnectedLabel; + break; + } - final chatThemeData = StreamChatTheme.of(context); - final channelListHeaderThemeData = - StreamChannelListHeaderTheme.of(context); - final theme = Theme.of(context); - return StreamInfoTile( - showMessage: showConnectionStateTile && showStatus, - message: statusString, - child: AppBar( - toolbarTextStyle: theme.textTheme.bodyMedium, - titleTextStyle: theme.textTheme.titleLarge, - systemOverlayStyle: theme.brightness == Brightness.dark - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark, - elevation: elevation, - backgroundColor: - backgroundColor ?? channelListHeaderThemeData.color, - centerTitle: centerTitle, - leading: leading ?? - Center( - child: user != null - ? StreamUserAvatar( - user: user, - showOnlineStatus: false, - onTap: onUserAvatarTap ?? - (_) { - preNavigationCallback?.call(); - Scaffold.of(context).openDrawer(); - }, - borderRadius: channelListHeaderThemeData - .avatarTheme?.borderRadius, - constraints: channelListHeaderThemeData - .avatarTheme?.constraints, - ) - : const Empty(), - ), - actions: actions ?? - [ - StreamNeumorphicButton( - child: IconButton( - icon: StreamConnectionStatusBuilder( - statusBuilder: (context, status) { - final color = switch (status) { - ConnectionStatus.connected => - chatThemeData.colorTheme.accentPrimary, - ConnectionStatus.connecting => Colors.grey, - ConnectionStatus.disconnected => Colors.grey, - }; + final channelListHeaderThemeData = StreamChannelListHeaderTheme.of(context); - return StreamSvgIcon( - size: 24, - color: color, - icon: StreamSvgIcons.penWrite, - ); + return StreamInfoTile( + showMessage: showConnectionStateTile && showStatus, + message: statusString, + child: StreamAppBar( + elevation: elevation, + scrolledUnderElevation: scrolledUnderElevation, + backgroundColor: backgroundColor ?? channelListHeaderThemeData.color, + centerTitle: centerTitle, + leading: switch ((leading, user)) { + (final leading?, _) => leading, + (_, final user?) => Padding( + padding: .directional(start: context.streamSpacing.sm), + child: Center( + child: GestureDetector( + onTap: switch (onUserAvatarTap) { + final onTap? => () => onTap(user), + _ => () { + preNavigationCallback?.call(); + Scaffold.of(context).openDrawer(); }, + }, + child: StreamUserAvatar( + size: .lg, + user: user, + showOnlineIndicator: false, ), - onPressed: onNewChatButtonTap, ), ), - ], - title: Column( - children: [ - Builder( - builder: (context) { - if (titleBuilder != null) { - return titleBuilder!(context, status, _client); - } - switch (status) { - case ConnectionStatus.connected: - return _ConnectedTitleState(); - case ConnectionStatus.connecting: - return _ConnectingTitleState(); - case ConnectionStatus.disconnected: - return _DisconnectedTitleState(client: _client); - } - }, ), - subtitle ?? const Empty(), - ], + _ => const Empty(), + }, + actionsPadding: .directional(end: context.streamSpacing.sm), + actions: + actions ?? + [ + StreamConnectionStatusBuilder( + statusBuilder: (context, status) { + final callback = switch (status) { + ConnectionStatus.connected => onNewChatButtonTap, + ConnectionStatus.connecting => null, + ConnectionStatus.disconnected => null, + }; + + return StreamButton.icon( + icon: context.streamIcons.plus20, + onTap: callback, + ); + }, + ), + ], + title: Column( + children: [ + Builder( + builder: (context) { + if (titleBuilder != null) { + return titleBuilder!(context, status, _client); + } + switch (status) { + case ConnectionStatus.connected: + return _ConnectedTitleState(); + case ConnectionStatus.connecting: + return _ConnectingTitleState(); + case ConnectionStatus.disconnected: + return _DisconnectedTitleState(client: _client); + } + }, + ), + subtitle ?? const Empty(), + ], + ), ), - ), - ); - }, + ); + }, + ), ); } } @@ -212,12 +211,10 @@ class StreamChannelListHeader extends StatelessWidget class _ConnectedTitleState extends StatelessWidget { @override Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); + final textTheme = context.streamTextTheme; return Text( context.translations.streamChatLabel, - style: chatThemeData.textTheme.headlineBold.copyWith( - color: chatThemeData.colorTheme.textHighEmphasis, - ), + style: textTheme.headingSm, ); } } @@ -239,9 +236,9 @@ class _ConnectingTitleState extends StatelessWidget { Text( context.translations.searchingForNetworkText, style: StreamChannelListHeaderTheme.of(context).titleStyle?.copyWith( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), ], ); diff --git a/packages/stream_chat_flutter/lib/src/channel/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel/channel_name.dart index ddccdb7200..79aedaddbd 100644 --- a/packages/stream_chat_flutter/lib/src/channel/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel/channel_name.dart @@ -87,8 +87,7 @@ class _NameGenerator extends StatelessWidget { } }); - final exceedingMembers = - otherMembers.length - currentMembers.length; + final exceedingMembers = otherMembers.length - currentMembers.length; channelName = '${currentMembers.map((e) => e.user?.name).join(', ')} ' '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; 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 deleted file mode 100644 index 2f7665939f..0000000000 --- a/packages/stream_chat_flutter/lib/src/channel/stream_channel_avatar.dart +++ /dev/null @@ -1,256 +0,0 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image_paint.png) -/// -/// It shows the current [Channel] image. -/// -/// ```dart -/// class MyApp extends StatelessWidget { -/// final StreamChatClient client; -/// final Channel channel; -/// -/// MyApp(this.client, this.channel); -/// -/// @override -/// Widget build(BuildContext context) { -/// return MaterialApp( -/// debugShowCheckedModeBanner: false, -/// home: StreamChat( -/// client: client, -/// child: StreamChannel( -/// channel: channel, -/// child: Center( -/// child: StreamChannelAvatar( -/// channel: channel, -/// ), -/// ), -/// ), -/// ), -/// ); -/// } -/// } -/// ``` -/// -/// The widget uses a [StreamBuilder] to render the channel information -/// image as soon as it updates. -/// -/// By default the widget radius size is 40x40 pixels. -/// Set the property [constraints] to set a custom dimension. -/// -/// The widget renders the ui based on the first ancestor of type -/// [StreamChatTheme]. -/// Modify it to change the widget appearance. -class StreamChannelAvatar extends StatelessWidget { - /// Instantiate a new ChannelImage - StreamChannelAvatar({ - super.key, - required this.channel, - this.constraints, - this.onTap, - this.borderRadius, - this.selected = false, - this.selectionColor, - this.selectionThickness = 4, - this.ownSpaceAvatarBuilder, - this.oneToOneAvatarBuilder, - this.groupAvatarBuilder, - }) : assert( - channel.state != null, - 'Channel ${channel.id} is not initialized', - ); - - /// [BorderRadius] to display the widget - final BorderRadius? borderRadius; - - /// The channel to show the image of - final Channel channel; - - /// The diameter of the image - final BoxConstraints? constraints; - - /// The function called when the image is tapped - final VoidCallback? onTap; - - /// If image is selected - final bool selected; - - /// Selection color for image - final Color? selectionColor; - - /// Thickness of selection image - final double selectionThickness; - - /// Builder to create avatar for own space channel. - /// - /// Defaults to [StreamUserAvatar]. - final StreamUserAvatarBuilder? ownSpaceAvatarBuilder; - - /// Builder to create avatar for one to one channel. - /// - /// Defaults to [StreamUserAvatar]. - final StreamUserAvatarBuilder? oneToOneAvatarBuilder; - - /// Builder to create avatar for group channel. - /// - /// Defaults to [StreamGroupAvatar]. - final StreamGroupAvatarBuilder? groupAvatarBuilder; - - @override - Widget build(BuildContext context) { - final client = channel.client.state; - - final chatThemeData = StreamChatTheme.of(context); - final colorTheme = chatThemeData.colorTheme; - final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme; - - final fallbackWidget = Center( - child: Text( - channel.name?.characters.firstOrNull ?? '', - style: TextStyle( - color: colorTheme.barsBg, - fontWeight: FontWeight.bold, - ), - ), - ); - - return BetterStreamBuilder( - stream: channel.imageStream, - initialData: channel.image, - builder: (context, channelImage) { - Widget child = ClipRRect( - borderRadius: - borderRadius ?? previewTheme?.borderRadius ?? BorderRadius.zero, - child: Container( - constraints: constraints ?? previewTheme?.constraints, - decoration: BoxDecoration(color: colorTheme.accentPrimary), - child: InkWell( - onTap: onTap, - child: LayoutBuilder( - builder: (context, constraints) { - if (channelImage.isEmpty) return fallbackWidget; - - // Calculate optimal thumbnail size for the avatar - final devicePixel = MediaQuery.devicePixelRatioOf(context); - final thumbnailSize = constraints.biggest * devicePixel; - - int? cacheWidth, cacheHeight; - if (thumbnailSize.isFinite && !thumbnailSize.isEmpty) { - cacheWidth = thumbnailSize.width.round(); - cacheHeight = thumbnailSize.height.round(); - } - - return CachedNetworkImage( - imageUrl: channelImage, - memCacheWidth: cacheWidth, - memCacheHeight: cacheHeight, - errorWidget: (_, __, ___) => fallbackWidget, - fit: BoxFit.cover, - ); - }, - ), - ), - ), - ); - - if (selected) { - child = ClipRRect( - key: const Key('selectedImage'), - borderRadius: BorderRadius.circular(selectionThickness) + - (borderRadius ?? - previewTheme?.borderRadius ?? - BorderRadius.zero), - child: Container( - constraints: constraints ?? previewTheme?.constraints, - color: selectionColor ?? colorTheme.accentPrimary, - child: Padding( - padding: EdgeInsets.all(selectionThickness), - child: child, - ), - ), - ); - } - return child; - }, - noDataBuilder: (context) { - final currentUser = client.currentUser!; - final otherMembers = channel.state!.members - .where((it) => it.userId != currentUser.id) - .toList(growable: false); - - // our own space, no other members - if (otherMembers.isEmpty) { - return BetterStreamBuilder( - stream: client.currentUserStream.map((it) => it!), - initialData: currentUser, - builder: (context, user) { - final ownSpaceBuilder = ownSpaceAvatarBuilder; - if (ownSpaceBuilder != null) { - return ownSpaceBuilder(context, user, selected); - } - - return StreamUserAvatar( - borderRadius: borderRadius ?? previewTheme?.borderRadius, - user: user, - constraints: constraints ?? previewTheme?.constraints, - onTap: onTap != null ? (_) => onTap!() : null, - selected: selected, - selectionColor: selectionColor ?? colorTheme.accentPrimary, - selectionThickness: selectionThickness, - ); - }, - ); - } - - // 1-1 Conversation - if (otherMembers.length == 1) { - final member = otherMembers.first; - return BetterStreamBuilder( - stream: channel.state!.membersStream.map( - (members) => members.firstWhere( - (it) => it.userId == member.userId, - orElse: () => member, - ), - ), - initialData: member, - builder: (context, member) { - final oneToOneBuilder = oneToOneAvatarBuilder; - if (oneToOneBuilder != null) { - return oneToOneBuilder(context, member.user!, selected); - } - - return StreamUserAvatar( - borderRadius: borderRadius ?? previewTheme?.borderRadius, - user: member.user!, - constraints: constraints ?? previewTheme?.constraints, - onTap: onTap != null ? (_) => onTap!() : null, - selected: selected, - selectionColor: selectionColor ?? colorTheme.accentPrimary, - selectionThickness: selectionThickness, - ); - }, - ); - } - - final groupBuilder = groupAvatarBuilder; - if (groupBuilder != null) { - return groupBuilder(context, otherMembers, selected); - } - - // Group conversation - return StreamGroupAvatar( - channel: channel, - members: otherMembers, - borderRadius: borderRadius ?? previewTheme?.borderRadius, - constraints: constraints ?? previewTheme?.constraints, - onTap: onTap, - selected: selected, - selectionColor: selectionColor ?? colorTheme.accentPrimary, - selectionThickness: selectionThickness, - ); - }, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/channel/stream_channel_name.dart b/packages/stream_chat_flutter/lib/src/channel/stream_channel_name.dart index 273b5b37e8..4dc226be84 100644 --- a/packages/stream_chat_flutter/lib/src/channel/stream_channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel/stream_channel_name.dart @@ -13,9 +13,9 @@ class StreamChannelName extends StatelessWidget { this.textStyle, this.textOverflow = TextOverflow.ellipsis, }) : assert( - channel.state != null, - 'Channel ${channel.id} is not initialized', - ); + channel.state != null, + 'Channel ${channel.id} is not initialized', + ); /// The [Channel] to show the name for. final Channel channel; @@ -28,63 +28,69 @@ class StreamChannelName extends StatelessWidget { @override Widget build(BuildContext context) => BetterStreamBuilder( - stream: channel.nameStream, - initialData: channel.name, - builder: (context, channelName) => Text( - channelName, - style: textStyle, - overflow: textOverflow, - ), - noDataBuilder: (context) => _generateName( - channel.client.state.currentUser!, - channel.state!.members, - ), - ); + stream: channel.nameStream, + initialData: channel.name, + builder: (context, channelName) => Text( + channelName, + style: textStyle, + overflow: textOverflow, + ), + noDataBuilder: (context) => _generateName( + channel.client.state.currentUser!, + channel.state!.members, + ), + ); Widget _generateName( User currentUser, List members, - ) => - LayoutBuilder( - builder: (context, constraints) { - var channelName = context.translations.noTitleText; - final otherMembers = members.where( - (member) => member.userId != currentUser.id, - ); - - if (otherMembers.isNotEmpty) { - if (otherMembers.length == 1) { - final user = otherMembers.first.user; - if (user != null) { - channelName = user.name; - } - } else { - final maxWidth = constraints.maxWidth; - final maxChars = maxWidth / (textStyle?.fontSize ?? 1); - var currentChars = 0; - final currentMembers = []; - otherMembers.forEach((element) { - final newLength = - currentChars + (element.user?.name.length ?? 0); - if (newLength < maxChars) { - currentChars = newLength; - currentMembers.add(element); - } - }); + ) => LayoutBuilder( + builder: (context, constraints) { + var channelName = context.translations.noTitleText; + final otherMembers = members.where( + (member) => member.userId != currentUser.id, + ); - final exceedingMembers = - otherMembers.length - currentMembers.length; - channelName = - '${currentMembers.map((e) => e.user?.name).join(', ')} ' - '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; - } + if (otherMembers.isNotEmpty) { + if (otherMembers.length == 1) { + final user = otherMembers.first.user; + if (user != null) { + channelName = user.name; } + } else { + final maxWidth = constraints.maxWidth; + channelName = ''; + final currentMembers = []; + otherMembers.forEach((element) { + final newTitle = _getChannelName(currentMembers: [...currentMembers, element], members: members); + if (_calculateTextSize(newTitle).width < maxWidth) { + currentMembers.add(element); + channelName = newTitle; + } + }); + } + } - return Text( - channelName, - style: textStyle, - overflow: textOverflow, - ); - }, + return Text( + channelName, + style: textStyle, + overflow: textOverflow, ); + }, + ); + + String _getChannelName({required List currentMembers, required List members}) { + final exceedingMembers = members.length - currentMembers.length; + return '${currentMembers.map((e) => e.user?.name).join(', ')} ' + '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + } + + Size _calculateTextSize(String text) { + final textPainter = TextPainter( + text: TextSpan(text: text, style: textStyle), + maxLines: 1, + textDirection: TextDirection.ltr, + )..layout(minWidth: 0, maxWidth: double.infinity); + return textPainter.size; + } } diff --git a/packages/stream_chat_flutter/lib/src/components/avatar/stream_channel_avatar.dart b/packages/stream_chat_flutter/lib/src/components/avatar/stream_channel_avatar.dart new file mode 100644 index 0000000000..17199eac47 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/avatar/stream_channel_avatar.dart @@ -0,0 +1,130 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/components/avatar/stream_user_avatar.dart'; +import 'package:stream_chat_flutter/src/components/avatar/stream_user_avatar_group.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// A circular avatar component for displaying a channel's image. +/// +/// [StreamChannelAvatar] displays a channel's image or an appropriate fallback +/// based on the channel type. It supports channel images, user avatars for +/// 1-1 conversations, and group avatars for multi-member channels. +/// +/// The avatar automatically handles: +/// - Reactive updates when channel image changes via [Channel.imageStream] +/// - Fallback to member avatars when no channel image is set +/// - Deterministic color assignment for member avatars +/// +/// {@tool snippet} +/// +/// Basic usage with a channel: +/// +/// ```dart +/// StreamChannelAvatar(channel: channel) +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// +/// With custom size: +/// +/// ```dart +/// StreamChannelAvatar( +/// channel: channel, +/// size: StreamAvatarGroupSize.xl, +/// ) +/// ``` +/// {@end-tool} +/// +/// ## Theming +/// +/// [StreamChannelAvatar] uses [StreamAvatarThemeData] for default styling. +/// Member avatars within the channel avatar use deterministic colors from +/// [StreamColorScheme.avatarPalette]. +/// +/// See also: +/// +/// * [StreamAvatarGroupSize], which defines the available size variants. +/// * [StreamAvatarThemeData], which provides theme-level customization. +/// * [StreamUserAvatar], which is used to display individual member avatars. +class StreamChannelAvatar extends StatelessWidget { + /// Creates a Stream channel avatar. + const StreamChannelAvatar({ + super.key, + this.size, + required this.channel, + }); + + /// The channel whose avatar is displayed. + final Channel channel; + + /// The size of the channel avatar. + /// + /// If null, defaults to [StreamAvatarGroupSize.lg]. + final StreamAvatarGroupSize? size; + + @override + Widget build(BuildContext context) { + assert(channel.state != null, 'Channel ${channel.id} is not initialized'); + + final effectiveSize = size ?? StreamAvatarGroupSize.lg; + + return BetterStreamBuilder( + stream: channel.imageStream, + initialData: channel.image, + builder: (context, channelImage) => StreamAvatar( + imageUrl: channelImage, + size: _avatarSizeForAvatarGroupSize(effectiveSize), + placeholder: (_) => const _StreamChannelAvatarPlaceholder(), + ), + noDataBuilder: (context) => BetterStreamBuilder( + stream: channel.state!.membersStream, + initialData: channel.state!.members, + builder: (context, members) { + final users = members.map((it) => it.user!).toList(); + final currentUserId = channel.client.state.currentUser?.id; + + if (channel.isDistinct && users.length == 2) { + final otherUser = users.firstWhere( + (u) => u.id != currentUserId, + orElse: () => users.first, + ); + return StreamUserAvatar( + user: otherUser, + size: _avatarSizeForAvatarGroupSize(effectiveSize), + // TODO: make this configurable when the online state is shown. + showOnlineIndicator: otherUser.online, + ); + } + + return StreamUserAvatarGroup( + size: effectiveSize, + users: users.sortedBy((it) => it.id == currentUserId ? 1 : 0), + ); + }, + ), + ); + } + + // Maps [StreamAvatarGroupSize] to corresponding [StreamAvatarSize]. + // + // Used when displaying a single channel image avatar. + StreamAvatarSize _avatarSizeForAvatarGroupSize( + StreamAvatarGroupSize size, + ) => switch (size) { + .lg => StreamAvatarSize.lg, + .xl => StreamAvatarSize.xl, + .xxl => StreamAvatarSize.xxl, + }; +} + +// Placeholder widget for [StreamChannelAvatar]. +// +// Displays a team icon as a fallback when the channel image fails to load. +class _StreamChannelAvatarPlaceholder extends StatelessWidget { + const _StreamChannelAvatarPlaceholder(); + + @override + Widget build(BuildContext context) => Icon(context.streamIcons.users20); +} diff --git a/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar.dart b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar.dart new file mode 100644 index 0000000000..9bfe44f69d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// A circular avatar component for displaying a user's profile. +/// +/// [StreamUserAvatar] displays a user's profile image or initials placeholder +/// when no image is available. It supports multiple sizes, deterministic +/// color assignment, and an optional online status indicator. +/// +/// The avatar automatically handles: +/// - Displaying the user's profile image when available +/// - Showing user initials as a placeholder when no image exists +/// - Deterministic color assignment based on the user's ID +/// - Online status indicator positioning +/// +/// {@tool snippet} +/// +/// Basic usage with a user: +/// +/// ```dart +/// StreamUserAvatar(user: currentUser) +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// +/// With online indicator and custom size: +/// +/// ```dart +/// StreamUserAvatar( +/// user: currentUser, +/// size: StreamAvatarSize.lg, +/// showOnlineIndicator: true, +/// ) +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// +/// With border for selection states: +/// +/// ```dart +/// StreamUserAvatar( +/// user: selectedUser, +/// showBorder: true, +/// ) +/// ``` +/// {@end-tool} +/// +/// ## Theming +/// +/// [StreamUserAvatar] uses [StreamAvatarThemeData] for default styling. Colors +/// are automatically assigned based on the user's ID hash, selecting from +/// [StreamColorScheme.avatarPalette] for consistent user-specific colors. +/// +/// See also: +/// +/// * [StreamAvatarSize], which defines the available size variants. +/// * [StreamAvatarThemeData], which provides theme-level customization. +/// * [StreamColorScheme.avatarPalette], which provides colors for user avatars. +class StreamUserAvatar extends StatelessWidget { + /// Creates a Stream user avatar. + const StreamUserAvatar({ + super.key, + this.size, + required this.user, + this.showBorder = true, + this.showOnlineIndicator = true, + }); + + /// The user whose avatar is displayed. + final User user; + + /// Whether to show a border around the avatar. + /// + /// Defaults to true. The border style is determined by + /// [StreamAvatarThemeData.border]. + final bool showBorder; + + /// Whether to show the online status indicator. + /// + /// Defaults to true. + final bool showOnlineIndicator; + + /// The size of the avatar. + /// + /// If null, uses [StreamAvatarThemeData.size], or falls back to + /// [StreamAvatarSize.lg]. + final StreamAvatarSize? size; + + @override + Widget build(BuildContext context) { + final avatarTheme = context.streamAvatarTheme; + final colorScheme = context.streamColorScheme; + final avatarPalette = colorScheme.avatarPalette; + + final userHash = user.id.hashCode; // Ensure deterministic colors. + final colorPair = avatarPalette[userHash % avatarPalette.length]; + + final effectiveSize = size ?? avatarTheme.size ?? .lg; + final effectiveBackgroundColor = avatarTheme.backgroundColor ?? colorPair.backgroundColor; + final effectiveForegroundColor = avatarTheme.foregroundColor ?? colorPair.foregroundColor; + + final userAvatar = StreamAvatar( + size: effectiveSize, + imageUrl: user.image, + showBorder: showBorder, + backgroundColor: effectiveBackgroundColor, + foregroundColor: effectiveForegroundColor, + placeholder: (_) => _StreamUserAvatarPlaceholder(user: user, size: effectiveSize), + ); + + if (showOnlineIndicator) { + final indicatorSize = _indicatorSizeForAvatarSize(effectiveSize); + + return StreamOnlineIndicator( + size: indicatorSize, + isOnline: user.online, + alignment: AlignmentDirectional.topEnd, + child: userAvatar, + ); + } + + return userAvatar; + } + + // Maps [StreamAvatarSize] to corresponding [StreamOnlineIndicatorSize]. + // + // Ensures the online indicator scales appropriately with the avatar size. + StreamOnlineIndicatorSize _indicatorSizeForAvatarSize( + StreamAvatarSize size, + ) => switch (size) { + .xs || .sm => StreamOnlineIndicatorSize.sm, + .md => StreamOnlineIndicatorSize.md, + .lg => StreamOnlineIndicatorSize.lg, + .xl || .xxl => StreamOnlineIndicatorSize.xl, + }; +} + +// Placeholder widget for [StreamUserAvatar]. +// +// Displays user initials or a fallback person icon when no name is available. +// Shows full initials (up to 2 characters) for medium and larger sizes, +// and only the first initial for extra-small and small sizes. +class _StreamUserAvatarPlaceholder extends StatelessWidget { + const _StreamUserAvatarPlaceholder({ + required this.user, + required this.size, + }); + + final User user; + final StreamAvatarSize size; + + @override + Widget build(BuildContext context) { + final userInitials = user.name.initials; + if (userInitials != null && userInitials.isNotEmpty) { + return switch (size) { + .md || .lg || .xl || .xxl => Text(userInitials), + .xs || .sm => Text(userInitials.characters.first), + }; + } + + return Icon(context.streamIcons.user20); + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_group.dart b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_group.dart new file mode 100644 index 0000000000..5b557f4e6b --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_group.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/components/avatar/stream_user_avatar.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// A widget that displays multiple user avatars in a grid layout. +/// +/// [StreamUserAvatarGroup] arranges user avatars in a 2x2 grid pattern, +/// typically used for displaying group channel participants. It supports +/// two sizes and automatically handles overflow with a badge indicator. +/// +/// The group automatically handles: +/// - Grid layout for up to 4 user avatars +/// - Overflow indicator showing remaining count for additional users +/// - Deterministic color assignment for each user avatar +/// - Consistent sizing across all child avatars +/// +/// {@tool snippet} +/// +/// Basic usage with a list of users: +/// +/// ```dart +/// StreamUserAvatarGroup(users: groupMembers) +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// +/// With custom size: +/// +/// ```dart +/// StreamUserAvatarGroup( +/// users: groupMembers, +/// size: StreamAvatarGroupSize.xl, +/// ) +/// ``` +/// {@end-tool} +/// +/// ## Theming +/// +/// [StreamUserAvatarGroup] uses [StreamAvatarThemeData] for styling the child +/// avatars. Individual user avatars use deterministic colors from +/// [StreamColorScheme.avatarPalette]. +/// +/// See also: +/// +/// * [StreamAvatarGroupSize], which defines the available size variants. +/// * [StreamUserAvatar], which is used to display individual user avatars. +/// * [StreamAvatarGroup], the underlying group component. +class StreamUserAvatarGroup extends StatelessWidget { + /// Creates a Stream user avatar group. + /// + /// If [users] is empty, returns an empty [SizedBox]. + const StreamUserAvatarGroup({ + super.key, + required this.users, + this.size, + }); + + /// The list of users whose avatars are displayed. + final Iterable users; + + /// The size of the avatar group. + /// + /// If null, defaults to [StreamAvatarGroupSize.lg]. + final StreamAvatarGroupSize? size; + + @override + Widget build(BuildContext context) { + return StreamAvatarGroup( + size: size, + children: users.map( + (user) => StreamUserAvatar( + user: user, + showOnlineIndicator: false, + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_stack.dart b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_stack.dart new file mode 100644 index 0000000000..12b8ee48f9 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/avatar/stream_user_avatar_stack.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/components/avatar/stream_user_avatar.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// A widget that displays a stack of user avatars with overlap. +/// +/// [StreamUserAvatarStack] displays multiple user avatars in a horizontal +/// stack, with each avatar partially overlapping the previous one. This is +/// useful for showing participants in a conversation, group members, or +/// any collection of users in a compact space. +/// +/// The stack automatically handles: +/// - Displaying user avatars with deterministic colors +/// - Overflow handling with a badge showing remaining count +/// - Customizable overlap and maximum visible avatars +/// +/// {@tool snippet} +/// +/// Basic usage with a list of users: +/// +/// ```dart +/// StreamUserAvatarStack(users: participants) +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// +/// With custom max and size: +/// +/// ```dart +/// StreamUserAvatarStack( +/// users: participants, +/// max: 3, +/// size: StreamAvatarStackSize.xs, +/// ) +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// +/// With custom overlap: +/// +/// ```dart +/// StreamUserAvatarStack( +/// users: participants, +/// overlap: 0.5, // 50% overlap +/// ) +/// ``` +/// {@end-tool} +/// +/// ## Theming +/// +/// [StreamUserAvatarStack] uses [StreamAvatarThemeData] for default styling. +/// Individual user avatars use deterministic colors from +/// [StreamColorScheme.avatarPalette]. +/// +/// See also: +/// +/// * [StreamAvatarStackSize], which defines the available size variants. +/// * [StreamUserAvatar], which is used to display individual user avatars. +/// * [StreamAvatarStack], the underlying stack component. +class StreamUserAvatarStack extends StatelessWidget { + /// Creates a Stream user avatar stack. + /// + /// If [users] is empty, returns an empty [SizedBox]. + /// The [max] must be at least 2. + const StreamUserAvatarStack({ + super.key, + required this.users, + this.size, + this.overlap = 0.33, + this.max = 5, + }) : assert(max >= 2, 'max must be at least 2'); + + /// The list of users whose avatars are displayed. + final Iterable users; + + /// The size of the avatar stack. + /// + /// If null, defaults to [StreamAvatarStackSize.sm]. + final StreamAvatarStackSize? size; + + /// How much each avatar overlaps the previous one, as a fraction of size. + /// + /// - `0.0`: No overlap (side by side) + /// - `0.33`: 33% overlap (default) + /// - `1.0`: Fully stacked + final double overlap; + + /// Maximum number of avatars to display before showing overflow badge. + /// + /// When [users] exceeds this value, displays [max] avatars followed + /// by a badge showing the overflow count (e.g., "+2"). + /// + /// Must be at least 2. Defaults to 5. + final int max; + + @override + Widget build(BuildContext context) { + return StreamAvatarStack( + max: max, + size: size, + overlap: overlap, + children: users.map( + (user) => StreamUserAvatar( + user: user, + showOnlineIndicator: false, + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer.dart new file mode 100644 index 0000000000..08601961f1 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer.dart @@ -0,0 +1,4 @@ +export 'message_composer_component_props.dart'; +export 'message_composer_input_trailing.dart' show DefaultStreamMessageComposerInputTrailing; +export 'message_composer_leading.dart' show DefaultStreamMessageComposerLeading; +export 'stream_chat_message_composer.dart'; diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart new file mode 100644 index 0000000000..4df376246f --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_component_props.dart @@ -0,0 +1,277 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +/// Properties to build any of the sub-components. +/// These properties are all the same, so features such as 'add attachment', +/// can be added to any of the sub-components. +class MessageComposerComponentProps { + /// Creates a new instance of [MessageComposerComponentProps]. + /// [controller] is the controller for the message composer component. + /// [isFloating] is whether the message composer is floating. + /// [message] is the message for the message composer component. + /// [onSendPressed] is the callback for when the send button is pressed. + /// [onMicrophonePressed] is the callback for when the microphone button is pressed. + /// [onAttachmentButtonPressed] is the callback for when the attachment button is pressed. + /// [focusNode] is the focus node for the message composer component. + /// [currentUserId] is the current user id. + const MessageComposerComponentProps({ + required this.controller, + this.isFloating = false, + this.message, + required this.onSendPressed, + this.voiceRecordingCallback, + this.onAttachmentButtonPressed, + this.isPickerOpen = false, + this.focusNode, + this.currentUserId, + required this.audioRecorderState, + this.onQuotedMessageCleared, + }); + + /// The controller for the message composer component. + final StreamMessageInputController controller; + + /// Whether the message composer is floating. + final bool isFloating; + + /// The message for the message composer component. + final Message? message; + + /// The callback for when the send button is pressed. + final VoidCallback onSendPressed; + + /// The callback for when the microphone button is pressed. + final core.VoiceRecordingCallback? voiceRecordingCallback; + + /// The callback for when the attachment button is pressed. + final VoidCallback? onAttachmentButtonPressed; + + /// Whether the inline attachment picker is currently open. + final bool isPickerOpen; + + /// The focus node for the message composer component. + final FocusNode? focusNode; + + /// The current user id. + final String? currentUserId; + + /// Whether the audio recording flow is active. + final AudioRecorderState audioRecorderState; + + /// Callback for when the quoted message is cleared. + final VoidCallback? onQuotedMessageCleared; + + /// Whether the audio recording flow is active. + bool get isAudioRecordingFlowActive => audioRecorderState is RecordStateRecording || isAudioRecordingFlowStopped; + + /// Whether the audio recording flow is locked. + bool get isAudioRecordingFlowLocked => audioRecorderState is RecordStateRecordingLocked; + + /// Whether the audio recording flow is stopped. + bool get isAudioRecordingFlowStopped => audioRecorderState is RecordStateStopped; +} + +/// Properties for building the leading component of the message composer. +class MessageComposerLeadingProps extends MessageComposerComponentProps { + const MessageComposerLeadingProps._({ + required super.controller, + required super.isFloating, + required super.message, + required super.onSendPressed, + required super.voiceRecordingCallback, + required super.onAttachmentButtonPressed, + required super.isPickerOpen, + required super.focusNode, + required super.currentUserId, + required super.audioRecorderState, + required super.onQuotedMessageCleared, + }) : super(); + + /// Creates a new instance of [MessageComposerLeadingProps] from a [MessageComposerComponentProps]. + factory MessageComposerLeadingProps.from(MessageComposerComponentProps props) { + return MessageComposerLeadingProps._( + controller: props.controller, + isFloating: props.isFloating, + message: props.message, + onSendPressed: props.onSendPressed, + voiceRecordingCallback: props.voiceRecordingCallback, + onAttachmentButtonPressed: props.onAttachmentButtonPressed, + isPickerOpen: props.isPickerOpen, + focusNode: props.focusNode, + currentUserId: props.currentUserId, + audioRecorderState: props.audioRecorderState, + onQuotedMessageCleared: props.onQuotedMessageCleared, + ); + } +} + +/// Properties for building the trailing component of the message composer. +class MessageComposerTrailingProps extends MessageComposerComponentProps { + const MessageComposerTrailingProps._({ + required super.controller, + required super.isFloating, + required super.message, + required super.onSendPressed, + required super.voiceRecordingCallback, + required super.onAttachmentButtonPressed, + required super.isPickerOpen, + required super.focusNode, + required super.currentUserId, + required super.audioRecorderState, + required super.onQuotedMessageCleared, + }) : super(); + + /// Creates a new instance of [MessageComposerTrailingProps] from a [MessageComposerComponentProps]. + factory MessageComposerTrailingProps.from(MessageComposerComponentProps props) { + return MessageComposerTrailingProps._( + controller: props.controller, + isFloating: props.isFloating, + message: props.message, + onSendPressed: props.onSendPressed, + voiceRecordingCallback: props.voiceRecordingCallback, + onAttachmentButtonPressed: props.onAttachmentButtonPressed, + isPickerOpen: props.isPickerOpen, + focusNode: props.focusNode, + currentUserId: props.currentUserId, + audioRecorderState: props.audioRecorderState, + onQuotedMessageCleared: props.onQuotedMessageCleared, + ); + } +} + +/// Properties for building the input component of the message composer. +class MessageComposerInputProps extends MessageComposerComponentProps { + const MessageComposerInputProps._({ + required super.controller, + required super.isFloating, + required super.message, + required super.onSendPressed, + required super.voiceRecordingCallback, + required super.onAttachmentButtonPressed, + required super.isPickerOpen, + required super.focusNode, + required super.currentUserId, + required super.audioRecorderState, + required super.onQuotedMessageCleared, + }) : super(); + + /// Creates a new instance of [MessageComposerInputProps] from a [MessageComposerComponentProps]. + factory MessageComposerInputProps.from(MessageComposerComponentProps props) { + return MessageComposerInputProps._( + controller: props.controller, + isFloating: props.isFloating, + message: props.message, + onSendPressed: props.onSendPressed, + voiceRecordingCallback: props.voiceRecordingCallback, + onAttachmentButtonPressed: props.onAttachmentButtonPressed, + isPickerOpen: props.isPickerOpen, + focusNode: props.focusNode, + currentUserId: props.currentUserId, + audioRecorderState: props.audioRecorderState, + onQuotedMessageCleared: props.onQuotedMessageCleared, + ); + } +} + +/// Properties for building the input leading component of the message composer. +class MessageComposerInputLeadingProps extends MessageComposerComponentProps { + const MessageComposerInputLeadingProps._({ + required super.controller, + required super.isFloating, + required super.message, + required super.onSendPressed, + required super.voiceRecordingCallback, + required super.onAttachmentButtonPressed, + required super.isPickerOpen, + required super.focusNode, + required super.currentUserId, + required super.audioRecorderState, + required super.onQuotedMessageCleared, + }) : super(); + + /// Creates a new instance of [MessageComposerInputLeadingProps] from a [MessageComposerComponentProps]. + factory MessageComposerInputLeadingProps.from(MessageComposerComponentProps props) { + return MessageComposerInputLeadingProps._( + controller: props.controller, + isFloating: props.isFloating, + message: props.message, + onSendPressed: props.onSendPressed, + voiceRecordingCallback: props.voiceRecordingCallback, + onAttachmentButtonPressed: props.onAttachmentButtonPressed, + isPickerOpen: props.isPickerOpen, + focusNode: props.focusNode, + currentUserId: props.currentUserId, + audioRecorderState: props.audioRecorderState, + onQuotedMessageCleared: props.onQuotedMessageCleared, + ); + } +} + +/// Properties for building the input header component of the message composer. +class MessageComposerInputHeaderProps extends MessageComposerComponentProps { + const MessageComposerInputHeaderProps._({ + required super.controller, + required super.isFloating, + required super.message, + required super.onSendPressed, + required super.voiceRecordingCallback, + required super.onAttachmentButtonPressed, + required super.isPickerOpen, + required super.focusNode, + required super.currentUserId, + required super.audioRecorderState, + required super.onQuotedMessageCleared, + }) : super(); + + /// Creates a new instance of [MessageComposerInputHeaderProps] from a [MessageComposerComponentProps]. + factory MessageComposerInputHeaderProps.from(MessageComposerComponentProps props) { + return MessageComposerInputHeaderProps._( + controller: props.controller, + isFloating: props.isFloating, + message: props.message, + onSendPressed: props.onSendPressed, + voiceRecordingCallback: props.voiceRecordingCallback, + onAttachmentButtonPressed: props.onAttachmentButtonPressed, + isPickerOpen: props.isPickerOpen, + focusNode: props.focusNode, + currentUserId: props.currentUserId, + audioRecorderState: props.audioRecorderState, + onQuotedMessageCleared: props.onQuotedMessageCleared, + ); + } +} + +/// Properties for building the input trailing component of the message composer. +class MessageComposerInputTrailingProps extends MessageComposerComponentProps { + const MessageComposerInputTrailingProps._({ + required super.controller, + required super.isFloating, + required super.message, + required super.onSendPressed, + required super.voiceRecordingCallback, + required super.onAttachmentButtonPressed, + required super.isPickerOpen, + required super.focusNode, + required super.currentUserId, + required super.audioRecorderState, + required super.onQuotedMessageCleared, + }) : super(); + + /// Creates a new instance of [MessageComposerInputTrailingProps] from a [MessageComposerComponentProps]. + factory MessageComposerInputTrailingProps.from(MessageComposerComponentProps props) { + return MessageComposerInputTrailingProps._( + controller: props.controller, + isFloating: props.isFloating, + message: props.message, + onSendPressed: props.onSendPressed, + voiceRecordingCallback: props.voiceRecordingCallback, + onAttachmentButtonPressed: props.onAttachmentButtonPressed, + isPickerOpen: props.isPickerOpen, + focusNode: props.focusNode, + currentUserId: props.currentUserId, + audioRecorderState: props.audioRecorderState, + onQuotedMessageCleared: props.onQuotedMessageCleared, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_header.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_header.dart new file mode 100644 index 0000000000..303481dd23 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_header.dart @@ -0,0 +1,241 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// A widget that shows the input header of the message composer. +/// Uses the factory to show custom components or used the default implementation. +class StreamMessageComposerInputHeader extends StatelessWidget { + /// Creates a new instance of [StreamMessageComposerInputHeader]. + /// [props] contains the properties for the message composer component. + const StreamMessageComposerInputHeader({super.key, required this.props}); + + /// The properties for the message composer component. + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + return context.chatComponentBuilder()?.call( + context, + MessageComposerInputHeaderProps.from(props), + ) ?? + _DefaultStreamMessageComposerInputHeader(props: props); + } +} + +class _DefaultStreamMessageComposerInputHeader extends StatelessWidget { + const _DefaultStreamMessageComposerInputHeader({required this.props}); + + final MessageComposerComponentProps props; + StreamMessageInputController get controller => props.controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: props.controller, + builder: (context, _, __) => _buildContent(context), + ); + } + + Widget _buildContent(BuildContext context) { + final isEditing = !controller.message.state.isInitial; + final quotedMessage = !isEditing ? controller.message.quotedMessage : null; + final ogAttachment = controller.ogAttachment; + final nonOGAttachments = controller.attachments + .where((it) { + return it.titleLink == null && it.type != AttachmentType.voiceRecording; + }) + .toList(growable: false); + final voiceRecordings = controller.attachments + .where((it) { + return it.type == AttachmentType.voiceRecording; + }) + .toList(growable: false); + + final hasAttachments = nonOGAttachments.isNotEmpty; + final hasContent = + isEditing || quotedMessage != null || hasAttachments || ogAttachment != null || voiceRecordings.isNotEmpty; + + final spacing = context.streamSpacing; + final contentPadding = EdgeInsets.only( + left: spacing.xs, + right: spacing.xs, + ); + + return AnimatedSize( + duration: const Duration(milliseconds: 200), + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.only( + top: hasContent ? spacing.xs : 0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (isEditing) + Padding( + padding: contentPadding, + child: _EditMessageInHeader( + message: controller.editingOriginalMessage ?? controller.message, + onRemovePressed: controller.cancelEditMessage, + ), + ) + else if (quotedMessage != null) + Padding( + padding: contentPadding, + child: _QuotedMessageInHeader( + quotedMessage: quotedMessage, + onRemovePressed: () { + controller.clearQuotedMessage(); + props.onQuotedMessageCleared?.call(); + }, + currentUserId: props.currentUserId, + ), + ), + if (voiceRecordings.isNotEmpty) + Padding( + padding: contentPadding, + child: StreamVoiceRecordingAttachmentPlaylist( + voiceRecordings: voiceRecordings, + voiceRecordingTitle: 'Voice Message', + message: props.controller.message, + itemDecorator: (context, index, child) { + final attachment = voiceRecordings.elementAtOrNull(index); + if (attachment == null) return child; + + return StreamMessageComposerAttachmentContainer( + onRemovePressed: () => _onAttachmentRemovePressed(attachment), + child: child, + ); + }, + ), + ), + if (hasAttachments) + StreamMessageComposerAttachmentList( + attachments: nonOGAttachments, + onRemovePressed: _onAttachmentRemovePressed, + ), + if (ogAttachment != null) + Padding( + padding: contentPadding, + child: MessageComposerLinkPreviewAttachment( + title: ogAttachment.title, + subtitle: ogAttachment.text, + image: ogAttachment.imageUrl != null ? CachedNetworkImageProvider(ogAttachment.imageUrl!) : null, + url: ogAttachment.titleLink, + onRemovePressed: () { + controller.clearOGAttachment(); + props.focusNode?.unfocus(); + }, + ), + ), + ], + ), + ), + ); + } + + // Default callback for removing an attachment. + Future _onAttachmentRemovePressed(Attachment attachment) async { + final file = attachment.file; + final uploadState = attachment.uploadState; + + if (file != null && !uploadState.isSuccess && !isWeb) { + await StreamAttachmentHandler.instance.deleteAttachmentFile( + attachmentFile: file, + ); + } + + controller.removeAttachmentById(attachment.id); + } +} + +class _EditMessageInHeader extends StatelessWidget { + const _EditMessageInHeader({ + required this.message, + required this.onRemovePressed, + }); + + final Message message; + final VoidCallback onRemovePressed; + + @override + Widget build(BuildContext context) { + return MessageComposerReplyAttachment( + title: Text(context.translations.editMessageLabel), + subtitle: StreamMessagePreviewText(message: message), + onRemovePressed: onRemovePressed, + style: ReplyStyle.outgoing, + ); + } +} + +class _QuotedMessageInHeader extends StatelessWidget { + const _QuotedMessageInHeader({ + required this.quotedMessage, + required this.onRemovePressed, + required this.currentUserId, + }); + + final Message quotedMessage; + final VoidCallback onRemovePressed; + final String? currentUserId; + + ImageProvider? _imageProvider(Message message) { + final attachments = message.attachments; + if (attachments.isEmpty || attachments.length > 1) return null; + + final attachment = attachments.first; + if (attachment.type == AttachmentType.file) return null; + final imageUrl = attachment.imageUrl ?? attachment.thumbUrl ?? attachment.assetUrl; + + if (imageUrl == null) return null; + return CachedNetworkImageProvider(imageUrl); + } + + String? _mimeTypeAttachment(Message message) { + final attachments = message.attachments; + if (attachments.isEmpty) return null; + final attachment = attachments.first; + + if (attachment.type != AttachmentType.file) return null; + if (attachments.any((it) => it.mimeType != attachment.mimeType)) return null; + + return attachment.mimeType; + } + + @override + Widget build(BuildContext context) { + final isIncoming = currentUserId != quotedMessage.user?.id; + + final image = _imageProvider(quotedMessage); + final mimeType = _mimeTypeAttachment(quotedMessage); + + Widget? trailing; + if (image != null) { + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(context.streamRadius.md), + image: DecorationImage(image: image, fit: BoxFit.cover), + ), + ); + } else if (mimeType != null) { + trailing = StreamFileTypeIcon.fromMimeType(mimeType: mimeType); + } else { + trailing = null; + } + + return + // TODO: localize strings + MessageComposerReplyAttachment( + title: Text(isIncoming ? 'Reply to ${quotedMessage.user?.name}' : 'You'), + subtitle: StreamMessagePreviewText(message: quotedMessage), + onRemovePressed: onRemovePressed, + trailing: trailing, + style: isIncoming ? .incoming : .outgoing, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_leading.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_leading.dart new file mode 100644 index 0000000000..2399796aa3 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_leading.dart @@ -0,0 +1,22 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A widget that shows the input leading of the message composer. +/// Uses the factory to show custom components, but is empty by default. +class StreamMessageComposerInputLeading extends StatelessWidget { + /// Creates a new instance of [StreamMessageComposerInputLeading]. + /// [props] contains the properties for the message composer component. + const StreamMessageComposerInputLeading({super.key, required this.props}); + + /// The properties for the message composer component. + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + return context.chatComponentBuilder()?.call( + context, + MessageComposerInputLeadingProps.from(props), + ) ?? + const SizedBox.shrink(); + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart new file mode 100644 index 0000000000..c45f5a6cc3 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_input_trailing.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// A widget that shows the input trailing of the message composer. +/// Uses the factory to show custom components or the default implementation. +/// By default it shows the send button and the microphone button. +class StreamMessageComposerInputTrailing extends StatelessWidget { + /// Creates a new instance of [StreamMessageComposerInputTrailing]. + /// [props] contains the properties for the message composer component. + const StreamMessageComposerInputTrailing({super.key, required this.props}); + + /// The properties for the message composer component. + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + return context.chatComponentBuilder()?.call( + context, + MessageComposerInputTrailingProps.from(props), + ) ?? + DefaultStreamMessageComposerInputTrailing(props: props); + } +} + +/// Default implementation of the input trailing of the message composer. +/// Shows the send button or the microphone button based on the state of the message composer. +/// It shows no button when the audio recording flow is locked or stopped. +class DefaultStreamMessageComposerInputTrailing extends StatelessWidget { + /// Creates a new instance of [DefaultStreamMessageComposerInputTrailing]. + /// [props] contains the properties for the message composer component. + const DefaultStreamMessageComposerInputTrailing({super.key, required this.props}); + + /// The properties for the message composer component. + final MessageComposerComponentProps props; + + StreamMessageInputController get _controller => props.controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: _controller, + builder: (context, value, child) { + final hasText = _controller.text.trim().isNotEmpty; + final hasContent = hasText || _controller.attachments.isNotEmpty; + final isEditing = !_controller.message.state.isInitial; + final hasCommand = _controller.message.command != null; + var buttonState = StreamMessageComposerInputTrailingState.microphone; + if (props.isAudioRecordingFlowActive) { + buttonState = StreamMessageComposerInputTrailingState.voiceRecordingActive; + } + + if (isEditing) { + buttonState = StreamMessageComposerInputTrailingState.edit; + } else if (hasCommand) { + buttonState = StreamMessageComposerInputTrailingState.command; + } else if (hasContent) { + buttonState = StreamMessageComposerInputTrailingState.send; + } + + final isEnabled = (!isEditing && !hasCommand) || hasContent; + + return props.isAudioRecordingFlowLocked || props.isAudioRecordingFlowStopped + ? const SizedBox.shrink() + : StreamCoreMessageComposerInputTrailing( + controller: _controller.textFieldController, + onSendPressed: isEnabled ? props.onSendPressed : null, + voiceRecordingCallback: props.voiceRecordingCallback, + buttonState: buttonState, + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_leading.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_leading.dart new file mode 100644 index 0000000000..d51cd9e3c4 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_leading.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// A widget that shows the leading of the message composer. +/// Uses the factory to show custom components or the default implementation. +/// By default this contains a button to add attachments. +class StreamMessageComposerLeading extends StatelessWidget { + /// Creates a new instance of [StreamMessageComposerLeading]. + /// [props] contains the properties for the message composer component. + const StreamMessageComposerLeading({super.key, required this.props}); + + /// The properties for the message composer component. + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + return context.chatComponentBuilder()?.call( + context, + MessageComposerLeadingProps.from(props), + ) ?? + DefaultStreamMessageComposerLeading(props: props); + } +} + +/// Default implementation of the leading of the message composer. +/// Shows the attachment button when the message composer is not in audio recording flow and no command is selected. +class DefaultStreamMessageComposerLeading extends StatelessWidget { + /// Creates a new instance of [DefaultStreamMessageComposerLeading]. + /// [props] contains the properties for the message composer component. + const DefaultStreamMessageComposerLeading({super.key, required this.props}); + + /// The properties for the message composer component. + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + // 45 degrees = 0.125 turns + const closedRotation = 0.125; + final showButton = + props.onAttachmentButtonPressed != null && + !props.isAudioRecordingFlowActive && + props.controller.message.command == null; + + return AnimatedOpacity( + opacity: showButton ? 1.0 : 0.0, + duration: showButton ? const Duration(milliseconds: 200) : Duration.zero, + curve: Curves.easeInQuint, + child: AnimatedSize( + duration: const Duration(milliseconds: 200), + alignment: Alignment.bottomCenter, + child: Row( + children: [ + if (showButton) ...[ + AnimatedRotation( + turns: props.isPickerOpen ? closedRotation : 0, + duration: const Duration(milliseconds: 150), + curve: Curves.easeOut, + child: StreamButton.icon( + icon: context.streamIcons.plus20, + style: StreamButtonStyle.secondary, + type: StreamButtonType.outline, + size: StreamButtonSize.large, + isFloating: props.isFloating, + onTap: () { + props.onAttachmentButtonPressed?.call(); + }, + ), + ), + SizedBox(width: context.streamSpacing.xs), + ], + ], + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_recording_locked.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_recording_locked.dart new file mode 100644 index 0000000000..be4ee294d0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_recording_locked.dart @@ -0,0 +1,328 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/audio/audio_playlist_controller.dart'; +import 'package:stream_chat_flutter/src/audio/audio_playlist_state.dart'; +import 'package:stream_chat_flutter/src/audio/audio_sampling.dart' as sampling; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +const _kDefaultWaveformHeight = 20.0; +const _kDefaultWaveformLimit = 35; + +/// Widget to display the recording locked state. +/// This widget can be used inside of the [StreamBaseMessageComposer] instead of the default `inputBody`. +class MessageComposerRecordingLocked extends StatelessWidget { + /// Creates a new instance of [MessageComposerRecordingLocked]. + /// [audioRecorderController] is the controller for the audio recorder. + /// [feedback] is the feedback for the audio recorder. + /// [messageInputController] is the controller for the message input. + /// [sendMessageCallback] is the callback for when the message is sent automatically. + const MessageComposerRecordingLocked({ + super.key, + required this.audioRecorderController, + required this.feedback, + required this.messageInputController, + required this.sendMessageCallback, + required this.state, + }); + + /// The controller for the audio recorder. + final StreamAudioRecorderController audioRecorderController; + + /// The feedback for the audio recorder. + final AudioRecorderFeedback feedback; + + /// The controller for the message input. + final StreamMessageInputController messageInputController; + + /// The callback for when the message is sent automatically. + /// This callback should be null when the message is not supposed to be sent automatically. + final VoidCallback? sendMessageCallback; + + /// The state of the recording. + final RecordStateRecording state; + + @override + Widget build(BuildContext context) { + final icons = context.streamIcons; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + final spacing = context.streamSpacing; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + height: 48, + width: 48, + alignment: Alignment.center, + child: Icon( + icons.voice20, + color: context.streamColorScheme.accentError, + size: 20, + ), + ), + Text( + state.duration.toMinutesAndSeconds(), + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + fontFeatures: [const FontFeature.tabularFigures()], + ), + ), + Expanded( + child: Container( + height: _kDefaultWaveformHeight, + padding: EdgeInsets.symmetric(horizontal: spacing.md), + child: StreamAudioWaveform(waveform: state.waveform, limit: 50), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StreamButton.icon( + key: const ValueKey('cancel-record-button'), + style: StreamButtonStyle.secondary, + type: StreamButtonType.outline, + size: StreamButtonSize.small, + icon: icons.delete20, + onTap: audioRecorderController.cancelRecord, + ), + if (audioRecorderController.value is RecordStateRecording) + StreamButton.icon( + key: const ValueKey('stop-record-button'), + style: StreamButtonStyle.destructive, + type: StreamButtonType.outline, + size: StreamButtonSize.small, + icon: icons.stopFill20, + onTap: audioRecorderController.stopRecord, + ), + StreamButton.icon( + key: const ValueKey('finish-record-button'), + style: StreamButtonStyle.primary, + type: StreamButtonType.solid, + size: StreamButtonSize.small, + icon: icons.checkmark16, + onTap: () async { + await feedback.onRecordFinish(context); + final audio = await audioRecorderController.finishRecord(); + if (audio != null) { + messageInputController.addAttachment(audio); + } + + // Once the recording is finished, cancel the recorder. + audioRecorderController.cancelRecord(discardTrack: false); + + // Send the message if the user has enabled the option to + // send the voice recording automatically. + sendMessageCallback?.call(); + }, + ), + ], + ), + ], + ); + } +} + +/// Widget to display the recording stopped state. +/// This widget can be used inside of the [StreamBaseMessageComposer] instead of the default `inputBody`. +class MessageComposerRecordingStopped extends StatefulWidget { + /// Creates a new instance of [MessageComposerRecordingStopped]. + /// [audioRecorderController] is the controller for the audio recorder. + /// [feedback] is the feedback for the audio recorder. + /// [messageInputController] is the controller for the message input. + /// [sendMessageCallback] is the callback for when the message is sent automatically. + const MessageComposerRecordingStopped({ + super.key, + required this.audioRecorderController, + required this.feedback, + required this.messageInputController, + required this.sendMessageCallback, + required this.recordingState, + }); + + /// The controller for the audio recorder. + final StreamAudioRecorderController audioRecorderController; + + /// The feedback for the audio recorder. + final AudioRecorderFeedback feedback; + + /// The controller for the message input. + final StreamMessageInputController messageInputController; + + /// The callback for when the message is sent automatically. + /// This callback should be null when the message is not supposed to be sent automatically. + final VoidCallback? sendMessageCallback; + + /// The state of the recording. + final RecordStateStopped recordingState; + + Attachment get _audioRecording => recordingState.audioRecording; + + @override + State createState() => _MessageComposerRecordingStoppedState(); +} + +class _MessageComposerRecordingStoppedState extends State { + late final _controller = StreamAudioPlaylistController( + widget._audioRecording.toPlaylist(), + ); + + @override + void initState() { + super.initState(); + _controller.initialize(); + } + + @override + void didUpdateWidget( + covariant MessageComposerRecordingStopped oldWidget, + ) { + super.didUpdateWidget(oldWidget); + if (widget._audioRecording != widget._audioRecording) { + // If the playlist have changed, update the playlist. + _controller.updatePlaylist(widget._audioRecording.toPlaylist()); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final icons = context.streamIcons; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + const index = 0; + + final spacing = context.streamSpacing; + + return ValueListenableBuilder( + valueListenable: _controller, + builder: (context, state, child) { + final track = state.tracks.firstOrNull; + if (track == null) return const SizedBox.shrink(); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + AudioControlButton( + state: track.state, + style: StreamButtonStyle.secondary, + type: StreamButtonType.ghost, + size: StreamButtonSize.small, + onPlay: () { + if (state.currentIndex == index) { + // Play the track directly if it is already loaded. + _controller.play(); + } else { + // Otherwise, load the track first and then play it. + _controller.skipToItem(index); + } + }, + onPause: _controller.pause, + ), + AudioDurationText( + duration: track.duration, + position: track.position, + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + fontFeatures: [const FontFeature.tabularFigures()], + ), + ), + + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: spacing.md), + height: _kDefaultWaveformHeight, + child: StreamAudioWaveformSlider( + limit: _kDefaultWaveformLimit, + waveform: sampling.resampleWaveformData( + track.waveform, + _kDefaultWaveformLimit, + ), + progress: track.progress, + // Only allow seeking if the current track is the one being + // interacted with. + onChangeStart: (_) async { + if (state.currentIndex != index) return; + return _controller.pause(); + }, + onChangeEnd: (_) async { + if (state.currentIndex != index) return; + return _controller.play(); + }, + onChanged: (progress) async { + if (state.currentIndex != index) return; + + final duration = track.duration.inMicroseconds; + final seekPosition = (duration * progress).toInt(); + final seekDuration = Duration(microseconds: seekPosition); + + return _controller.seek(seekDuration); + }, + isActive: track.state != TrackState.idle, + ), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StreamButton.icon( + key: const ValueKey('cancel-record-button'), + style: StreamButtonStyle.secondary, + type: StreamButtonType.outline, + size: StreamButtonSize.small, + icon: icons.delete20, + onTap: widget.audioRecorderController.cancelRecord, + ), + if (widget.audioRecorderController.value is RecordStateRecording) + StreamButton.icon( + key: const ValueKey('stop-record-button'), + style: StreamButtonStyle.destructive, + type: StreamButtonType.outline, + size: StreamButtonSize.small, + icon: icons.stopFill20, + onTap: widget.audioRecorderController.stopRecord, + ), + StreamButton.icon( + key: const ValueKey('finish-record-button'), + style: StreamButtonStyle.primary, + type: StreamButtonType.solid, + size: StreamButtonSize.small, + icon: icons.checkmark16, + onTap: () async { + await widget.feedback.onRecordFinish(context); + final audio = await widget.audioRecorderController.finishRecord(); + if (audio != null) { + widget.messageInputController.addAttachment(audio); + } + + // Once the recording is finished, cancel the recorder. + widget.audioRecorderController.cancelRecord(discardTrack: false); + + // Send the message if the user has enabled the option to + // send the voice recording automatically. + widget.sendMessageCallback?.call(); + }, + ), + ], + ), + ], + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_recording_ongoing.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_recording_ongoing.dart new file mode 100644 index 0000000000..623a0f2622 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_recording_ongoing.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Widget to display the recording ongoing state. +/// This widget can be used inside of the [StreamBaseMessageComposer] instead of the default `inputBody`. +/// It shows a hint to slide to cancel the recording. +class StreamMessageComposerRecordingOngoing extends StatelessWidget { + /// Creates a new instance of [StreamMessageComposerRecordingOngoing]. + /// [audioRecorderController] is the controller for the audio recorder. + const StreamMessageComposerRecordingOngoing({super.key, required this.audioRecorderController}); + + /// The controller for the audio recorder. + final StreamAudioRecorderController audioRecorderController; + + @override + Widget build(BuildContext context) { + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + final icons = context.streamIcons; + + return ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 48, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 48, + width: 48, + alignment: Alignment.center, + child: Icon( + icons.voice20, + color: context.streamColorScheme.accentError, + size: 20, + ), + ), + ValueListenableBuilder( + valueListenable: audioRecorderController, + builder: (context, state, child) { + final duration = state is RecordStateRecording ? state.duration : Duration.zero; + return Text( + duration.toMinutesAndSeconds(), + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textPrimary, + fontFeatures: [const FontFeature.tabularFigures()], + ), + ); + }, + ), + const SizedBox(width: 23), + _GradientText( + 'Slide to cancel', + style: context.streamTextTheme.bodyDefault, + gradient: LinearGradient( + colors: [colorScheme.textPrimary, colorScheme.textTertiary], + ), + ), + SizedBox(width: context.streamSpacing.xxs), + Icon(icons.chevronLeft20, color: colorScheme.textTertiary, size: 20), + ], + ), + ); + } +} + +class _GradientText extends StatelessWidget { + const _GradientText( + this.text, { + required this.gradient, + this.style, + }); + + final String text; + final TextStyle? style; + final Gradient gradient; + + @override + Widget build(BuildContext context) { + return ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: (bounds) => gradient.createShader( + Rect.fromLTWH(0, 0, bounds.width, bounds.height), + ), + child: Text(text, style: style), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_trailing.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_trailing.dart new file mode 100644 index 0000000000..f5399ebc34 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/message_composer_trailing.dart @@ -0,0 +1,23 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A widget that shows the trailing of the message composer. +/// Uses the factory to show custom components or the default implementation. +/// By default this area is empty. +class StreamMessageComposerTrailing extends StatelessWidget { + /// Creates a new instance of [StreamMessageComposerTrailing]. + /// [props] contains the properties for the message composer component. + const StreamMessageComposerTrailing({super.key, required this.props}); + + /// The properties for the message composer component. + final MessageComposerComponentProps props; + + @override + Widget build(BuildContext context) { + return context.chatComponentBuilder()?.call( + context, + MessageComposerTrailingProps.from(props), + ) ?? + const SizedBox.shrink(); + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_composer.dart b/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_composer.dart new file mode 100644 index 0000000000..8e43d81fef --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/message_composer/stream_chat_message_composer.dart @@ -0,0 +1,434 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:stream_chat_flutter/src/components/message_composer/message_composer_input_header.dart'; +import 'package:stream_chat_flutter/src/components/message_composer/message_composer_input_leading.dart'; +import 'package:stream_chat_flutter/src/components/message_composer/message_composer_input_trailing.dart'; +import 'package:stream_chat_flutter/src/components/message_composer/message_composer_leading.dart'; +import 'package:stream_chat_flutter/src/components/message_composer/message_composer_recording_locked.dart'; +import 'package:stream_chat_flutter/src/components/message_composer/message_composer_recording_ongoing.dart'; +import 'package:stream_chat_flutter/src/components/message_composer/message_composer_trailing.dart'; +import 'package:stream_chat_flutter/src/message_input/dm_checkbox_list_tile.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +/// A widget that shows the message composer. +/// Uses the factory to show custom components or the default implementation. +class StreamChatMessageComposer extends StatefulWidget { + /// Creates a new instance of [StreamChatMessageComposer]. + /// [controller] is the controller for the message composer. + /// [onSendPressed] is the callback for when the send button is pressed. + /// [onMicrophonePressed] is the callback for when the microphone button is pressed. + /// [onAttachmentButtonPressed] is the callback for when the attachment button is pressed. + /// [focusNode] is the focus node for the message composer. + /// [currentUserId] is the current user id. + /// [placeholder] is the placeholder text of the message composer. + StreamChatMessageComposer({ + super.key, + StreamMessageInputController? controller, + required VoidCallback onSendPressed, + VoidCallback? onAttachmentButtonPressed, + bool isPickerOpen = false, + FocusNode? focusNode, + String? currentUserId, + String placeholder = '', + StreamAudioRecorderController? audioRecorderController, + bool sendVoiceRecordingAutomatically = false, + AudioRecorderFeedback feedback = const AudioRecorderFeedback(), + bool canAlsoSendToChannel = false, + VoidCallback? onQuotedMessageCleared, + TextInputAction? textInputAction, + TextInputType? keyboardType, + TextCapitalization textCapitalization = TextCapitalization.sentences, + bool autofocus = false, + bool autocorrect = true, + }) : props = MessageComposerProps( + controller: controller, + isFloating: false, + message: null, + onSendPressed: onSendPressed, + onAttachmentButtonPressed: onAttachmentButtonPressed, + isPickerOpen: isPickerOpen, + focusNode: focusNode, + currentUserId: currentUserId, + placeholder: placeholder, + audioRecorderController: audioRecorderController, + sendVoiceRecordingAutomatically: sendVoiceRecordingAutomatically, + feedback: feedback, + canAlsoSendToChannel: canAlsoSendToChannel, + onQuotedMessageCleared: onQuotedMessageCleared, + textInputAction: textInputAction, + keyboardType: keyboardType, + textCapitalization: textCapitalization, + autofocus: autofocus, + autocorrect: autocorrect, + ); + + /// The controller for the message composer. + StreamMessageInputController? get controller => props.controller; + + /// The properties for the message composer. + final MessageComposerProps props; + + @override + State createState() => _StreamChatMessageComposerState(); +} + +class _StreamChatMessageComposerState extends State { + late StreamMessageInputController _controller; + + @override + void initState() { + super.initState(); + _initController(); + } + + @override + void didUpdateWidget(StreamChatMessageComposer oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller) { + _disposeController(oldWidget); + _initController(); + } + } + + @override + void dispose() { + _disposeController(widget); + super.dispose(); + } + + void _initController() { + _controller = widget.controller ?? StreamMessageInputController(); + } + + void _disposeController(StreamChatMessageComposer widget) { + if (widget.controller == null) { + _controller.dispose(); + } + } + + @override + Widget build(BuildContext context) { + if (context.chatComponentBuilder()?.call(context, widget.props) case final messageComposer?) { + return messageComposer; + } + + final audioRecorderController = widget.props.audioRecorderController; + if (audioRecorderController == null) { + return DefaultStreamChatMessageComposer( + props: widget.props, + inputController: _controller, + ); + } + + return ValueListenableBuilder( + valueListenable: audioRecorderController, + builder: (context, state, _) { + final body = switch (state) { + RecordStateRecordingLocked() => MessageComposerRecordingLocked( + audioRecorderController: audioRecorderController, + feedback: widget.props.feedback, + messageInputController: _controller, + sendMessageCallback: widget.props.sendVoiceRecordingAutomatically ? widget.props.onSendPressed : null, + state: state, + ), + RecordStateStopped() => MessageComposerRecordingStopped( + audioRecorderController: audioRecorderController, + feedback: widget.props.feedback, + messageInputController: _controller, + sendMessageCallback: widget.props.sendVoiceRecordingAutomatically ? widget.props.onSendPressed : null, + recordingState: state, + ), + RecordStateRecording() => StreamMessageComposerRecordingOngoing( + audioRecorderController: audioRecorderController, + ), + _ => null, + }; + + final streamSpacing = context.streamSpacing; + + return PortalTarget( + anchor: Aligned( + offset: Offset(-streamSpacing.md, -streamSpacing.md), + target: Alignment.topRight, + follower: Alignment.bottomRight, + ), + visible: state is RecordStateRecording, + portalFollower: SwipeToLockButton(isLocked: state is RecordStateRecordingLocked), + child: DefaultStreamChatMessageComposer( + props: widget.props, + inputController: _controller, + audioRecorderState: state, + body: body, + ), + ); + }, + ); + } +} + +/// Properties to build the main message composer component +class MessageComposerProps { + /// Creates a new instance of [MessageComposerProps]. + /// [isFloating] is whether the message composer is floating. + /// [message] is the message for the message composer. + /// [placeholder] is the placeholder text of the message composer. + /// [onSendPressed] is the callback for when the send button is pressed. + /// [onMicrophonePressed] is the callback for when the microphone button is pressed. + /// [onAttachmentButtonPressed] is the callback for when the attachment button is pressed. + /// [focusNode] is the focus node for the message composer. + /// [currentUserId] is the current user id. + const MessageComposerProps({ + this.controller, + this.isFloating = false, + this.message, + this.placeholder = '', + required this.onSendPressed, + this.onAttachmentButtonPressed, + this.isPickerOpen = false, + this.focusNode, + this.currentUserId, + this.audioRecorderController, + this.sendVoiceRecordingAutomatically = false, + this.feedback = const AudioRecorderFeedback(), + this.canAlsoSendToChannel = false, + this.onQuotedMessageCleared, + this.textInputAction, + this.keyboardType, + this.textCapitalization = TextCapitalization.sentences, + this.autofocus = false, + this.autocorrect = true, + }); + + /// The controller for the message composer. + final StreamMessageInputController? controller; + + /// Whether the message composer is floating. + final bool isFloating; + + /// The message for the message composer. + final Message? message; + + /// The placeholder text of the message composer. + final String placeholder; + + /// The callback for when the send button is pressed. + final VoidCallback onSendPressed; + + /// The callback for when the attachment button is pressed. + final VoidCallback? onAttachmentButtonPressed; + + /// Whether the inline attachment picker is currently open. + final bool isPickerOpen; + + /// The focus node for the message composer. + final FocusNode? focusNode; + + /// The current user id. + final String? currentUserId; + + /// The audio recorder controller. + final StreamAudioRecorderController? audioRecorderController; + + /// Whether the voice recording should be sent automatically. + /// If enabled, the voice recording will be sent automatically when the recording is finished. + /// If disabled, the voice recording will be added as an attachment to the message + /// and the user will need to send the message manually. + final bool sendVoiceRecordingAutomatically; + + /// The feedback for the audio recorder. + final AudioRecorderFeedback feedback; + + /// Whether the user can also send the message as a direct message. + /// Usually used in threads. + final bool canAlsoSendToChannel; + + /// Callback for when the quoted message is cleared. + final VoidCallback? onQuotedMessageCleared; + + /// The type of action button to use for the keyboard. + final TextInputAction? textInputAction; + + /// The type of keyboard to use for editing the text. + final TextInputType? keyboardType; + + /// {@macro flutter.widgets.editableText.textCapitalization} + final TextCapitalization textCapitalization; + + /// Whether the text field should be focused initially. + final bool autofocus; + + /// Whether to enable autocorrect. + final bool autocorrect; +} + +extension on StreamAudioRecorderController { + bool get isRecording => value is RecordStateRecording; + bool get isLocked => isRecording && value is! RecordStateRecordingHold; +} + +/// Default implementation of the message composer. +/// Shows the message composer with the default components. +/// Does not include the audio recording flow in the body. +class DefaultStreamChatMessageComposer extends StatelessWidget { + /// Creates a new instance of [DefaultStreamChatMessageComposer]. + /// [props] contains the properties for the message composer. + /// [inputController] is the controller for the message input. + /// [audioRecorderState] is the state of the audio recorder. + /// [body] is the body of the message composer. + const DefaultStreamChatMessageComposer({ + super.key, + required this.props, + required this.inputController, + this.audioRecorderState = const RecordStateIdle(), + this.body, + }); + + /// The properties for the message composer. + final MessageComposerProps props; + + /// The controller for the message input. + final StreamMessageInputController inputController; + + /// The state of the audio recorder. + /// Used for the microphone button state. + final AudioRecorderState audioRecorderState; + + /// The body of the message composer. + final Widget? body; + + /// The threshold to lock the recording. + static const double _lockRecordThreshold = 50; + + /// The threshold to cancel the recording. + static const double _cancelRecordThreshold = 75; + + @override + Widget build(BuildContext context) { + final componentProps = MessageComposerComponentProps( + controller: inputController, + isFloating: props.isFloating, + message: props.message, + currentUserId: props.currentUserId, + onSendPressed: props.onSendPressed, + voiceRecordingCallback: _createVoiceRecordingCallback(context), + onAttachmentButtonPressed: props.onAttachmentButtonPressed, + isPickerOpen: props.isPickerOpen, + audioRecorderState: audioRecorderState, + focusNode: props.focusNode, + onQuotedMessageCleared: props.onQuotedMessageCleared, + ); + + return core.StreamCoreMessageComposer( + placeholder: props.placeholder, + controller: inputController.textFieldController, + isFloating: props.isFloating, + focusNode: props.focusNode, + composerLeading: StreamMessageComposerLeading(props: componentProps), + composerTrailing: StreamMessageComposerTrailing( + props: componentProps, + ), + inputHeader: StreamMessageComposerInputHeader(props: componentProps), + inputTrailing: StreamMessageComposerInputTrailing( + props: componentProps, + ), + inputLeading: StreamMessageComposerInputLeading( + props: componentProps, + ), + inputBody: + body ?? + Column( + mainAxisSize: MainAxisSize.min, + children: [ + core.StreamMessageComposerInputField( + controller: inputController.textFieldController, + placeholder: props.placeholder, + focusNode: props.focusNode, + command: inputController.message.command?.toUpperCase(), + onDismissCommand: inputController.clear, + textInputAction: props.textInputAction, + keyboardType: props.keyboardType, + textCapitalization: props.textCapitalization, + autofocus: props.autofocus, + autocorrect: props.autocorrect, + ), + if (props.canAlsoSendToChannel) + DmCheckboxListTile( + value: props.controller?.showInChannel ?? false, + // height of list tile is 34px, height of checkbox is 16px, so we need to subtract 8px to make the spacing correct. + contentPadding: EdgeInsets.only( + right: context.streamSpacing.md, + left: context.streamSpacing.md, + bottom: context.streamSpacing.md - 8, + ), + onChanged: (value) => props.controller?.showInChannel = value, + ), + ], + ), + ); + } + + core.VoiceRecordingCallback? _createVoiceRecordingCallback(BuildContext context) { + if (props.audioRecorderController case final audioRecorderController?) { + return core.VoiceRecordingCallback( + onLongPressStart: () async { + // Return if the recording is already started. + if (audioRecorderController.isRecording) return; + + await props.feedback.onRecordStart(context); + return audioRecorderController.startRecord(); + }, + onLongPressEnd: (_) async { + // Return if the recording not yet started or already locked. + if (!audioRecorderController.isRecording || audioRecorderController.isLocked) return; + + await props.feedback.onRecordFinish(context); + final audio = await audioRecorderController.finishRecord(); + if (audio != null) { + inputController.addAttachment(audio); + } + + // Once the recording is finished, cancel the recorder. + audioRecorderController.cancelRecord(discardTrack: false); + + // Send the message if the user has enabled the option to + // send the voice recording automatically. + if (props.sendVoiceRecordingAutomatically) { + return props.onSendPressed.call(); + } + }, + onLongPressCancel: () async { + // Return if the recording is already started. + if (audioRecorderController.isRecording) return; + + // Notify the parent that the recorder is canceled before it starts. + await props.feedback.onRecordStartCancel(context); + // Show a message to the user to hold to record. + audioRecorderController.showInfo( + context.translations.holdToRecordLabel, + ); + }, + onLongPressMoveUpdate: (details) async { + // Return if the recording not yet started or already locked. + if (!audioRecorderController.isRecording || audioRecorderController.isLocked) return; + final dragOffset = details.offsetFromOrigin; + + // Lock recording if the drag offset is greater than the threshold. + if (dragOffset.dy <= -_lockRecordThreshold) { + await props.feedback.onRecordLock(context); + return audioRecorderController.lockRecord(); + } + // Cancel recording if the drag offset is greater than the threshold. + if (dragOffset.dx <= -_cancelRecordThreshold) { + await props.feedback.onRecordCancel(context); + return audioRecorderController.cancelRecord(); + } + + // Update the drag offset. + return audioRecorderController.dragRecord(dragOffset); + }, + ); + } + return null; + } +} diff --git a/packages/stream_chat_flutter/lib/src/components/stream_chat_component_builders.dart b/packages/stream_chat_flutter/lib/src/components/stream_chat_component_builders.dart new file mode 100644 index 0000000000..3939652853 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/components/stream_chat_component_builders.dart @@ -0,0 +1,59 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Builds the list of component builders for the stream chat components. +Iterable> streamChatComponentBuilders({ + StreamComponentBuilder? channelListItem, + StreamComponentBuilder? threadListItem, + StreamComponentBuilder? messageComposer, + StreamComponentBuilder? messageComposerLeading, + StreamComponentBuilder? messageComposerTrailing, + StreamComponentBuilder? messageComposerInput, + StreamComponentBuilder? messageComposerInputLeading, + StreamComponentBuilder? messageComposerInputHeader, + StreamComponentBuilder? messageComposerInputTrailing, + StreamComponentBuilder? messageWidget, + StreamComponentBuilder? unreadIndicator, + StreamComponentBuilder? messageComposerAttachmentList, + StreamComponentBuilder? messageComposerAttachment, + StreamComponentBuilder? imageAttachment, + StreamComponentBuilder? videoAttachment, + StreamComponentBuilder? giphyAttachment, + StreamComponentBuilder? galleryAttachment, + StreamComponentBuilder? fileAttachment, + StreamComponentBuilder? linkPreviewAttachment, + StreamComponentBuilder? voiceRecordingAttachment, + StreamComponentBuilder? pollAttachment, +}) { + final builders = [ + if (channelListItem != null) StreamComponentBuilderExtension(builder: channelListItem), + if (threadListItem != null) StreamComponentBuilderExtension(builder: threadListItem), + if (messageComposer != null) StreamComponentBuilderExtension(builder: messageComposer), + if (messageComposerLeading != null) StreamComponentBuilderExtension(builder: messageComposerLeading), + if (messageComposerTrailing != null) StreamComponentBuilderExtension(builder: messageComposerTrailing), + if (messageComposerInput != null) StreamComponentBuilderExtension(builder: messageComposerInput), + if (messageComposerInputLeading != null) StreamComponentBuilderExtension(builder: messageComposerInputLeading), + if (messageComposerInputHeader != null) StreamComponentBuilderExtension(builder: messageComposerInputHeader), + if (messageComposerInputTrailing != null) StreamComponentBuilderExtension(builder: messageComposerInputTrailing), + if (messageWidget != null) StreamComponentBuilderExtension(builder: messageWidget), + if (unreadIndicator != null) StreamComponentBuilderExtension(builder: unreadIndicator), + if (messageComposerAttachmentList != null) StreamComponentBuilderExtension(builder: messageComposerAttachmentList), + if (messageComposerAttachment != null) StreamComponentBuilderExtension(builder: messageComposerAttachment), + if (imageAttachment != null) StreamComponentBuilderExtension(builder: imageAttachment), + if (videoAttachment != null) StreamComponentBuilderExtension(builder: videoAttachment), + if (giphyAttachment != null) StreamComponentBuilderExtension(builder: giphyAttachment), + if (galleryAttachment != null) StreamComponentBuilderExtension(builder: galleryAttachment), + if (fileAttachment != null) StreamComponentBuilderExtension(builder: fileAttachment), + if (linkPreviewAttachment != null) StreamComponentBuilderExtension(builder: linkPreviewAttachment), + if (voiceRecordingAttachment != null) StreamComponentBuilderExtension(builder: voiceRecordingAttachment), + if (pollAttachment != null) StreamComponentBuilderExtension(builder: pollAttachment), + ]; + + return builders; +} + +/// Helper extensions for the factory builders. +extension StreamChatComponentBuildersExtension on BuildContext { + /// The builder for the given component type. + StreamComponentBuilder? chatComponentBuilder() => StreamComponentFactory.of(this).extension(); +} diff --git a/packages/stream_chat_flutter/lib/src/context_menu/context_menu.dart b/packages/stream_chat_flutter/lib/src/context_menu/context_menu.dart index 12b9e68e25..2158b77e31 100644 --- a/packages/stream_chat_flutter/lib/src/context_menu/context_menu.dart +++ b/packages/stream_chat_flutter/lib/src/context_menu/context_menu.dart @@ -1,16 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; const double _kContextMenuScreenPadding = 8; -const double _kContextMenuWidth = 222; -/// Signature for a builder function that wraps the context menu widget. +/// Signature for a builder function that wraps the context menu items. /// /// This builder can be used to customize the appearance of the menu -/// container by wrapping [child] in additional UI elements. -typedef ContextMenuBuilder = Widget Function( - BuildContext context, - Widget child, -); +/// container by wrapping [menuItems] in additional UI elements. +typedef ContextMenuContainerBuilder = + Widget Function( + BuildContext context, + List menuItems, + ); /// A widget that displays a context menu anchored to a specific [Offset]. /// @@ -41,35 +42,20 @@ class ContextMenu extends StatelessWidget { /// Builds the outer container for the menu. /// - /// The [menuBuilder] receives the current context and a [child] widget - /// containing all the [menuItems]. + /// The [menuBuilder] receives the current context and the [menuItems] list, + /// and should return a widget wrapping all items. /// - /// Defaults to a card-style scrollable container with fixed width. - final ContextMenuBuilder menuBuilder; + /// Defaults to a [StreamContextMenu] wrapping all items. + final ContextMenuContainerBuilder menuBuilder; /// Default menu container with standard styling. /// /// Wraps the menu content in a card-like [Material] with scroll support, /// applying max width and height constraints. - static Widget _defaultMenuBuilder(BuildContext context, Widget child) { - final availableHeight = MediaQuery.of(context).size.height; - final maxHeight = availableHeight - _kContextMenuScreenPadding * 2; - - return ConstrainedBox( - constraints: BoxConstraints( - minWidth: _kContextMenuWidth, - maxWidth: _kContextMenuWidth, - maxHeight: maxHeight, - ), - child: Material( - elevation: 1, - type: MaterialType.card, - clipBehavior: Clip.antiAlias, - borderRadius: const BorderRadius.all(Radius.circular(7)), - child: SingleChildScrollView(child: child), - ), - ); - } + static Widget _defaultMenuBuilder( + BuildContext context, + List menuItems, + ) => StreamContextMenu(children: menuItems); @override Widget build(BuildContext context) { @@ -90,14 +76,7 @@ class ContextMenu extends StatelessWidget { delegate: DesktopTextSelectionToolbarLayoutDelegate( anchor: anchor - localAdjustment, ), - child: menuBuilder.call( - context, - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: menuItems, - ), - ), + child: menuBuilder.call(context, menuItems), ), ); } diff --git a/packages/stream_chat_flutter/lib/src/context_menu/context_menu_region.dart b/packages/stream_chat_flutter/lib/src/context_menu/context_menu_region.dart index d3554889dc..9a30e03e3a 100644 --- a/packages/stream_chat_flutter/lib/src/context_menu/context_menu_region.dart +++ b/packages/stream_chat_flutter/lib/src/context_menu/context_menu_region.dart @@ -6,14 +6,15 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// /// The function receives the [BuildContext] and the [Offset] where /// the menu should appear. -typedef ContextMenuBuilder = Widget Function( - BuildContext context, - Offset offset, -); +typedef ContextMenuBuilder = + Widget Function( + BuildContext context, + Offset offset, + ); /// Displays a custom context menu as a general dialog. /// -/// The [contextMenuBuilder] is used to construct the contents of the +/// The [menuBuilder] is used to construct the contents of the /// context menu, typically positioned based on the triggering gesture. /// /// The dialog can be customized using parameters such as [barrierColor], @@ -22,7 +23,7 @@ typedef ContextMenuBuilder = Widget Function( /// Returns a [Future] that resolves when the menu is dismissed. Future showContextMenu({ required BuildContext context, - required WidgetBuilder contextMenuBuilder, + required WidgetBuilder menuBuilder, String? barrierLabel, Color? barrierColor, Duration transitionDuration = const Duration(milliseconds: 150), @@ -59,7 +60,7 @@ Future showContextMenu({ ); }, pageBuilder: (context, animation, secondaryAnimation) { - final pageChild = Builder(builder: contextMenuBuilder); + final pageChild = Builder(builder: menuBuilder); return capturedThemes.wrap(pageChild); }, ); @@ -73,11 +74,12 @@ class ContextMenuRegion extends StatefulWidget { /// Creates a [ContextMenuRegion]. /// /// The [child] is the widget wrapped by this region. When a gesture is - /// detected on it, the [contextMenuBuilder] is used to construct the menu. + /// detected on it, the [menuBuilder] is used to construct the menu. const ContextMenuRegion({ super.key, required this.child, - required this.contextMenuBuilder, + required this.menuBuilder, + this.onSelected, }); /// The widget below this widget in the tree. @@ -86,7 +88,14 @@ class ContextMenuRegion extends StatefulWidget { /// Called to build the context menu when the gesture is triggered. /// /// The builder is given the [BuildContext] and the [Offset] of the gesture. - final ContextMenuBuilder contextMenuBuilder; + final ContextMenuBuilder menuBuilder; + + /// Called with the value returned when the context menu is dismissed. + /// + /// When a menu item pops the route with a value (e.g. via + /// [Navigator.pop]), that value is forwarded here. If the menu is dismissed + /// without a selection the value will be `null`. + final ValueChanged? onSelected; @override State createState() => _ContextMenuRegionState(); @@ -107,14 +116,16 @@ class _ContextMenuRegionState extends State { super.dispose(); } - Future _showContextMenu(BuildContext context, Offset position) async { - print('ContextMenuRegion: Showing context menu at $position'); - await showContextMenu( + Future _showContextMenu( + BuildContext context, + Offset position, + ) async { + final result = await showContextMenu( context: context, - contextMenuBuilder: (context) { - return widget.contextMenuBuilder(context, position); - }, + menuBuilder: (context) => widget.menuBuilder(context, position), ); + + return widget.onSelected?.call(result); } @override 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/dialogs/channel_info_dialog.dart b/packages/stream_chat_flutter/lib/src/dialogs/channel_info_dialog.dart index 974a1c2fc6..dc1991b74c 100644 --- a/packages/stream_chat_flutter/lib/src/dialogs/channel_info_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/dialogs/channel_info_dialog.dart @@ -39,9 +39,7 @@ class ChannelInfoDialog extends StatelessWidget { children: [ StreamChannelInfo( channel: channel, - textStyle: StreamChatTheme.of(context) - .channelPreviewTheme - .subtitleStyle, + textStyle: StreamChatTheme.of(context).channelPreviewTheme.subtitleStyle, ), ], ), @@ -50,18 +48,12 @@ class ChannelInfoDialog extends StatelessWidget { Column( children: [ StreamUserAvatar( + size: .xl, user: members .firstWhere( (e) => e.user?.id != userAsMember.user?.id, ) .user!, - constraints: const BoxConstraints.tightFor( - height: 64, - width: 64, - ), - borderRadius: BorderRadius.circular(32), - onlineIndicatorConstraints: - BoxConstraints.tight(const Size(12, 12)), ), const SizedBox(height: 6), Text( diff --git a/packages/stream_chat_flutter/lib/src/dialogs/message_dialog.dart b/packages/stream_chat_flutter/lib/src/dialogs/message_dialog.dart index f8b9930c43..24daa59d48 100644 --- a/packages/stream_chat_flutter/lib/src/dialogs/message_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/dialogs/message_dialog.dart @@ -33,8 +33,7 @@ class MessageDialog extends StatelessWidget { title: Text(titleText ?? context.translations.somethingWentWrongError), content: messageText != null ? Text( - messageText ?? - context.translations.operationCouldNotBeCompletedText, + messageText ?? context.translations.operationCouldNotBeCompletedText, ) : null, actions: [ diff --git a/packages/stream_chat_flutter/lib/src/fullscreen_media/fsm_stub.dart b/packages/stream_chat_flutter/lib/src/fullscreen_media/fsm_stub.dart index 1bf7c435b3..1dd6ac10c3 100644 --- a/packages/stream_chat_flutter/lib/src/fullscreen_media/fsm_stub.dart +++ b/packages/stream_chat_flutter/lib/src/fullscreen_media/fsm_stub.dart @@ -15,5 +15,4 @@ FullScreenMediaWidget getFsm({ ReplyMessageCallback? onReplyMessage, AttachmentActionsBuilder? attachmentActionsModalBuilder, bool? autoplayVideos, -}) => - throw UnsupportedError('Cannot create FullScreenMedia'); +}) => throw UnsupportedError('Cannot create FullScreenMedia'); diff --git a/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media.dart index eb95489c24..b9c72ceb45 100644 --- a/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media.dart @@ -81,17 +81,16 @@ class _FullScreenMediaState extends State { return; } - final currentAttachment = - widget.mediaAttachmentPackages[widget.startIndex].attachment; + final currentAttachment = widget.mediaAttachmentPackages[widget.startIndex].attachment; - await Future.wait(videoPackages.values.map( - (it) => it.initialize(), - )); + await Future.wait( + videoPackages.values.map( + (it) => it.initialize(), + ), + ); - if (widget.autoplayVideos && - currentAttachment.type == AttachmentType.video) { - final package = videoPackages.values - .firstWhere((e) => e._attachment == currentAttachment); + if (widget.autoplayVideos && currentAttachment.type == AttachmentType.video) { + final package = videoPackages.values.firstWhere((e) => e._attachment == currentAttachment); package._chewieController?.play(); } setState(() {}); // ignore: no-empty-block @@ -115,8 +114,7 @@ class _FullScreenMediaState extends State { body: ValueListenableBuilder( valueListenable: _currentPage, builder: (context, currentPage, child) { - final _currentAttachmentPackage = - widget.mediaAttachmentPackages[currentPage]; + final _currentAttachmentPackage = widget.mediaAttachmentPackages[currentPage]; final _currentMessage = _currentAttachmentPackage.message; final _currentAttachment = _currentAttachmentPackage.attachment; return Stack( @@ -130,8 +128,7 @@ class _FullScreenMediaState extends State { return AnimatedPositionedDirectional( duration: kThemeAnimationDuration, curve: Curves.easeInOut, - top: - isDisplayingDetail ? 0 : -(topPadding + kToolbarHeight), + top: isDisplayingDetail ? 0 : -(topPadding + kToolbarHeight), start: 0, end: 0, height: topPadding + kToolbarHeight, @@ -163,8 +160,7 @@ class _FullScreenMediaState extends State { ); } : null, - attachmentActionsModalBuilder: - widget.attachmentActionsModalBuilder, + attachmentActionsModalBuilder: widget.attachmentActionsModalBuilder, ), ); }, @@ -178,9 +174,7 @@ class _FullScreenMediaState extends State { return AnimatedPositionedDirectional( duration: kThemeAnimationDuration, curve: Curves.easeInOut, - bottom: isDisplayingDetail - ? 0 - : -(bottomPadding + kToolbarHeight), + bottom: isDisplayingDetail ? 0 : -(bottomPadding + kToolbarHeight), start: 0, end: 0, height: bottomPadding + kToolbarHeight, @@ -246,8 +240,7 @@ class _FullScreenMediaState extends State { } }, onRightArrowKeypress: () { - if (_currentPage.value < - widget.mediaAttachmentPackages.length - 1) { + if (_currentPage.value < widget.mediaAttachmentPackages.length - 1) { _currentPage.value++; _pageController.nextPage( duration: const Duration(milliseconds: 300), @@ -261,22 +254,19 @@ class _FullScreenMediaState extends State { onPageChanged: (val) { _currentPage.value = val; if (videoPackages.isEmpty) return; - final currentAttachment = - widget.mediaAttachmentPackages[val].attachment; + final currentAttachment = widget.mediaAttachmentPackages[val].attachment; for (final e in videoPackages.values) { if (e._attachment != currentAttachment) { e._chewieController?.pause(); } } - if (widget.autoplayVideos && - currentAttachment.type == AttachmentType.video) { + if (widget.autoplayVideos && currentAttachment.type == AttachmentType.video) { final controller = videoPackages[currentAttachment.id]!; controller._chewieController?.play(); } }, itemBuilder: (context, index) { - final currentAttachmentPackage = - widget.mediaAttachmentPackages[index]; + final currentAttachmentPackage = widget.mediaAttachmentPackages[index]; final attachment = currentAttachmentPackage.attachment; return ValueListenableBuilder( valueListenable: _isDisplayingDetail, @@ -298,8 +288,7 @@ class _FullScreenMediaState extends State { }, child: Builder( builder: (context) { - if (attachment.type == AttachmentType.image || - attachment.type == AttachmentType.giphy) { + if (attachment.type == AttachmentType.image || attachment.type == AttachmentType.giphy) { return PhotoView.customChild( maxScale: PhotoViewComputedScale.covered, minScale: PhotoViewComputedScale.contained, @@ -345,15 +334,15 @@ class VideoPackage { this._attachment, { bool showControls = false, bool autoInitialize = true, - }) : _showControls = showControls, - _autoInitialize = autoInitialize, - _videoPlayerController = _attachment.localUri != null - ? VideoPlayerController.file( - File.fromUri(_attachment.localUri!), - ) - : VideoPlayerController.networkUrl( - Uri.parse(_attachment.assetUrl!), - ); + }) : _showControls = showControls, + _autoInitialize = autoInitialize, + _videoPlayerController = _attachment.localUri != null + ? VideoPlayerController.file( + File.fromUri(_attachment.localUri!), + ) + : VideoPlayerController.networkUrl( + Uri.parse(_attachment.assetUrl!), + ); final Attachment _attachment; final bool _showControls; @@ -384,12 +373,10 @@ class VideoPackage { } /// Add a listener to video player controller - void addListener(VoidCallback listener) => - _videoPlayerController.addListener(listener); + void addListener(VoidCallback listener) => _videoPlayerController.addListener(listener); /// Remove a listener to video player controller - void removeListener(VoidCallback listener) => - _videoPlayerController.removeListener(listener); + void removeListener(VoidCallback listener) => _videoPlayerController.removeListener(listener); /// Dispose controllers Future dispose() { diff --git a/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_builder.dart b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_builder.dart index f1919db192..1609445cd8 100644 --- a/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_builder.dart +++ b/packages/stream_chat_flutter/lib/src/fullscreen_media/full_screen_media_builder.dart @@ -1,7 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/fullscreen_media/fsm_stub.dart' - if (dart.library.io) 'full_screen_media_desktop.dart' as desktop_fsm; + if (dart.library.io) 'full_screen_media_desktop.dart' + as desktop_fsm; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@template fsmBuilder} 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 3e04fc5e7d..c1fb7ac002 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'; @@ -110,8 +109,7 @@ class _FullScreenMediaDesktopState extends State { @override Widget build(BuildContext context) { - final containsOnlyVideos = - widget.mediaAttachmentPackages.length == videoPackages.length; + final containsOnlyVideos = widget.mediaAttachmentPackages.length == videoPackages.length; return Scaffold( resizeToAvoidBottomInset: false, @@ -123,13 +121,14 @@ class _FullScreenMediaDesktopState extends State { return Stack( children: [ ContextMenuRegion( - contextMenuBuilder: (_, anchor) { + menuBuilder: (context, anchor) { + final index = _currentPage.value; + final mediaAttachment = widget.mediaAttachmentPackages[index]; return ContextMenu( anchor: anchor, menuItems: [ - DownloadMenuItem( - attachment: widget - .mediaAttachmentPackages[_currentPage.value].attachment, + _DownloadMenuItem( + mediaAttachment: mediaAttachment.attachment, ), ], ); @@ -149,9 +148,9 @@ class _FullScreenMediaDesktopState extends State { videoPackages.values.first.player.stop(); Navigator.of(context).pop(); }, - child: const StreamSvgIcon( + child: Icon( + context.streamIcons.xmark32, size: 30, - icon: StreamSvgIcons.close, ), ), ), @@ -164,8 +163,7 @@ class _FullScreenMediaDesktopState extends State { return ValueListenableBuilder( valueListenable: _currentPage, builder: (context, currentPage, child) { - final _currentAttachmentPackage = - widget.mediaAttachmentPackages[currentPage]; + final _currentAttachmentPackage = widget.mediaAttachmentPackages[currentPage]; final _currentMessage = _currentAttachmentPackage.message; final _currentAttachment = _currentAttachmentPackage.attachment; return Stack( @@ -198,8 +196,7 @@ class _FullScreenMediaDesktopState extends State { StreamChannel.of(context).channel, ); }, - attachmentActionsModalBuilder: - widget.attachmentActionsModalBuilder, + attachmentActionsModalBuilder: widget.attachmentActionsModalBuilder, ), ); }, @@ -213,9 +210,7 @@ class _FullScreenMediaDesktopState extends State { return AnimatedPositionedDirectional( duration: kThemeAnimationDuration, curve: Curves.easeInOut, - bottom: isDisplayingDetail - ? 0 - : -(bottomPadding + kToolbarHeight), + bottom: isDisplayingDetail ? 0 : -(bottomPadding + kToolbarHeight), start: 0, end: 0, height: bottomPadding + kToolbarHeight, @@ -281,8 +276,7 @@ class _FullScreenMediaDesktopState extends State { } }, onRightArrowKeypress: () { - if (_currentPage.value < - widget.mediaAttachmentPackages.length - 1) { + if (_currentPage.value < widget.mediaAttachmentPackages.length - 1) { _currentPage.value++; _pageController.nextPage( duration: const Duration(milliseconds: 300), @@ -296,22 +290,19 @@ class _FullScreenMediaDesktopState extends State { onPageChanged: (val) { _currentPage.value = val; if (videoPackages.isEmpty) return; - final currentAttachment = - widget.mediaAttachmentPackages[val].attachment; + final currentAttachment = widget.mediaAttachmentPackages[val].attachment; for (final p in videoPackages.values) { if (p.attachment != currentAttachment) { p.player.pause(); } } - if (widget.autoplayVideos && - currentAttachment.type == AttachmentType.video) { + if (widget.autoplayVideos && currentAttachment.type == AttachmentType.video) { final package = videoPackages[currentAttachment.id]!; package.player.play(); } }, itemBuilder: (context, index) { - final currentAttachmentPackage = - widget.mediaAttachmentPackages[index]; + final currentAttachmentPackage = widget.mediaAttachmentPackages[index]; final attachment = currentAttachmentPackage.attachment; return ValueListenableBuilder( @@ -334,8 +325,7 @@ class _FullScreenMediaDesktopState extends State { }, child: Builder( builder: (context) { - if (attachment.type == AttachmentType.image || - attachment.type == AttachmentType.giphy) { + if (attachment.type == AttachmentType.image || attachment.type == AttachmentType.giphy) { return PhotoView.customChild( maxScale: PhotoViewComputedScale.covered, minScale: PhotoViewComputedScale.contained, @@ -362,16 +352,14 @@ class _FullScreenMediaDesktopState extends State { } return ContextMenuRegion( - contextMenuBuilder: (_, anchor) { - return ContextMenu( - anchor: anchor, - menuItems: [ - DownloadMenuItem( - attachment: attachment, - ), - ], - ); - }, + menuBuilder: (_, anchor) => ContextMenu( + anchor: anchor, + menuItems: [ + _DownloadMenuItem( + mediaAttachment: currentAttachmentPackage.attachment, + ), + ], + ), child: Video( controller: package.controller, ), @@ -390,6 +378,37 @@ 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 [StreamContextMenuAction] to stay consistent with 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 Attachment mediaAttachment; + + @override + Widget build(BuildContext context) { + final icons = context.streamIcons; + return StreamContextMenuAction( + leading: Icon(icons.arrowDown20), + label: Text(context.translations.downloadLabel), + onTap: () { + final handler = StreamAttachmentHandler.instance; + return handler.downloadAttachment(mediaAttachment).ignore(); + }, + ); + } +} + /// Class for packaging up things required for videos class DesktopVideoPackage { /// Constructor for creating [VideoPackage] diff --git a/packages/stream_chat_flutter/lib/src/gallery/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery/gallery_footer.dart index ffcfef6cd2..07f723f1ef 100644 --- a/packages/stream_chat_flutter/lib/src/gallery/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery/gallery_footer.dart @@ -9,8 +9,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@template streamGalleryFooter} /// Footer widget for media display /// {@endtemplate} -class StreamGalleryFooter extends StatefulWidget - implements PreferredSizeWidget { +class StreamGalleryFooter extends StatefulWidget implements PreferredSizeWidget { /// {@macro streamGalleryFooter} const StreamGalleryFooter({ super.key, @@ -73,10 +72,8 @@ class _StreamGalleryFooterState extends State { context: context, removeTop: true, child: BottomAppBar( - surfaceTintColor: - widget.backgroundColor ?? galleryFooterThemeData.backgroundColor, - color: - widget.backgroundColor ?? galleryFooterThemeData.backgroundColor, + surfaceTintColor: widget.backgroundColor ?? galleryFooterThemeData.backgroundColor, + color: widget.backgroundColor ?? galleryFooterThemeData.backgroundColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -85,34 +82,26 @@ class _StreamGalleryFooterState extends State { else IconButton( key: shareButtonKey, - icon: StreamSvgIcon( + icon: Icon( + context.streamIcons.export20, size: 24, - icon: StreamSvgIcons.share, color: galleryFooterThemeData.shareIconColor, ), onPressed: () async { - final attachment = widget - .mediaAttachmentPackages[widget.currentPage].attachment; - final url = attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl!; - final type = attachment.type == AttachmentType.image - ? 'jpg' - : url.split('?').first.split('.').last; + final attachment = widget.mediaAttachmentPackages[widget.currentPage].attachment; + final url = attachment.imageUrl ?? attachment.assetUrl ?? attachment.thumbUrl!; + final type = attachment.type == AttachmentType.image ? 'jpg' : url.split('?').first.split('.').last; final request = await HttpClient().getUrl(Uri.parse(url)); final response = await request.close(); - final bytes = - await consolidateHttpClientResponseBytes(response); + final bytes = await consolidateHttpClientResponseBytes(response); final tmpPath = await getTemporaryDirectory(); final filePath = '${tmpPath.path}/${attachment.id}.$type'; final file = File(filePath); await file.writeAsBytes(bytes); - final box = - shareButtonKey.currentContext?.findRenderObject(); + final box = shareButtonKey.currentContext?.findRenderObject(); final size = shareButtonKey.currentContext?.size; - final position = - (box! as RenderBox).localToGlobal(Offset.zero); + final position = (box! as RenderBox).localToGlobal(Offset.zero); await SharePlus.instance.share( ShareParams( @@ -146,8 +135,8 @@ class _StreamGalleryFooterState extends State { ), ), IconButton( - icon: StreamSvgIcon( - icon: StreamSvgIcons.grid, + icon: Icon( + context.streamIcons.gallery20, color: galleryFooterThemeData.gridIconButtonColor, ), onPressed: () => _showPhotosModal(context), @@ -176,8 +165,7 @@ class _StreamGalleryFooterState extends State { builder: (context) { return DraggableScrollableSheet( expand: false, - initialChildSize: - (CurrentPlatform.isAndroid || CurrentPlatform.isIos) ? 0.3 : 0.5, + initialChildSize: (CurrentPlatform.isAndroid || CurrentPlatform.isIos) ? 0.3 : 0.5, minChildSize: 0.3, maxChildSize: 0.7, builder: (context, scrollController) => Column( @@ -189,16 +177,15 @@ class _StreamGalleryFooterState extends State { padding: const EdgeInsets.all(16), child: Text( context.translations.photosLabel, - style: - galleryFooterThemeData.bottomSheetPhotosTextStyle, + style: galleryFooterThemeData.bottomSheetPhotosTextStyle, ), ), ), Align( alignment: Alignment.centerRight, child: IconButton( - icon: StreamSvgIcon( - icon: StreamSvgIcons.close, + icon: Icon( + context.streamIcons.xmark20, color: galleryFooterThemeData.bottomSheetCloseIconColor, ), onPressed: () => Navigator.of(context).maybePop(), @@ -219,8 +206,7 @@ class _StreamGalleryFooterState extends State { ), itemBuilder: (context, index) { Widget media; - final attachmentPackage = - widget.mediaAttachmentPackages[index]; + final attachmentPackage = widget.mediaAttachmentPackages[index]; final attachment = attachmentPackage.attachment; final message = attachmentPackage.message; if (attachment.type == AttachmentType.video) { @@ -268,18 +254,16 @@ class _StreamGalleryFooterState extends State { boxShadow: [ BoxShadow( blurRadius: 8, - color: chatThemeData - .colorTheme.textHighEmphasis + color: chatThemeData.colorTheme.textHighEmphasis // ignore: deprecated_member_use .withOpacity(0.3), ), ], ), child: StreamUserAvatar( + size: .sm, user: message.user!, - constraints: - BoxConstraints.tight(const Size(24, 24)), - showOnlineStatus: false, + showOnlineIndicator: false, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/gallery/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery/gallery_header.dart index 679c1c3d72..af3546c7cc 100644 --- a/packages/stream_chat_flutter/lib/src/gallery/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery/gallery_header.dart @@ -1,18 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/attachment_actions_modal/attachment_actions_modal.dart'; -import 'package:stream_chat_flutter/src/icons/stream_svg_icon.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/theme/themes.dart'; import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template streamGalleryHeader} /// Header/AppBar widget for media display screen /// {@endtemplate} -class StreamGalleryHeader extends StatelessWidget - implements PreferredSizeWidget { +class StreamGalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// {@macro streamGalleryHeader} const StreamGalleryHeader({ super.key, @@ -79,33 +77,27 @@ class StreamGalleryHeader extends StatelessWidget @override Widget build(BuildContext context) { final galleryHeaderThemeData = StreamGalleryHeaderTheme.of(context); - final theme = Theme.of(context); - return AppBar( - toolbarTextStyle: theme.textTheme.bodyMedium, - titleTextStyle: theme.textTheme.titleLarge, - systemOverlayStyle: theme.brightness == Brightness.dark - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark, + final textTheme = context.streamTextTheme; + + return StreamAppBar( elevation: elevation, leading: showBackButton ? IconButton( - icon: StreamSvgIcon( - icon: StreamSvgIcons.close, + icon: Icon( + context.streamIcons.arrowLeft20, color: galleryHeaderThemeData.closeButtonColor, - size: 24, + size: 20, ), onPressed: onBackPressed, ) : const Empty(), - surfaceTintColor: - backgroundColor ?? galleryHeaderThemeData.backgroundColor, - backgroundColor: - backgroundColor ?? galleryHeaderThemeData.backgroundColor, + surfaceTintColor: backgroundColor ?? galleryHeaderThemeData.backgroundColor, + backgroundColor: backgroundColor ?? galleryHeaderThemeData.backgroundColor, actions: [ if (!message.isEphemeral) IconButton( - icon: StreamSvgIcon( - icon: StreamSvgIcons.menuPoint, + icon: Icon( + context.streamIcons.more20, color: galleryHeaderThemeData.iconMenuPointColor, ), onPressed: () => _showMessageActionModalBottomSheet(context), @@ -124,11 +116,13 @@ class StreamGalleryHeader extends StatelessWidget children: [ Text( userName, - style: galleryHeaderThemeData.titleTextStyle, + style: galleryHeaderThemeData.titleTextStyle ?? textTheme.headingSm, ), Text( sentAt, - style: galleryHeaderThemeData.subtitleTextStyle, + style: + galleryHeaderThemeData.subtitleTextStyle ?? + textTheme.captionDefault.copyWith(color: context.streamColorScheme.textSecondary), ), ], ), @@ -143,8 +137,7 @@ class StreamGalleryHeader extends StatelessWidget Future _showMessageActionModalBottomSheet(BuildContext context) async { final channel = StreamChannel.of(context).channel; - final galleryHeaderThemeData = - StreamChatTheme.of(context).galleryHeaderTheme; + final galleryHeaderThemeData = StreamChatTheme.of(context).galleryHeaderTheme; final defaultModal = AttachmentActionsModal( attachment: attachment, @@ -153,7 +146,8 @@ class StreamGalleryHeader extends StatelessWidget onReply: onReplyMessage, ); - final effectiveModal = attachmentActionsModalBuilder?.call( + final effectiveModal = + attachmentActionsModalBuilder?.call( context, attachment, defaultModal, 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..21d0d93c07 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,7 +1,5 @@ -// ignore_for_file: deprecated_member_use_from_same_package - import 'package:flutter/material.dart'; -import 'package:svg_icon_widget/svg_icon_widget.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; part 'stream_svg_icon.g.dart'; @@ -13,1155 +11,110 @@ typedef StreamSvgIconData = SvgIconData; /// {@template StreamSvgIcon} /// Icon set of stream chat /// {@endtemplate} +@Deprecated('Use Icon(context.streamIcons.*) instead') class StreamSvgIcon extends StatelessWidget { /// Creates a [StreamSvgIcon]. + /// + /// Deprecated in favor of regular [Icon] widgets. + /// New icons can be replaced using the [StreamIcons] theme data. + /// + /// Previously: + /// + /// ```dart + /// StreamSvgIcon(icon: StreamSvgIcons.arrowRight) + /// ``` + /// + /// Replacement: + /// + /// ```dart + /// Icon(context.streamIcons.arrowRight20) + /// ``` + /// + /// List of replacement icons: + /// + /// - StreamSvgIcons.arrowRight -> context.streamIcons.arrowRight20 + /// - StreamSvgIcons.attach -> context.streamIcons.attachment20 + /// - StreamSvgIcons.award -> context.streamIcons.trophy20 + /// - StreamSvgIcons.camera -> context.streamIcons.camera20 + /// - StreamSvgIcons.check -> context.streamIcons.checkmark20 + /// - StreamSvgIcons.checkAll -> context.streamIcons.checks20 + /// - StreamSvgIcons.checkSend -> context.streamIcons.checkmark20 + /// - StreamSvgIcons.circleUp -> context.streamIcons.arrowUp20 + /// - StreamSvgIcons.close -> context.streamIcons.xmark20 + /// - StreamSvgIcons.closeSmall -> context.streamIcons.xmark16 + /// - StreamSvgIcons.contacts -> context.streamIcons.users20 + /// - StreamSvgIcons.copy -> context.streamIcons.copy20 + /// - StreamSvgIcons.delete -> context.streamIcons.delete20 + /// - StreamSvgIcons.down -> context.streamIcons.chevronDown20 + /// - StreamSvgIcons.download -> context.streamIcons.download20 + /// - StreamSvgIcons.edit -> context.streamIcons.edit20 + /// - StreamSvgIcons.emptyCircleRight -> context.streamIcons.chevronRight20 + /// - StreamSvgIcons.error -> context.streamIcons.exclamationCircleFill20 + /// - StreamSvgIcons.eye -> context.streamIcons.eyeFill20 + /// - StreamSvgIcons.files -> context.streamIcons.file20 + /// - StreamSvgIcons.flag -> context.streamIcons.flag20 + /// - StreamSvgIcons.grid -> context.streamIcons.gallery20 + /// - StreamSvgIcons.group -> context.streamIcons.users20 + /// - StreamSvgIcons.left -> context.streamIcons.chevronLeft20 + /// - StreamSvgIcons.lightning -> context.streamIcons.bolt20 + /// - StreamSvgIcons.link -> context.streamIcons.link20 + /// - StreamSvgIcons.lock -> context.streamIcons.lock20 + /// - StreamSvgIcons.mentions -> context.streamIcons.mention20 + /// - StreamSvgIcons.menuPoint -> context.streamIcons.more20 + /// - StreamSvgIcons.message -> context.streamIcons.messageBubble20 + /// - StreamSvgIcons.messageUnread -> context.streamIcons.notification20 + /// - StreamSvgIcons.mic -> context.streamIcons.voice20 + /// - StreamSvgIcons.mute -> context.streamIcons.mute20 + /// - StreamSvgIcons.notification -> context.streamIcons.bell20 + /// - StreamSvgIcons.pause -> context.streamIcons.pauseFill20 + /// - StreamSvgIcons.penWrite -> context.streamIcons.edit20 + /// - StreamSvgIcons.pictures -> context.streamIcons.image20 + /// - StreamSvgIcons.pin -> context.streamIcons.pin20 + /// - StreamSvgIcons.play -> context.streamIcons.playFill20 + /// - StreamSvgIcons.polls -> context.streamIcons.poll20 + /// - StreamSvgIcons.record -> context.streamIcons.video20 + /// - StreamSvgIcons.reload -> context.streamIcons.refresh20 + /// - StreamSvgIcons.reply -> context.streamIcons.reply20 + /// - StreamSvgIcons.retry -> context.streamIcons.retry20 + /// - StreamSvgIcons.right -> context.streamIcons.chevronRight20 + /// - StreamSvgIcons.save -> context.streamIcons.save20 + /// - StreamSvgIcons.search -> context.streamIcons.search20 + /// - StreamSvgIcons.send -> context.streamIcons.send20 + /// - StreamSvgIcons.sendMessage -> context.streamIcons.send20 + /// - StreamSvgIcons.share -> context.streamIcons.export20 + /// - StreamSvgIcons.shareArrow -> context.streamIcons.share20 + /// - StreamSvgIcons.smile -> context.streamIcons.emoji20 + /// - StreamSvgIcons.stop -> context.streamIcons.stopFill20 + /// - StreamSvgIcons.threadReply -> context.streamIcons.thread20 + /// - StreamSvgIcons.time -> context.streamIcons.clock20 + /// - StreamSvgIcons.up -> context.streamIcons.chevronUp20 + /// - StreamSvgIcons.user -> context.streamIcons.user20 + /// - StreamSvgIcons.userAdd -> context.streamIcons.userAdd20 + /// - StreamSvgIcons.userDelete -> context.streamIcons.userRemove20 + /// - StreamSvgIcons.userRemove -> context.streamIcons.userRemove20 + /// - StreamSvgIcons.userSettings -> context.streamIcons.userCheck20 + /// - StreamSvgIcons.videoCall -> context.streamIcons.videoFill20 + /// - StreamSvgIcons.volumeUp -> context.streamIcons.audio20 + /// + /// Removed in new set (no equivalent): + /// - StreamSvgIcons.cloudDownload + /// - StreamSvgIcons.lolReaction + /// - StreamSvgIcons.loveReaction + /// - StreamSvgIcons.moon + /// - StreamSvgIcons.settings + /// - StreamSvgIcons.thumbsDownReaction + /// - StreamSvgIcons.thumbsUpReaction + /// - StreamSvgIcons.wutReaction + @Deprecated('Use Icon(context.streamIcons.*) instead') 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 +122,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 +192,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 +202,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/icons/stream_svg_icon.g.dart b/packages/stream_chat_flutter/lib/src/icons/stream_svg_icon.g.dart index c6a7fc2001..f9f154eb59 100644 --- a/packages/stream_chat_flutter/lib/src/icons/stream_svg_icon.g.dart +++ b/packages/stream_chat_flutter/lib/src/icons/stream_svg_icon.g.dart @@ -503,16 +503,14 @@ abstract final class StreamSvgIcons { ); /// Stream SVG icon named 'filetypePresentationStandard'. - static const StreamSvgIconData filetypePresentationStandard = - StreamSvgIconData( + static const StreamSvgIconData filetypePresentationStandard = StreamSvgIconData( 'lib/assets/icons/colored/icon_filetype_presentation_standard.svg', package: package, preserveColors: true, ); /// Stream SVG icon named 'filetypePresentationSpecial'. - static const StreamSvgIconData filetypePresentationSpecial = - StreamSvgIconData( + static const StreamSvgIconData filetypePresentationSpecial = StreamSvgIconData( 'lib/assets/icons/colored/icon_filetype_presentation_special.svg', package: package, preserveColors: true, @@ -652,8 +650,7 @@ abstract final class StreamSvgIcons { ); /// Stream SVG icon named 'filetypeSpreadsheetStandard'. - static const StreamSvgIconData filetypeSpreadsheetStandard = - StreamSvgIconData( + static const StreamSvgIconData filetypeSpreadsheetStandard = StreamSvgIconData( 'lib/assets/icons/colored/icon_filetype_spreadsheet_standard.svg', package: package, preserveColors: true, @@ -828,8 +825,7 @@ abstract final class StreamSvgIcons { ); /// Stream SVG icon named 'filetypeCompressionStandard'. - static const StreamSvgIconData filetypeCompressionStandard = - StreamSvgIconData( + static const StreamSvgIconData filetypeCompressionStandard = StreamSvgIconData( 'lib/assets/icons/colored/icon_filetype_compression_standard.svg', package: package, preserveColors: true, diff --git a/packages/stream_chat_flutter/lib/src/indicators/sending_indicator.dart b/packages/stream_chat_flutter/lib/src/indicators/sending_indicator.dart index 5951336615..9e7fe17a8c 100644 --- a/packages/stream_chat_flutter/lib/src/indicators/sending_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/indicators/sending_indicator.dart @@ -12,7 +12,7 @@ class StreamSendingIndicator extends StatelessWidget { required this.message, this.isMessageRead = false, this.isMessageDelivered = false, - this.size = 12, + this.size, }); /// The message whose sending status is to be shown. @@ -33,33 +33,33 @@ class StreamSendingIndicator extends StatelessWidget { final colorTheme = streamChatTheme.colorTheme; if (isMessageRead) { - return StreamSvgIcon( + return Icon( + context.streamIcons.checks16, size: size, - icon: StreamSvgIcons.checkAll, color: colorTheme.accentPrimary, ); } if (isMessageDelivered) { - return StreamSvgIcon( + return Icon( + context.streamIcons.checks16, size: size, - icon: StreamSvgIcons.checkAll, color: colorTheme.textLowEmphasis, ); } if (message.state.isCompleted) { - return StreamSvgIcon( + return Icon( + context.streamIcons.checkmark20, size: size, - icon: StreamSvgIcons.check, color: colorTheme.textLowEmphasis, ); } if (message.state.isOutgoing) { - return StreamSvgIcon( + return Icon( + context.streamIcons.clock20, size: size, - icon: StreamSvgIcons.time, color: colorTheme.textLowEmphasis, ); } diff --git a/packages/stream_chat_flutter/lib/src/indicators/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/indicators/typing_indicator.dart index fdf8e3a001..10a84b2c7f 100644 --- a/packages/stream_chat_flutter/lib/src/indicators/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/indicators/typing_indicator.dart @@ -34,17 +34,15 @@ class StreamTypingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - final channelState = - channel?.state ?? StreamChannel.of(context).channel.state!; + final channelState = channel?.state ?? StreamChannel.of(context).channel.state!; final altWidget = alternativeWidget ?? const Empty(); return BetterStreamBuilder>( initialData: channelState.typingEvents.keys, - stream: channelState.typingEventsStream.map((typingEvents) => typingEvents - .entries - .where((element) => element.value.parentId == parentId) - .map((e) => e.key)), + stream: channelState.typingEventsStream.map( + (typingEvents) => typingEvents.entries.where((element) => element.value.parentId == parentId).map((e) => e.key), + ), builder: (context, users) => AnimatedSwitcher( layoutBuilder: (currentChild, previousChildren) => Stack( children: [ @@ -60,11 +58,6 @@ class StreamTypingIndicator extends StatelessWidget { mainAxisSize: MainAxisSize.min, spacing: 4, children: [ - Lottie.asset( - 'lib/assets/animations/typing_dots.json', - package: 'stream_chat_flutter', - height: 4, - ), Flexible( child: Text( context.translations.userTypingText(users), @@ -72,6 +65,14 @@ class StreamTypingIndicator extends StatelessWidget { style: style, ), ), + Padding( + padding: const EdgeInsets.only(top: 2), + child: Lottie.asset( + 'lib/assets/animations/typing_dots.json', + package: 'stream_chat_flutter', + height: 5, + ), + ), ], ), ) 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..2ae46f73cc 100644 --- a/packages/stream_chat_flutter/lib/src/indicators/unread_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/indicators/unread_indicator.dart @@ -1,19 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template streamUnreadIndicator} /// Shows different unread counts of the user. /// {@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. /// @@ -35,31 +32,30 @@ class StreamUnreadIndicator extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); final client = StreamChat.of(context).client; final stream = switch (_unreadType) { _TotalUnreadCount() => client.state.totalUnreadCountStream, _UnreadChannels(cid: final cid) => switch (cid) { - final cid? => client.state.channels[cid]?.state?.unreadCountStream, - _ => client.state.unreadChannelsStream, - }, + final cid? => client.state.channels[cid]?.state?.unreadCountStream, + _ => client.state.unreadChannelsStream, + }, _UnreadThreads(id: final id) => switch (id) { - // TODO: Handle id once it's supported - _ => client.state.unreadThreadsStream, - } + // TODO: Handle id once it's supported + _ => client.state.unreadThreadsStream, + }, }; final initialData = switch (_unreadType) { _TotalUnreadCount() => client.state.totalUnreadCount, _UnreadChannels(cid: final cid) => switch (cid) { - final cid? => client.state.channels[cid]?.state?.unreadCount, - _ => client.state.unreadChannels, - }, + final cid? => client.state.channels[cid]?.state?.unreadCount, + _ => client.state.unreadChannels, + }, _UnreadThreads(id: final id) => switch (id) { - // TODO: Handle id once it's supported - _ => client.state.unreadThreads, - } + // TODO: Handle id once it's supported + _ => client.state.unreadThreads, + }, }; return IgnorePointer( @@ -69,16 +65,12 @@ class StreamUnreadIndicator extends StatelessWidget { builder: (context, unreadCount) { if (unreadCount == 0) return const Empty(); - return Badge( - textColor: Colors.white, - textStyle: theme.textTheme.footnoteBold, - backgroundColor: theme.channelPreviewTheme.unreadCounterColor, - label: Text( - switch (unreadCount) { - > 99 => '99+', - _ => '$unreadCount', - }, - ), + return StreamBadgeNotification( + size: StreamBadgeNotificationSize.xs, + label: switch (unreadCount) { + > 99 => '99+', + _ => '$unreadCount', + }, ); }, ), diff --git a/packages/stream_chat_flutter/lib/src/indicators/upload_progress_indicator.dart b/packages/stream_chat_flutter/lib/src/indicators/upload_progress_indicator.dart index 7a6b289814..c28dba83b6 100644 --- a/packages/stream_chat_flutter/lib/src/indicators/upload_progress_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/indicators/upload_progress_indicator.dart @@ -59,7 +59,8 @@ class StreamUploadProgressIndicator extends StatelessWidget { const SizedBox(width: 8), Text( '${_percentage.toInt()}%', - style: textStyle ?? + style: + textStyle ?? theme.textTheme.footnote.copyWith( color: theme.colorTheme.barsBg, ), diff --git a/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keyboard_shortcut_runner.dart b/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keyboard_shortcut_runner.dart index 706adad454..fa2383a17b 100644 --- a/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keyboard_shortcut_runner.dart +++ b/packages/stream_chat_flutter/lib/src/keyboard_shortcuts/keyboard_shortcut_runner.dart @@ -37,8 +37,7 @@ class KeyboardShortcutRunner extends StatelessWidget { shortcuts: { if (onEnterKeypress != null) enterKeySet: EnterKeyIntent(), if (onEscapeKeypress != null) escapeKeySet: EscapeKeyIntent(), - if (onRightArrowKeypress != null) - rightArrowKeySet: RightArrowKeyIntent(), + if (onRightArrowKeypress != null) rightArrowKeySet: RightArrowKeyIntent(), if (onLeftArrowKeypress != null) leftArrowKeySet: LeftArrowKeyIntent(), }, actions: { diff --git a/packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart b/packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart index 66c09e8847..bc7da0a593 100644 --- a/packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/stream_chat_localizations.dart @@ -1,6 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/src/localization/translations.dart' - show Translations; +import 'package:stream_chat_flutter/src/localization/translations.dart' show Translations; /// Defines the localized resource values used by the StreamChatFlutter widgets. /// diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index ce5989e7df..4303410583 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'; @@ -535,6 +537,18 @@ abstract class Translations { /// The text for video attachment in channel list preview String get videoAttachmentText; + /// The text for file attachment in channel list preview + String get fileAttachmentText; + + /// The text for multiple files attachment in channel list preview + String filesAttachmentCountText(int count); + + /// The text for multiple photos attachment in channel list preview + String photosAttachmentCountText(int count); + + /// The text for multiple videos attachment in channel list preview + String videosAttachmentCountText(int count); + /// The text for poll when current user voted String get pollYouVotedText; @@ -549,6 +563,58 @@ 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}); + + /// The text shown when there are no conversations yet. + String get noConversationsYetText; + + /// The text shown when there are no threads yet. + String get replyToStartThreadText; + + /// The text shown to prompt the user to send a message. + String get sendMessageToStartConversationText; + + /// The label for the "Saved for later" message annotation. + String get savedForLaterLabel; + + /// The annotation label shown on a message that was replied to a thread, + /// displayed in channel view (e.g. "Replied to a thread"). + String get repliedToThreadAnnotationLabel; + + /// The annotation label shown on a message that was also sent in channel, + /// displayed in thread view (e.g. "Also sent in channel"). + String get alsoSentInChannelAnnotationLabel; + + /// The "View" link label used in message annotations. + String get viewLabel; + + /// The annotation label for a reminder (e.g. "Reminder set"). + String get reminderSetLabel; + + /// The text displaying the reminder time (e.g. "Today at 3:00 PM"). + String reminderAtText(String time); + + /// The label for "Create a poll and let everyone vote!" + String get createPollPromptLabel; + + /// The label for "Take a photo and share" + String get takePhotoAndShareLabel; + + /// The label for "Take a video and share" + String get takeVideoAndShareLabel; + + /// The label for "Open camera" + String get openCameraLabel; + + /// The label for "Select files to share" + String get selectFilesToShareLabel; + + /// The label for "Open files" + String get openFilesLabel; } /// Default implementation of Translation strings for the stream chat widgets @@ -590,20 +656,19 @@ class DefaultTranslations implements Translations { } @override - String get threadReplyLabel => 'Thread Reply'; + String get threadReplyLabel => 'Thread'; @override String get onlyVisibleToYouText => 'Only visible to you'; @override - String threadReplyCountText(int count) => '$count Thread Replies'; + String threadReplyCountText(int count) => count == 1 ? '1 reply' : '$count replies'; @override String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - 'Uploading $remaining/$total ...'; + }) => 'Uploading $remaining/$total ...'; @override String pinnedByUserText({ @@ -616,8 +681,7 @@ class DefaultTranslations implements Translations { } @override - String get sendMessagePermissionError => - "You don't have permission to send messages"; + String get sendMessagePermissionError => "You don't have permission to send messages"; @override String get emptyMessagesText => 'There are no messages currently'; @@ -651,8 +715,8 @@ class DefaultTranslations implements Translations { @override String threadSeparatorText(int replyCount) { - if (replyCount == 1) return '1 Reply'; - return '$replyCount Replies'; + if (replyCount == 1) return '1 reply'; + return '$replyCount replies'; } @override @@ -665,7 +729,7 @@ class DefaultTranslations implements Translations { String get reconnectingLabel => 'Reconnecting...'; @override - String get alsoSendAsDirectMessageLabel => 'Also send as direct message'; + String get alsoSendAsDirectMessageLabel => 'Also send in Channel'; @override String get addACommentOrSendLabel => 'Add a comment or send'; @@ -690,8 +754,7 @@ class DefaultTranslations implements Translations { 'The file is too large to upload. The file size limit is $limitInMB MB.'; @override - String get couldNotReadBytesFromFileError => - 'Could not read bytes from file.'; + String get couldNotReadBytesFromFileError => 'Could not read bytes from file.'; @override String get addAFileLabel => 'Add a file'; @@ -718,7 +781,7 @@ class DefaultTranslations implements Translations { String get somethingWentWrongError => 'Something went wrong'; @override - String get addMoreFilesLabel => 'Add more files'; + String get addMoreFilesLabel => 'Add more'; @override String get enablePhotoAndVideoAccessMessage => @@ -733,8 +796,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'; @@ -746,8 +808,7 @@ class DefaultTranslations implements Translations { String get flagMessageSuccessfulLabel => 'Message flagged'; @override - String get flagMessageSuccessfulText => - 'The message has been reported to a moderator.'; + String get flagMessageSuccessfulText => 'The message has been reported to a moderator.'; @override String get deleteLabel => 'DELETE'; @@ -756,12 +817,10 @@ class DefaultTranslations implements Translations { String get deleteMessageLabel => 'Delete Message'; @override - String get deleteMessageQuestion => - 'Are you sure you want to permanently delete this\nmessage?'; + String get deleteMessageQuestion => 'Are you sure you want to permanently delete this message?'; @override - String get operationCouldNotBeCompletedText => - "The operation couldn't be completed."; + String get operationCouldNotBeCompletedText => "The operation couldn't be completed."; @override String get replyLabel => 'Reply'; @@ -839,8 +898,7 @@ class DefaultTranslations implements Translations { String get letsStartChattingLabel => 'Let’s start chatting!'; @override - String get sendingFirstMessageLabel => - 'How about sending your first message to a friend?'; + String get sendingFirstMessageLabel => 'How about sending your first message to a friend?'; @override String get startAChatLabel => 'Start a chat'; @@ -852,8 +910,7 @@ class DefaultTranslations implements Translations { String get deleteConversationLabel => 'Delete Conversation'; @override - String get deleteConversationQuestion => - 'Are you sure you want to delete this conversation?'; + String get deleteConversationQuestion => 'Are you sure you want to delete this conversation?'; @override String get streamChatLabel => 'Stream Chat'; @@ -892,8 +949,7 @@ class DefaultTranslations implements Translations { String get leaveConversationLabel => 'Leave conversation'; @override - String get leaveConversationQuestion => - 'Are you sure you want to leave this conversation?'; + String get leaveConversationQuestion => 'Are you sure you want to leave this conversation?'; @override String get showInChatLabel => 'Show in Chat'; @@ -929,8 +985,7 @@ class DefaultTranslations implements Translations { String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '${currentPage + 1} of $totalPages'; + }) => '${currentPage + 1} of $totalPages'; @override String get fileText => 'File'; @@ -997,8 +1052,7 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments } @override - String get linkDisabledDetails => - 'Sending links is not allowed in this conversation.'; + String get linkDisabledDetails => 'Sending links is not allowed in this conversation.'; @override String get linkDisabledError => 'Links are disabled'; @@ -1007,7 +1061,8 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments String unreadMessagesSeparatorText() => 'New messages'; @override - String get enableFileAccessMessage => 'Please enable access to files' + String get enableFileAccessMessage => + 'Please enable access to files' '\nso you can share them with friends.'; @override @@ -1108,15 +1163,13 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments String get enterYourCommentLabel => 'Enter your comment'; @override - String get endVoteConfirmationText => - 'Are you sure you want to end the vote?'; + String get endVoteConfirmationText => 'Are you sure you want to end the vote?'; @override String get deletePollOptionLabel => 'Delete Option'; @override - String get deletePollOptionQuestion => - 'Are you sure you want to delete this option?'; + String get deletePollOptionQuestion => 'Are you sure you want to delete this option?'; @override String get createLabel => 'Create'; @@ -1160,10 +1213,10 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments @override String voteCountLabel({int? count}) => switch (count) { - null || < 1 => '0 votes', - 1 => '1 vote', - _ => '$count votes', - }; + null || < 1 => '0 votes', + 1 => '1 vote', + _ => '$count votes', + }; @override String get noPollVotesLabel => 'There are no poll votes currently'; @@ -1190,8 +1243,7 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments String get sendAnywayLabel => 'Send Anyway'; @override - String get moderatedMessageBlockedText => - 'Message was blocked by moderation policies'; + String get moderatedMessageBlockedText => 'Message was blocked by moderation policies'; @override String get moderationReviewModalTitle => 'Are you sure?'; @@ -1215,6 +1267,18 @@ Attachment limit exceeded: it's not possible to add more than $limit attachments @override String get videoAttachmentText => 'Video'; + @override + String get fileAttachmentText => 'File'; + + @override + String filesAttachmentCountText(int count) => count == 1 ? 'File' : '$count files'; + + @override + String photosAttachmentCountText(int count) => count == 1 ? 'Photo' : '$count photos'; + + @override + String videosAttachmentCountText(int count) => count == 1 ? 'Video' : '$count videos'; + @override String get pollYouVotedText => 'You voted'; @@ -1229,4 +1293,55 @@ 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'; + } + + @override + String get noConversationsYetText => 'No conversations yet'; + + @override + String get replyToStartThreadText => 'Reply to a message to start a thread'; + + @override + String get sendMessageToStartConversationText => 'Send a message to start the conversation'; + + @override + String get savedForLaterLabel => 'Saved for later'; + + @override + String get repliedToThreadAnnotationLabel => 'Replied to a thread'; + + @override + String get alsoSentInChannelAnnotationLabel => 'Also sent in channel'; + + @override + String get viewLabel => 'View'; + + @override + String get reminderSetLabel => 'Reminder set'; + + @override + String reminderAtText(String time) => 'Today at $time'; + + @override + String get createPollPromptLabel => 'Create a poll and let everyone vote!'; + + @override + String get takePhotoAndShareLabel => 'Take a photo and share'; + + @override + String get takeVideoAndShareLabel => 'Take a video and share'; + + @override + String get openCameraLabel => 'Open camera'; + + @override + String get selectFilesToShareLabel => 'Select files to share'; + + @override + String get openFilesLabel => 'Open files'; } 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..27f950eae8 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_action/message_action.dart @@ -0,0 +1,138 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// {@template messageActionsBuilder} +/// Signature for a builder that customizes the list of message actions shown +/// in the long-press context menu. +/// +/// [defaultActions] are the pre-built actions already filtered by the widget's +/// `show*` flags. Return a modified list to add, remove, or reorder actions. +/// +/// The return type is [List] so any widget — including +/// [StreamContextMenuSeparator] or fully custom items — can be mixed in +/// alongside the default [StreamContextMenuAction] items. +/// {@endtemplate} +typedef MessageActionsBuilder = + List Function( + BuildContext context, + List> defaultActions, + ); + +/// {@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}); +} 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..5a774be904 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart @@ -0,0 +1,259 @@ +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'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.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 [StreamContextMenuAction]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 []; + + final icons = context.streamIcons; + + return >[ + StreamContextMenuAction( + value: ResendMessage(message: message), + label: Text(context.translations.sendAnywayLabel), + leading: Icon( + icons.send20, + color: StreamChatTheme.of(context).colorTheme.accentPrimary, + ), + ), + StreamContextMenuAction( + value: EditMessage(message: message), + label: Text(context.translations.editMessageLabel), + leading: Icon(icons.edit20), + ), + StreamContextMenuAction.destructive( + value: HardDeleteMessage(message: message), + label: Text(context.translations.deleteMessageLabel), + leading: Icon(icons.delete20), + ), + ]; + } + + /// Returns a list of message actions based on the provided message and + /// channel capabilities. + /// + /// This method builds a list of [StreamContextMenuAction]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, + }) { + final messageState = message.state; + + // If the message is deleted, we don't show any actions. + if (messageState.isDeleted) return []; + + final icons = context.streamIcons; + + if (messageState.isFailed) { + return [ + if (messageState.isSendingFailed || messageState.isUpdatingFailed) ...[ + StreamContextMenuAction( + value: ResendMessage(message: message), + leading: Icon(icons.send20), + label: Text( + context.translations.toggleResendOrResendEditedMessage( + isUpdateFailed: messageState.isUpdatingFailed, + ), + ), + ), + if (messageState.isSendingFailed) + StreamContextMenuAction.destructive( + value: HardDeleteMessage(message: message), + leading: Icon(icons.delete20), + label: Text( + context.translations.toggleDeleteRetryDeleteMessageText( + isDeleteFailed: false, + ), + ), + ), + ], + if (message.state.isDeletingFailed) + StreamContextMenuAction.destructive( + value: ResendMessage(message: message), + leading: Icon(icons.delete20), + label: 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.canUseReadReceipts; + 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( + StreamContextMenuAction( + value: QuotedReply(message: message), + label: Text(context.translations.replyLabel), + leading: Icon(icons.reply20), + ), + ); + } + + if (canSendReply && !isThreadMessage) { + messageActions.add( + StreamContextMenuAction( + value: ThreadReply(message: message), + label: Text(context.translations.threadReplyLabel), + leading: Icon(icons.thread20), + ), + ); + } + + // Mark unread action is only available for other users' messages. + if (canReceiveReadEvents && !isSentByCurrentUser) { + StreamContextMenuAction markUnreadAction() { + return StreamContextMenuAction( + value: MarkUnread(message: message), + label: Text(context.translations.markAsUnreadLabel), + leading: Icon(icons.notification20), + ); + } + + // 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 (!isThreadMessage || canShowInChannel) { + messageActions.add(markUnreadAction()); + } + } + + if (message.text case final text? when text.isNotEmpty) { + messageActions.add( + StreamContextMenuAction( + value: CopyMessage(message: message), + label: Text(context.translations.copyMessageLabel), + leading: Icon(icons.copy20), + ), + ); + } + + if (!containsPoll && !containsGiphy) { + if (canUpdateAnyMessage || (canUpdateOwnMessage && isSentByCurrentUser)) { + messageActions.add( + StreamContextMenuAction( + value: EditMessage(message: message), + label: Text(context.translations.editMessageLabel), + leading: Icon(icons.edit20), + ), + ); + } + } + + // 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( + StreamContextMenuAction( + value: action, + label: Text(label.call(pinned: isPinned)), + leading: Icon(icons.pin20), + ), + ); + } + + if (canDeleteAnyMessage || (canDeleteOwnMessage && isSentByCurrentUser)) { + final label = context.translations.toggleDeleteRetryDeleteMessageText; + + messageActions.add( + StreamContextMenuAction.destructive( + value: DeleteMessage(message: message), + leading: Icon(icons.delete20), + label: Text(label.call(isDeleteFailed: false)), + ), + ); + } + + if (!isSentByCurrentUser) { + messageActions.add( + StreamContextMenuAction( + value: FlagMessage(message: message), + label: Text(context.translations.flagMessageLabel), + leading: Icon(icons.flag20), + ), + ); + } + + 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( + StreamContextMenuAction( + value: action, + label: Text(label.call(isMuted: isMuted)), + leading: Icon(icons.mute20), + ), + ); + } + + 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_button.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_button.dart index e77465572c..d8fc74f593 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_button.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_button.dart @@ -53,7 +53,7 @@ class AttachmentButton extends StatelessWidget { color: color, iconSize: size, onPressed: onPressed, - icon: icon ?? const StreamSvgIcon(icon: StreamSvgIcons.attach), + icon: icon ?? Icon(context.streamIcons.attachment20), ); } } diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/options.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/options.dart index 2e34169c56..f7c187f3ee 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/options.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/options.dart @@ -1,3 +1,4 @@ +export 'stream_command_picker.dart'; export 'stream_file_picker.dart'; export 'stream_gallery_picker.dart'; export 'stream_image_picker.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_command_icon.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_command_icon.dart new file mode 100644 index 0000000000..e397f8b30d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_command_icon.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// An icon widget for a chat command. +/// +/// Displays a 20px icon matching the given [command] name. +class StreamCommandIcon extends StatelessWidget { + /// Creates a [StreamCommandIcon]. + const StreamCommandIcon({super.key, required this.command}); + + /// The command whose icon is displayed. + final Command command; + + @override + Widget build(BuildContext context) { + const size = 20.0; + final color = context.streamColorScheme.textSecondary; + + return IconTheme.merge( + data: IconThemeData(size: size, color: color), + child: switch (command.name) { + 'giphy' => const StreamSvgIcon(icon: StreamSvgIcons.giphy), + 'imgur' => const StreamSvgIcon(icon: StreamSvgIcons.imgur), + 'ban' => Icon(context.streamIcons.userRemove20), + 'flag' => Icon(context.streamIcons.flag20), + 'mute' => Icon(context.streamIcons.mute20), + 'unban' => Icon(context.streamIcons.userAdd20), + 'unmute' => Icon(context.streamIcons.audio20), + _ => Icon(context.streamIcons.bolt20), + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_command_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_command_picker.dart new file mode 100644 index 0000000000..eb489638c4 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_command_picker.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_picker/options/stream_command_icon.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Widget shown in the attachment picker for browsing and selecting commands. +class StreamCommandPicker extends StatelessWidget { + /// Creates a [StreamCommandPicker] widget. + const StreamCommandPicker({ + super.key, + this.onCommandSelected, + }); + + /// Callback called when a command is selected. + final ValueSetter? onCommandSelected; + + @override + Widget build(BuildContext context) { + final channel = StreamChannel.of(context).channel; + final commands = channel.config?.commands ?? const []; + + final textTheme = StreamChatTheme.of(context).textTheme; + final colorTheme = StreamChatTheme.of(context).colorTheme; + final spacing = context.streamSpacing; + + return OptionDrawer( + margin: EdgeInsets.zero, + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: commands.length + 1, + itemBuilder: (context, index) { + final command = index == 0 ? null : commands[index - 1]; + if (command == null) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: spacing.md), + child: Text(context.translations.instantCommandsLabel, style: textTheme.headlineBold), + ); + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: onCommandSelected == null ? null : () => onCommandSelected!(command), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: spacing.sm, vertical: spacing.xs), + child: Row( + spacing: spacing.sm, + children: [ + StreamCommandIcon(command: command), + Text( + command.name.sentenceCase, + style: textTheme.bodyBold.copyWith( + color: colorTheme.textHighEmphasis, + ), + ), + Expanded( + child: Text( + '/${command.name} ${command.args}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.body.copyWith( + color: colorTheme.textLowEmphasis, + ), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } +} 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..d2f431eb95 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 @@ -2,11 +2,10 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler.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/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Widget used to pick files from the device class StreamFilePicker extends StatelessWidget { @@ -19,7 +18,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 +42,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; @@ -57,56 +56,82 @@ class StreamFilePicker extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); + final spacing = context.streamSpacing; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + + Future onPickFile() async { + final pickedFile = await runInPermissionRequestLock(() { + return StreamAttachmentHandler.instance.pickFile( + dialogTitle: dialogTitle, + initialDirectory: initialDirectory, + type: type, + allowedExtensions: allowedExtensions, + onFileLoading: onFileLoading, + compressionQuality: compressionQuality, + withData: withData, + withReadStream: withReadStream, + lockParentWindow: lockParentWindow, + ); + }); + + return onFilePicked.call(pickedFile); + } + return OptionDrawer( child: EndOfFrameCallbackWidget( - child: StreamSvgIcon( - size: 240, - icon: StreamSvgIcons.files, - color: theme.colorTheme.disabled, + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + size: 32, + context.streamIcons.file32, + color: colorScheme.textTertiary, + ), + SizedBox(height: spacing.xs), + Text( + context.translations.selectFilesToShareLabel, + style: textTheme.bodyDefault.copyWith( + color: colorScheme.textSecondary, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: spacing.md), + StreamButton( + type: .outline, + style: .secondary, + onTap: onPickFile, + label: context.translations.openFilesLabel, + ), + ], + ), ), - onEndOfFrame: (_) async { - final pickedFile = await runInPermissionRequestLock(() { - return StreamAttachmentHandler.instance.pickFile( - dialogTitle: dialogTitle, - initialDirectory: initialDirectory, - type: type, - allowedExtensions: allowedExtensions, - onFileLoading: onFileLoading, - allowCompression: allowCompression, - withData: withData, - withReadStream: withReadStream, - lockParentWindow: lockParentWindow, - ); - }); - - onFilePicked.call(pickedFile); - }, + onEndOfFrame: (_) => onPickFile(), errorBuilder: (context, error, stacktrace) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - StreamSvgIcon( - size: 240, - icon: StreamSvgIcons.files, - color: theme.colorTheme.disabled, + Icon( + size: 32, + context.streamIcons.file32, + color: colorScheme.textTertiary, ), + SizedBox(height: spacing.xs), Text( context.translations.enablePhotoAndVideoAccessMessage, - style: theme.textTheme.body.copyWith( - color: theme.colorTheme.textLowEmphasis, + style: textTheme.bodyDefault.copyWith( + color: colorScheme.textSecondary, ), textAlign: TextAlign.center, ), - const SizedBox(height: 8), - TextButton( - onPressed: PhotoManager.openSetting, - child: Text( - context.translations.allowGalleryAccessMessage, - style: theme.textTheme.bodyBold.copyWith( - color: theme.colorTheme.accentPrimary, - ), - ), + SizedBox(height: spacing.md), + StreamButton( + type: .outline, + style: .secondary, + onTap: PhotoManager.openSetting, + label: context.translations.allowGalleryAccessMessage, ), ], ); 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..ac6de1ae96 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 @@ -4,17 +4,17 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; 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'; -import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.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 +23,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(); } @@ -98,9 +79,9 @@ class _StreamGalleryPickerState extends State { builder: (context, snapshot) { if (!snapshot.hasData) return const Empty(); - final theme = StreamChatTheme.of(context); - final textTheme = theme.textTheme; - final colorTheme = theme.colorTheme; + final spacing = context.streamSpacing; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; // Available on both Android and iOS. final isAuthorized = snapshot.data == PermissionState.authorized; @@ -110,44 +91,32 @@ class _StreamGalleryPickerState extends State { final isPermissionGranted = isAuthorized || isLimited; return OptionDrawer( - actions: [ - if (isLimited) - IconButton( - color: colorTheme.accentPrimary, - icon: const Icon(Icons.add_circle_outline_rounded), - onPressed: () async { - await PhotoManager.presentLimited(); - _controller.doInitialLoad(); - }, - ), - ], + margin: .zero, child: Builder( builder: (context) { if (!isPermissionGranted) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - StreamSvgIcon( - size: 240, - icon: StreamSvgIcons.pictures, - color: colorTheme.disabled, + Icon( + size: 32, + context.streamIcons.image32, + color: colorScheme.textTertiary, ), + SizedBox(height: spacing.xs), Text( context.translations.enablePhotoAndVideoAccessMessage, - style: textTheme.body.copyWith( - color: colorTheme.textLowEmphasis, + style: textTheme.bodyDefault.copyWith( + color: colorScheme.textSecondary, ), textAlign: TextAlign.center, ), - const SizedBox(height: 8), - TextButton( - onPressed: PhotoManager.openSetting, - child: Text( - context.translations.allowGalleryAccessMessage, - style: textTheme.bodyBold.copyWith( - color: colorTheme.accentPrimary, - ), - ), + SizedBox(height: spacing.md), + StreamButton( + type: .outline, + style: .secondary, + onTap: PhotoManager.openSetting, + label: context.translations.allowGalleryAccessMessage, ), ], ); @@ -159,10 +128,18 @@ 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, + addMoreBuilder: isLimited + ? (context) => _AddMoreTile( + onTap: () async { + await PhotoManager.presentLimited(); + _controller.doInitialLoad(); + }, + ) + : null, itemBuilder: (context, mediaItems, index, defaultWidget) { final media = mediaItems[index]; return defaultWidget.copyWith( @@ -178,6 +155,72 @@ class _StreamGalleryPickerState extends State { } } +class _AddMoreTile extends StatelessWidget { + const _AddMoreTile({required this.onTap}); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + final spacing = context.streamSpacing; + + return Material( + color: colorScheme.backgroundSurfaceCard, + child: InkWell( + onTap: onTap, + overlayColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) return colorScheme.backgroundPressed; + if (states.contains(WidgetState.hovered)) return colorScheme.backgroundHover; + return StreamColors.transparent; + }), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + context.streamIcons.plus20, + size: 20, + color: colorScheme.textTertiary, + ), + SizedBox(height: spacing.xs), + Text( + context.translations.addMoreFilesLabel, + style: textTheme.captionEmphasis.copyWith( + color: colorScheme.textTertiary, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } +} + +/// 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 { /// @@ -227,6 +270,10 @@ extension StreamImagePickerX on StreamAttachmentPickerController { extraDataMap['file_size'] = file.size!; + if (type == AssetType.video) { + extraDataMap['duration'] = asset.videoDuration.inSeconds; + } + final attachment = Attachment( id: asset.id, file: file, @@ -243,8 +290,7 @@ extension StreamImagePickerX on StreamAttachmentPickerController { final image = await asset.originFile; if (image != null) { final tempDir = await getTemporaryDirectory(); - final cachedFile = - File('${tempDir.path}/${image.path.split('/').last}'); + final cachedFile = File('${tempDir.path}/${image.path.split('/').last}'); if (cachedFile.existsSync()) { cachedFile.deleteSync(); } diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_image_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_image_picker.dart index a9130ea73b..1c36378039 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_image_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_image_picker.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Widget used to pick images from the device. class StreamImagePicker extends StatelessWidget { @@ -36,52 +37,74 @@ class StreamImagePicker extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); + final spacing = context.streamSpacing; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + + Future onPickImage() async { + final pickedImage = await runInPermissionRequestLock(() { + return StreamAttachmentHandler.instance.pickImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + }); + + return onImagePicked.call(pickedImage); + } + return OptionDrawer( child: EndOfFrameCallbackWidget( - child: StreamSvgIcon( - size: 240, - icon: StreamSvgIcons.camera, - color: theme.colorTheme.disabled, + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + size: 32, + context.streamIcons.camera32, + color: colorScheme.textTertiary, + ), + SizedBox(height: spacing.xs), + Text( + context.translations.takePhotoAndShareLabel, + style: textTheme.bodyDefault.copyWith(color: colorScheme.textSecondary), + textAlign: TextAlign.center, + ), + SizedBox(height: spacing.md), + StreamButton( + type: .outline, + style: .secondary, + onTap: onPickImage, + label: context.translations.openCameraLabel, + ), + ], + ), ), - onEndOfFrame: (_) async { - final pickedImage = await runInPermissionRequestLock(() { - return StreamAttachmentHandler.instance.pickImage( - source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: imageQuality, - preferredCameraDevice: preferredCameraDevice, - ); - }); - - onImagePicked.call(pickedImage); - }, + onEndOfFrame: (_) => onPickImage(), errorBuilder: (context, error, stacktrace) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - StreamSvgIcon( - size: 240, - icon: StreamSvgIcons.camera, - color: theme.colorTheme.disabled, + Icon( + size: 32, + context.streamIcons.camera32, + color: colorScheme.textTertiary, ), + SizedBox(height: spacing.xs), Text( context.translations.enablePhotoAndVideoAccessMessage, - style: theme.textTheme.body.copyWith( - color: theme.colorTheme.textLowEmphasis, - ), + style: textTheme.bodyDefault.copyWith(color: colorScheme.textSecondary), textAlign: TextAlign.center, ), - const SizedBox(height: 8), - TextButton( - onPressed: PhotoManager.openSetting, - child: Text( - context.translations.allowGalleryAccessMessage, - style: theme.textTheme.bodyBold.copyWith( - color: theme.colorTheme.accentPrimary, - ), - ), + SizedBox(height: spacing.md), + StreamButton( + type: .outline, + style: .secondary, + onTap: PhotoManager.openSetting, + label: context.translations.allowGalleryAccessMessage, ), ], ); diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_poll_creator.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_poll_creator.dart index ecf95fe96d..a9f1bfc768 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_poll_creator.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_poll_creator.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Widget used to create a poll. class StreamPollCreator extends StatelessWidget { @@ -22,7 +23,9 @@ class StreamPollCreator extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); + final spacing = context.streamSpacing; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; Future _openCreatePollFlow() async { final result = await showStreamPollCreatorDialog( @@ -31,35 +34,53 @@ class StreamPollCreator extends StatelessWidget { config: config, ); - onPollCreated?.call(result); + return onPollCreated?.call(result); } return OptionDrawer( child: EndOfFrameCallbackWidget( - child: StreamSvgIcon( - size: 180, - icon: StreamSvgIcons.polls, - color: theme.colorTheme.disabled, + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + size: 32, + context.streamIcons.poll32, + color: colorScheme.textTertiary, + ), + SizedBox(height: spacing.xs), + Text( + context.translations.createPollPromptLabel, + style: textTheme.bodyDefault.copyWith(color: colorScheme.textSecondary), + textAlign: TextAlign.center, + ), + SizedBox(height: spacing.md), + StreamButton( + type: .outline, + style: .secondary, + onTap: _openCreatePollFlow, + label: context.translations.createPollLabel(), + ), + ], + ), ), onEndOfFrame: (_) => _openCreatePollFlow(), errorBuilder: (context, error, stacktrace) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - StreamSvgIcon( - size: 240, - icon: StreamSvgIcons.polls, - color: theme.colorTheme.disabled, + Icon( + size: 32, + context.streamIcons.poll32, + color: colorScheme.textTertiary, ), - const SizedBox(height: 8), - TextButton( - onPressed: _openCreatePollFlow, - child: Text( - context.translations.createPollLabel(isNew: true), - style: theme.textTheme.bodyBold.copyWith( - color: theme.colorTheme.accentPrimary, - ), - ), + SizedBox(height: spacing.md), + StreamButton( + type: .outline, + style: .secondary, + onTap: _openCreatePollFlow, + label: context.translations.createPollLabel(isNew: true), ), ], ); diff --git a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_video_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_video_picker.dart index 4ba4e8f7b6..aa11c7798c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_video_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/options/stream_video_picker.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Widget used to capture video using the device camera. class StreamVideoPicker extends StatelessWidget { @@ -28,50 +29,74 @@ class StreamVideoPicker extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); + final spacing = context.streamSpacing; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + + Future onPickVideo() async { + final pickedVideo = await runInPermissionRequestLock(() { + return StreamAttachmentHandler.instance.pickVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration, + ); + }); + + return onVideoPicked.call(pickedVideo); + } + return OptionDrawer( child: EndOfFrameCallbackWidget( - child: StreamSvgIcon( - size: 240, - icon: StreamSvgIcons.record, - color: theme.colorTheme.disabled, + child: Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + size: 32, + context.streamIcons.video32, + color: colorScheme.textTertiary, + ), + SizedBox(height: spacing.xs), + Text( + context.translations.takeVideoAndShareLabel, + style: textTheme.bodyDefault.copyWith(color: colorScheme.textSecondary), + textAlign: TextAlign.center, + ), + SizedBox(height: spacing.md), + StreamButton( + type: .outline, + style: .secondary, + onTap: onPickVideo, + label: context.translations.openCameraLabel, + ), + ], + ), ), - onEndOfFrame: (_) async { - final pickedVideo = await runInPermissionRequestLock(() { - return StreamAttachmentHandler.instance.pickVideo( - source: source, - preferredCameraDevice: preferredCameraDevice, - maxDuration: maxDuration, - ); - }); - - onVideoPicked.call(pickedVideo); - }, + onEndOfFrame: (_) => onPickVideo(), errorBuilder: (context, error, stacktrace) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - StreamSvgIcon( - size: 240, - icon: StreamSvgIcons.record, - color: theme.colorTheme.disabled, + Icon( + size: 32, + context.streamIcons.video32, + color: colorScheme.textTertiary, ), + SizedBox(height: spacing.xs), Text( context.translations.enablePhotoAndVideoAccessMessage, - style: theme.textTheme.body.copyWith( - color: theme.colorTheme.textLowEmphasis, + style: textTheme.bodyDefault.copyWith( + color: colorScheme.textSecondary, ), textAlign: TextAlign.center, ), - const SizedBox(height: 8), - TextButton( - onPressed: PhotoManager.openSetting, - child: Text( - context.translations.allowGalleryAccessMessage, - style: theme.textTheme.bodyBold.copyWith( - color: theme.colorTheme.accentPrimary, - ), - ), + SizedBox(height: spacing.md), + StreamButton( + type: .outline, + style: .secondary, + onTap: PhotoManager.openSetting, + label: context.translations.allowGalleryAccessMessage, ), ], ); 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..c60bad525c 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,468 +1,128 @@ 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'; +import 'package:stream_core_flutter/stream_core_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]. +/// Inline widget for the system attachment picker interface. /// -/// 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({ +/// Shows a list of options that launch native platform dialogs. +/// Selections are applied in real-time via the [controller]. +class StreamSystemAttachmentPicker extends StatelessWidget { + /// Creates a new instance of [StreamSystemAttachmentPicker]. + const StreamSystemAttachmentPicker({ 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 ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + final enabledTypes = value.filterEnabledTypes(options: options); - return ListTile( - enabled: enabled, - leading: option.icon, - title: Text(option.title), - onTap: onOptionTap, - ); - }), - ], + return ListView( + shrinkWrap: true, + children: [ + ...options.map( + (option) { + final supported = option.supportedTypes; + final isEnabled = enabledTypes.any(supported.contains); + + return ListTile( + enabled: isEnabled, + leading: Icon(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({ +/// Inline widget for the tabbed attachment picker interface. +/// +/// Displays a tabbed interface with horizontal tabs for different attachment +/// types (gallery, camera, files, etc.). Each tab shows a specialized +/// interface for selecting that type of attachment. +/// +/// Selections are applied in real-time via the [controller] rather than +/// through a modal result pattern. +class StreamTabbedAttachmentPicker extends StatefulWidget { + /// Creates a new instance of [StreamTabbedAttachmentPicker]. + const StreamTabbedAttachmentPicker({ super.key, required this.options, required this.controller, this.initialOption, - this.onSendValue, }); /// 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; - @override - State createState() => - _StreamMobileAttachmentPickerBottomSheetState(); + State createState() => _StreamTabbedAttachmentPickerState(); } -class _StreamMobileAttachmentPickerBottomSheetState - extends State { - late AttachmentPickerOption _currentOption; +class _StreamTabbedAttachmentPickerState extends State { + 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( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - _AttachmentPickerOptions( + _TabbedAttachmentPickerOptions( controller: widget.controller, options: widget.options, currentOption: _currentOption, - onSendValue: widget.onSendValue, onOptionSelected: (option) async { setState(() => _currentOption = option); }, ), Expanded( - child: _currentOption.optionViewBuilder - ?.call(context, widget.controller) ?? - const Empty(), + child: _currentOption.optionViewBuilder( + context, + widget.controller, + ), ), ], ); @@ -471,82 +131,58 @@ class _StreamMobileAttachmentPickerBottomSheetState } } -class _AttachmentPickerOptions extends StatelessWidget { - const _AttachmentPickerOptions({ +class _TabbedAttachmentPickerOptions extends StatelessWidget { + const _TabbedAttachmentPickerOptions({ required this.options, required this.currentOption, required this.controller, this.onOptionSelected, - 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; @override Widget build(BuildContext context) { - final colorTheme = StreamChatTheme.of(context).colorTheme; + final spacing = context.streamSpacing; + return ValueListenableBuilder( valueListenable: controller, - builder: (context, attachments, __) { - final enabledTypes = controller.filterEnabledTypes(options: options); - return Row( - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - ...options.map( - (option) { - final supportedTypes = option.supportedTypes; - - final isSelected = option == currentOption; - final isEnabled = enabledTypes.isEmpty || - enabledTypes.any(supportedTypes.contains); - - final color = isSelected - ? colorTheme.accentPrimary - : colorTheme.textLowEmphasis; - - final onPressed = - isEnabled ? () => onOptionSelected!(option) : null; - - return IconButton( - color: color, - disabledColor: colorTheme.disabled, - icon: option.icon, - onPressed: onPressed, - ); - }, - ), - ], - ), + builder: (context, value, __) { + final enabledTypes = value.filterEnabledTypes(options: options); + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: spacing.md, vertical: spacing.sm), + child: Row( + spacing: spacing.xxxs, + children: [ + ...options.map( + (option) { + final supported = option.supportedTypes; + // An option with no supportedTypes is always enabled. + final isEnabled = supported.isEmpty || enabledTypes.any(supported.contains); + final isSelected = option == currentOption; + + final onPressed = switch (isEnabled) { + true => () => onOptionSelected?.call(option), + _ => null, + }; + + return StreamButton.icon( + style: StreamButtonStyle.secondary, + type: StreamButtonType.ghost, + size: StreamButtonSize.large, + icon: option.icon, + onTap: onPressed, + isSelected: isSelected, + ); + }, ), - ), - Builder( - builder: (context) { - final VoidCallback? onPressed; - if (onSendValue != null && controller.isValueChanged) { - onPressed = () => onSendValue!(attachments); - } else { - onPressed = null; - } - - return IconButton( - color: colorTheme.accentPrimary, - disabledColor: colorTheme.disabled, - icon: const StreamSvgIcon( - icon: StreamSvgIcons.emptyCircleRight, - ), - onPressed: onPressed, - ); - }, - ), - ], + ], + ), ); }, ); @@ -555,11 +191,12 @@ class _AttachmentPickerOptions extends StatelessWidget { /// Signature used by [EndOfFrameCallbackWidget.errorBuilder] to create a /// replacement widget to render. -typedef EndOfFrameCallbackErrorWidgetBuilder = Widget Function( - BuildContext context, - Object error, - StackTrace? stackTrace, -); +typedef EndOfFrameCallbackErrorWidgetBuilder = + Widget Function( + BuildContext context, + Object error, + StackTrace? stackTrace, + ); /// Function signature for a callback that is called when the end of the frame /// is reached. @@ -579,7 +216,7 @@ class EndOfFrameCallbackWidget extends StatefulWidget { /// The widget below this widget in the tree. final Widget? child; - /// The callback that is called when the end of the frame is reached.x + /// The callback that is called when the end of the frame is reached. final EndOfFrameCallback onEndOfFrame; /// The callback that will be called if the [onEndOfFrame] callback throws an @@ -587,8 +224,7 @@ class EndOfFrameCallbackWidget extends StatefulWidget { final EndOfFrameCallbackErrorWidgetBuilder? errorBuilder; @override - State createState() => - _EndOfFrameCallbackWidgetState(); + State createState() => _EndOfFrameCallbackWidgetState(); } class _EndOfFrameCallbackWidgetState extends State { @@ -625,8 +261,6 @@ class _EndOfFrameCallbackWidgetState extends State { return const Text('An error occurred'); } - // Reset the error and stack trace so that we don't keep showing the same - // error over and over. _error = null; _stackTrace = null; @@ -634,13 +268,6 @@ class _EndOfFrameCallbackWidgetState extends State { } } -const _kDefaultOptionDrawerShape = RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), -); - /// A widget that will be shown in the attachment picker. /// It can be used to show a custom view for each attachment picker option. class OptionDrawer extends StatelessWidget { @@ -648,317 +275,283 @@ class OptionDrawer extends StatelessWidget { const OptionDrawer({ super.key, required this.child, - this.color, - this.elevation = 2, - this.margin = EdgeInsets.zero, - this.clipBehavior = Clip.hardEdge, - this.shape = _kDefaultOptionDrawerShape, - this.title, - this.actions = const [], + this.margin, }); /// The widget below this widget in the tree. final Widget child; - /// The background color of the options card. - /// - /// Defaults to [StreamColorTheme.barsBg]. - final Color? color; - - /// The elevation of the options card. - /// - /// The default value is 2. - final double elevation; - /// The margin of the options card. - /// - /// The default value is [EdgeInsets.zero]. - final EdgeInsetsGeometry margin; - - /// The clip behavior of the options card. - /// - /// The default value is [Clip.hardEdge]. - final Clip clipBehavior; - - /// The shape of the options card. - final ShapeBorder shape; - - /// The title of the options card. - final Widget? title; - - /// The actions available for the options card. - final List actions; + final EdgeInsetsGeometry? margin; @override Widget build(BuildContext context) { - final colorTheme = StreamChatTheme.of(context).colorTheme; + final spacing = context.streamSpacing; + final effectiveMargin = margin ?? .symmetric(horizontal: spacing.md, vertical: spacing.xxxl); - var height = 20.0; - if (title != null || actions.isNotEmpty) { - height = 40.0; - } - - final leading = title ?? const Empty(); - - Widget trailing; - if (actions.isNotEmpty) { - trailing = Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: actions, - ); - } else { - trailing = const Empty(); - } - - return Card( - elevation: elevation, - color: color ?? colorTheme.barsBg, - margin: margin, - shape: shape, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: height, - child: Row( - children: [ - Expanded(child: leading), - Container( - height: 4, - width: 40, - decoration: BoxDecoration( - color: colorTheme.disabled, - borderRadius: BorderRadius.circular(6), - ), - ), - Expanded(child: trailing), - ], - ), - ), - Expanded(child: child), - ], - ), + return Container( + margin: effectiveMargin, + child: child, ); } } -/// 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. +/// +/// Selections are applied in real-time via the [controller]. +/// +/// The [onError] callback is invoked when an error occurs during attachment +/// selection (e.g., file too large or attachment limit reached). +/// +/// The [onPollCreated] callback is invoked when a poll is created, allowing +/// the caller to handle poll-specific logic. +Widget tabbedAttachmentPickerBuilder({ required BuildContext context, required StreamAttachmentPickerController controller, - Poll? initialPoll, PollConfig? pollConfig, - Iterable? customOptions, + GalleryPickerConfig? galleryPickerConfig, List allowedTypes = AttachmentPickerType.values, - ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400), - ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg, - int attachmentThumbnailQuality = 100, - double attachmentThumbnailScale = 1, - ErrorListener? onError, + AttachmentPickerOptionsBuilder? optionsBuilder, + ValueSetter? onError, + ValueSetter? onPollCreated, + ValueSetter? onCommandSelected, }) { - return StreamMobileAttachmentPickerBottomSheet( - controller: controller, - onSendValue: Navigator.of(context).pop, - options: { - ...{ - if (customOptions != null) ...customOptions, - AttachmentPickerOption( - key: 'gallery-picker', - icon: const StreamSvgIcon(icon: StreamSvgIcons.pictures), - supportedTypes: [ - AttachmentPickerType.images, - AttachmentPickerType.videos, - ], - optionViewBuilder: (context, controller) { - final attachment = controller.value.attachments; - final selectedIds = attachment.map((it) => it.id); - return StreamGalleryPicker( - selectedMediaItems: selectedIds, - mediaThumbnailSize: attachmentThumbnailSize, - mediaThumbnailFormat: attachmentThumbnailFormat, - mediaThumbnailQuality: attachmentThumbnailQuality, - mediaThumbnailScale: attachmentThumbnailScale, - onMediaItemSelected: (media) async { - try { - if (selectedIds.contains(media.id)) { - return await controller.removeAssetAttachment(media); - } - return await controller.addAssetAttachment(media); - } catch (e, stk) { - if (onError != null) return onError.call(e, stk); - rethrow; - } - }, - ); - }, - ), - AttachmentPickerOption( - 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); - - rethrow; - } - }, - ); + final defaultOptions = [ + TabbedAttachmentPickerOption( + key: 'gallery-picker', + icon: context.streamIcons.image20, + supportedTypes: [ + AttachmentPickerType.images, + AttachmentPickerType.videos, + ], + optionViewBuilder: (context, controller) { + final attachment = controller.value.attachments; + final selectedIds = attachment.map((it) => it.id); + return StreamGalleryPicker( + config: galleryPickerConfig, + selectedMediaItems: selectedIds, + onMediaItemSelected: (media) async { + try { + if (selectedIds.contains(media.id)) { + return await controller.removeAssetAttachment(media); + } + return await controller.addAssetAttachment(media); + } catch (e, stk) { + onError?.call( + AttachmentPickerError(error: e, stackTrace: stk), + ); + } }, - ), - AttachmentPickerOption( - 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); - - rethrow; - } - }, + ); + }, + ), + TabbedAttachmentPickerOption( + key: 'file-picker', + icon: context.streamIcons.file20, + supportedTypes: [AttachmentPickerType.files], + optionViewBuilder: (context, controller) => StreamFilePicker( + onFilePicked: (file) async { + try { + if (file != null) await controller.addAttachment(file); + } catch (e, stk) { + onError?.call( + AttachmentPickerError(error: e, stackTrace: stk), ); - }, - ), - AttachmentPickerOption( - 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); - - rethrow; - } - }, + } + }, + ), + ), + TabbedAttachmentPickerOption( + key: 'image-picker', + icon: context.streamIcons.camera20, + supportedTypes: [AttachmentPickerType.images], + optionViewBuilder: (context, controller) => StreamImagePicker( + onImagePicked: (image) async { + try { + if (image != null) await controller.addAttachment(image); + } catch (e, stk) { + onError?.call( + AttachmentPickerError(error: e, stackTrace: stk), ); - }, - ), - AttachmentPickerOption( - key: 'poll-creator', - icon: const StreamSvgIcon(icon: StreamSvgIcons.polls), - supportedTypes: [AttachmentPickerType.poll], - 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); - - rethrow; - } - }, + } + }, + ), + ), + TabbedAttachmentPickerOption( + key: 'video-picker', + icon: context.streamIcons.video20, + supportedTypes: [AttachmentPickerType.videos], + optionViewBuilder: (context, controller) => StreamVideoPicker( + onVideoPicked: (video) async { + try { + if (video != null) await controller.addAttachment(video); + } catch (e, stk) { + onError?.call( + AttachmentPickerError(error: e, stackTrace: stk), ); + } + }, + ), + ), + TabbedAttachmentPickerOption( + key: 'poll-creator', + icon: context.streamIcons.poll20, + supportedTypes: [AttachmentPickerType.poll], + optionViewBuilder: (context, controller) { + final initialPoll = controller.value.poll; + return StreamPollCreator( + poll: initialPoll, + config: pollConfig, + onPollCreated: (poll) { + if (poll == null) return; + controller.poll = poll; + onPollCreated?.call(poll); }, - ), - }.where((option) => option.supportedTypes.every(allowedTypes.contains)), + ); + }, + ), + TabbedAttachmentPickerOption( + key: 'command-picker', + icon: context.streamIcons.command20, + supportedTypes: [AttachmentPickerType.command], + optionViewBuilder: (context, controller) => StreamCommandPicker( + onCommandSelected: onCommandSelected, + ), + ), + ]; + + final allOptions = switch (optionsBuilder) { + final builder? => builder(context, defaultOptions), + _ => defaultOptions, + }; + + final validOptions = allOptions.whereType(); + + if (validOptions.length < allOptions.length) { + throw ArgumentError( + 'custom options must be of type TabbedAttachmentPickerOption when using ' + 'the tabbed attachment picker (default on mobile).', + ); + } + + return StreamTabbedAttachmentPicker( + controller: controller, + options: { + ...validOptions.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. +/// +/// Selections are applied in real-time via the [controller]. +/// +/// The [onError] callback is invoked when an error occurs during attachment +/// selection. +/// +/// The [onPollCreated] callback is invoked when a poll is created. +Widget systemAttachmentPickerBuilder({ required BuildContext context, required StreamAttachmentPickerController controller, - Poll? initialPoll, - PollConfig? pollConfig, - Iterable? customOptions, + PollConfig? pollConfig = const PollConfig(), + GalleryPickerConfig? galleryPickerConfig = const GalleryPickerConfig(), List allowedTypes = AttachmentPickerType.values, - ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400), - ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg, - int attachmentThumbnailQuality = 100, - double attachmentThumbnailScale = 1, - ErrorListener? onError, + AttachmentPickerOptionsBuilder? optionsBuilder, + ValueSetter? onError, + ValueSetter? onPollCreated, }) { - return StreamWebOrDesktopAttachmentPickerBottomSheet( - controller: controller, - options: { - ...{ - if (customOptions != null) ...customOptions, - WebOrDesktopAttachmentPickerOption( - key: 'image-picker', - type: AttachmentPickerType.images, - icon: const StreamSvgIcon(icon: StreamSvgIcons.pictures), - title: context.translations.uploadAPhotoLabel, - ), - WebOrDesktopAttachmentPickerOption( - key: 'video-picker', - type: AttachmentPickerType.videos, - icon: const StreamSvgIcon(icon: StreamSvgIcons.record), - title: context.translations.uploadAVideoLabel, - ), - WebOrDesktopAttachmentPickerOption( - key: 'file-picker', - type: AttachmentPickerType.files, - icon: const StreamSvgIcon(icon: StreamSvgIcons.files), - title: context.translations.uploadAFileLabel, - ), - WebOrDesktopAttachmentPickerOption( - key: 'poll-creator', - type: 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) { + Future pickSystemFile( + StreamAttachmentPickerController controller, + FileType type, + ) async { + try { + final file = await StreamAttachmentHandler.instance.pickFile(type: type); + if (file != null) await controller.addAttachment(file); + } catch (e, stk) { + onError?.call(AttachmentPickerError(error: e, stackTrace: stk)); + } + } + + final defaultOptions = [ + SystemAttachmentPickerOption( + key: 'image-picker', + supportedTypes: [AttachmentPickerType.images], + icon: context.streamIcons.image20, + title: context.translations.uploadAPhotoLabel, + onTap: (context, controller) async { + await pickSystemFile(controller, FileType.image); + }, + ), + SystemAttachmentPickerOption( + key: 'video-picker', + supportedTypes: [AttachmentPickerType.videos], + icon: context.streamIcons.video20, + title: context.translations.uploadAVideoLabel, + onTap: (context, controller) async { + await pickSystemFile(controller, FileType.video); + }, + ), + SystemAttachmentPickerOption( + key: 'file-picker', + supportedTypes: [AttachmentPickerType.files], + icon: context.streamIcons.file20, + title: context.translations.uploadAFileLabel, + onTap: (context, controller) async { + await pickSystemFile(controller, FileType.any); + }, + ), + SystemAttachmentPickerOption( + key: 'poll-creator', + supportedTypes: [AttachmentPickerType.poll], + icon: context.streamIcons.poll20, + title: context.translations.createPollLabel(isNew: true), + onTap: (context, controller) async { + final initialPoll = controller.value.poll; final poll = await showStreamPollCreatorDialog( context: context, poll: initialPoll, config: pollConfig, ); - if (poll != null) controller.poll = poll; - return Navigator.pop(context, controller.value); - } + if (poll == null) return; + controller.poll = poll; + onPollCreated?.call(poll); + }, + ), + ]; - // 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); + final allOptions = switch (optionsBuilder) { + final builder? => builder(context, defaultOptions), + _ => defaultOptions, + }; - rethrow; - } + final validOptions = allOptions.whereType(); + + if (validOptions.length < allOptions.length) { + throw ArgumentError( + 'custom options must be of type SystemAttachmentPickerOption when using ' + 'the system attachment picker (enabled explicitly or on web/desktop).', + ); + } + + return StreamSystemAttachmentPicker( + controller: controller, + options: { + ...validOptions.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..4ae640eb6d 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,261 +1,12 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// Shows a modal material design bottom sheet. -/// -/// 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. -/// -/// 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. -/// -/// 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. -/// -/// 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. -/// -/// 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]. -/// -/// The [isDismissible] parameter specifies whether the bottom sheet will be -/// dismissed when user taps on the scrim. -/// -/// 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. -/// * -Future showStreamAttachmentPickerModalBottomSheet({ - required BuildContext context, - Iterable? customOptions, - List allowedTypes = AttachmentPickerType.values, - Poll? initialPoll, - PollConfig? pollConfig, - List? initialAttachments, - StreamAttachmentPickerController? controller, - ErrorListener? onError, - Color? backgroundColor, - double? elevation, - BoxConstraints? constraints, - Color? barrierColor, - bool isScrollControlled = false, - bool useRootNavigator = false, - 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( - context: context, - backgroundColor: color, - elevation: elevation, - shape: shape, - clipBehavior: clipBehavior, - constraints: constraints, - barrierColor: barrierColor, - isScrollControlled: isScrollControlled, - useRootNavigator: useRootNavigator, - isDismissible: isDismissible, - enableDrag: enableDrag, - routeSettings: routeSettings, - transitionAnimationController: transitionAnimationController, - builder: (BuildContext context) { - return StreamPlatformAttachmentPickerBottomSheetBuilder( - controller: controller, - initialPoll: initialPoll, - initialAttachments: initialAttachments, - builder: (context, controller, child) { - final isWebOrDesktop = switch (CurrentPlatform.type) { - PlatformType.web || - PlatformType.macOS || - PlatformType.linux || - PlatformType.windows => - true, - _ => false, - }; - - final useSystemPicker = useSystemAttachmentPicker || // - useNativeAttachmentPickerOnMobile; - - if (useSystemPicker || isWebOrDesktop) { - return webOrDesktopAttachmentPickerBuilder.call( - context: context, - onError: onError, - controller: controller, - allowedTypes: allowedTypes, - customOptions: customOptions?.map( - WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption, - ), - initialPoll: initialPoll, - pollConfig: pollConfig, - attachmentThumbnailSize: attachmentThumbnailSize, - attachmentThumbnailFormat: attachmentThumbnailFormat, - attachmentThumbnailQuality: attachmentThumbnailQuality, - attachmentThumbnailScale: attachmentThumbnailScale, - ); - } - - return mobileAttachmentPickerBuilder.call( - context: context, - onError: onError, - controller: controller, - allowedTypes: allowedTypes, - customOptions: customOptions, - initialPoll: initialPoll, - pollConfig: pollConfig, - attachmentThumbnailSize: attachmentThumbnailSize, - attachmentThumbnailFormat: attachmentThumbnailFormat, - attachmentThumbnailQuality: attachmentThumbnailQuality, - attachmentThumbnailScale: attachmentThumbnailScale, - ); - }, - ); - }, - ); -} - -/// Builds the attachment picker bottom sheet. -class StreamPlatformAttachmentPickerBottomSheetBuilder extends StatefulWidget { - /// Creates a new instance of the widget. - const StreamPlatformAttachmentPickerBottomSheetBuilder({ - super.key, - this.customOptions, - this.initialPoll, - this.initialAttachments, - this.child, - this.controller, - required this.builder, - }); - - /// The child widget. - final Widget? child; - - /// Builder for the attachment picker bottom sheet. - final Widget Function( - BuildContext context, - StreamAttachmentPickerController controller, - 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 controller. - final StreamAttachmentPickerController? controller; - - @override - State createState() => - _StreamPlatformAttachmentPickerBottomSheetBuilderState(); -} - -class _StreamPlatformAttachmentPickerBottomSheetBuilderState - extends State { - late StreamAttachmentPickerController _controller; - - @override - void initState() { - super.initState(); - _controller = widget.controller ?? - StreamAttachmentPickerController( - initialPoll: widget.initialPoll, - initialAttachments: widget.initialAttachments, - ); - } - - // Handle a potential change in StreamAttachmentPickerController by properly - // disposing of the old one and setting up the new one, if needed. - void _updateAttachmentPickerController( - StreamAttachmentPickerController? old, - StreamAttachmentPickerController? current, - ) { - if ((old == null && current == null) || old == current) return; - if (old == null) { - _controller.dispose(); - _controller = current!; - } else if (current == null) { - _controller = StreamAttachmentPickerController( - initialPoll: widget.initialPoll, - initialAttachments: widget.initialAttachments, - ); - } else { - _controller = current; - } - } - - @override - void didUpdateWidget( - StreamPlatformAttachmentPickerBottomSheetBuilder oldWidget, - ) { - super.didUpdateWidget(oldWidget); - _updateAttachmentPickerController( - oldWidget.controller, - widget.controller, - ); - } - - @override - void dispose() { - if (widget.controller == null) _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return widget.builder(context, _controller, widget.child); - } -} +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/message_input/attachment_picker/stream_attachment_picker_option.dart'; + +/// {@template streamAttachmentPickerOptionsBuilder} +/// Signature for a function that creates a list of [AttachmentPickerOption]s +/// to be used in the attachment picker. +/// +/// The function receives the [BuildContext] and a list of [defaultOptions] +/// that can be modified or extended. +/// {@endtemplate} +typedef AttachmentPickerOptionsBuilder = + List Function(BuildContext context, List defaultOptions); 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..f111ad5a33 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_controller.dart @@ -0,0 +1,342 @@ +import 'dart:async'; + +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/src/message_input/attachment_picker/stream_attachment_picker_result.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); + + final _customResultController = StreamController.broadcast(); + + /// 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 AttachmentLimitReachedError(maxCount: 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); + } + + /// A stream of custom attachment picker results emitted via + /// [notifyCustomResult]. + Stream get customResults => _customResultController.stream; + + /// Emits a [CustomAttachmentPickerResult] to notify the parent widget + /// (e.g. [StreamMessageInput]) that a custom attachment has been picked. + /// + /// Use this from a [TabbedAttachmentPickerOption.optionViewBuilder] instead + /// of calling `Navigator.pop` — the picker is an inline widget, not a modal + /// route, so popping the navigator would close the wrong page. + void notifyCustomResult(CustomAttachmentPickerResult result) { + if (!_customResultController.isClosed) _customResultController.add(result); + } + + @override + void dispose() { + _customResultController.close(); + super.dispose(); + } + + 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 AttachmentTooLargeError( + fileSize: attachment.fileSize!, + maxSize: maxAttachmentSize, + ); + } + + 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; + } +} + +const _nullConst = Object(); + +/// 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({ + Object? poll = _nullConst, + List? attachments, + Map? extraData, + }) { + return AttachmentPickerValue( + poll: poll == _nullConst ? this.poll : poll as 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; + } +} + +/// Error thrown when an attachment exceeds the maximum allowed file size. +/// +/// This occurs when calling [StreamAttachmentPickerController.addAttachment] +/// with a file whose size is greater than [maxAttachmentSize]. +/// +/// The error includes both the actual file size and the configured limit, +/// allowing you to provide specific feedback about the size violation. +class AttachmentTooLargeError extends StreamChatError { + /// Creates a new [AttachmentTooLargeError]. + const AttachmentTooLargeError({ + required this.fileSize, + required this.maxSize, + }) : super( + 'The size of the attachment is $fileSize bytes, ' + 'but the maximum size allowed is $maxSize bytes.', + ); + + /// The actual size of the attachment in bytes. + final int fileSize; + + /// The maximum allowed size in bytes. + final int maxSize; + + @override + List get props => [...super.props, fileSize, maxSize]; + + @override + String toString() => + 'AttachmentTooLargeError: ' + 'The size of the attachment is $fileSize bytes, ' + 'but the maximum size allowed is $maxSize bytes.'; +} + +/// Error thrown when the attachment count exceeds the maximum allowed. +/// +/// This occurs when setting [StreamAttachmentPickerController.value] with +/// more attachments than [maxAttachmentCount] allows. +/// +/// The error includes the configured attachment limit. +class AttachmentLimitReachedError extends StreamChatError { + /// Creates a new [AttachmentLimitReachedError]. + const AttachmentLimitReachedError({ + required this.maxCount, + }) : super('The maximum number of attachments is $maxCount.'); + + /// The maximum allowed number of attachments. + final int maxCount; + + @override + List get props => [...super.props, maxCount]; + + @override + String toString() => + 'AttachmentLimitReachedError: ' + 'The maximum number of attachments is $maxCount.'; +} 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..6001a5405f --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_option.dart @@ -0,0 +1,209 @@ +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 IconData 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(); + + /// The option will allow to pick commands. + static const command = CommandPickerType(); + + /// A list of all predefined attachment picker types. + static const values = [images, videos, audios, files, poll, command]; +} + +/// 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 predefined attachment picker type that allows picking commands. +final class CommandPickerType extends AttachmentPickerType { + /// Creates a new command picker type. + const CommandPickerType(); +} + +/// 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..dc4bbf563c --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/attachment_picker/stream_attachment_picker_result.dart @@ -0,0 +1,54 @@ +import 'dart:async'; + +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Signature for a function that is called when a attachment picker result +/// is received. +typedef OnAttachmentPickerResult = FutureOr 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/audio_recorder/audio_recorder_controller.dart b/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_controller.dart index 1c34f3b9fc..ba5fec625f 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_controller.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_controller.dart @@ -30,9 +30,9 @@ class StreamAudioRecorderController extends ValueNotifier { config: switch (config) { final config? => config, _ => const RecordConfig( - numChannels: 1, - encoder: kIsWeb ? AudioEncoder.wav : AudioEncoder.aacLc, - ), + numChannels: 1, + encoder: kIsWeb ? AudioEncoder.wav : AudioEncoder.aacLc, + ), }, ); } @@ -44,8 +44,8 @@ class StreamAudioRecorderController extends ValueNotifier { required AudioRecorder recorder, AudioRecorderState initialState = const RecordStateIdle(), Duration amplitudeInterval = const Duration(milliseconds: 100), - }) : _recorder = recorder, - super(initialState) { + }) : _recorder = recorder, + super(initialState) { // Listen to the recorder amplitude changes _recorderAmplitudeSubscription = _recorder .onAmplitudeChanged(amplitudeInterval) // @@ -61,8 +61,13 @@ class StreamAudioRecorderController extends ValueNotifier { // Only start the recorder if it is currently idle. if (value case RecordStateIdle()) { // Return if the recorder does not have permission to record audio. - final hasPermission = await _recorder.hasPermission(); - if (!hasPermission) return; + final hasPermission = await _recorder.hasPermission(request: false); + if (!hasPermission) { + /// Request permission to record audio. + /// User has to start the recording session again to record audio. + await _recorder.hasPermission(request: true); + return; + } // Start the recording session. final tempPath = await _getOutputFilePath(config.encoder); diff --git a/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_feedback.dart b/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_feedback.dart index 9bf98d2e1b..d33ce45657 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_feedback.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_feedback.dart @@ -175,13 +175,13 @@ class AudioRecorderFeedbackWrapper extends AudioRecorderFeedback { _FeedbackCallback? onCancel, _FeedbackCallback? onStartCancel, _FeedbackCallback? onStop, - }) : _onStop = onStop, - _onStartCancel = onStartCancel, - _onCancel = onCancel, - _onLock = onLock, - _onFinish = onFinish, - _onPause = onPause, - _onStart = onStart; + }) : _onStop = onStop, + _onStartCancel = onStartCancel, + _onCancel = onCancel, + _onLock = onLock, + _onFinish = onFinish, + _onPause = onPause, + _onStart = onStart; // Callback for when recording starts. final _FeedbackCallback? _onStart; diff --git a/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_state.dart b/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_state.dart index 95bdeff0bb..96bb1f00cc 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_state.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/audio_recorder_state.dart @@ -51,13 +51,13 @@ sealed class RecordStateRecording extends AudioRecorderState { }) { return switch (this) { RecordStateRecordingHold() => RecordStateRecordingHold( - duration: duration ?? this.duration, - waveform: waveform ?? this.waveform, - ), + duration: duration ?? this.duration, + waveform: waveform ?? this.waveform, + ), RecordStateRecordingLocked() => RecordStateRecordingLocked( - duration: duration ?? this.duration, - waveform: waveform ?? this.waveform, - ), + duration: duration ?? this.duration, + waveform: waveform ?? this.waveform, + ), }; } } diff --git a/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/stream_audio_recorder.dart b/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/stream_audio_recorder.dart index 241b1f5efb..4a203fd1be 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/stream_audio_recorder.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/audio_recorder/stream_audio_recorder.dart @@ -5,14 +5,13 @@ import 'package:flutter_portal/flutter_portal.dart'; import 'package:stream_chat_flutter/src/audio/audio_playlist_controller.dart'; import 'package:stream_chat_flutter/src/audio/audio_playlist_state.dart'; import 'package:stream_chat_flutter/src/audio/audio_sampling.dart'; -import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/message_input/audio_recorder/audio_recorder_feedback.dart'; import 'package:stream_chat_flutter/src/message_input/audio_recorder/audio_recorder_state.dart'; import 'package:stream_chat_flutter/src/message_input/stream_message_input_icon_button.dart'; -import 'package:stream_chat_flutter/src/misc/audio_waveform.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_core_flutter/stream_core_flutter.dart'; /// {@template audioRecorderBuilder} /// A builder function for constructing the audio recorder UI. @@ -21,11 +20,12 @@ import 'package:stream_chat_flutter/src/utils/extensions.dart'; /// - [StreamAudioRecorderButton], which uses this builder function. /// - [StreamAudioRecorderState], which provides the state of the recorder. /// {@endtemplate} -typedef AudioRecorderBuilder = Widget Function( - BuildContext, - AudioRecorderState, - Widget, -); +typedef AudioRecorderBuilder = + Widget Function( + BuildContext, + AudioRecorderState, + Widget, + ); /// {@template streamAudioRecorderButton} /// A configurable audio recording button with interactive states and gestures. @@ -172,49 +172,49 @@ class StreamAudioRecorderButton extends StatelessWidget { state: recordState, button: RecordButton( onPressed: () {}, // Allows showing ripple effect on tap. - icon: const StreamSvgIcon(icon: StreamSvgIcons.mic), + icon: Icon(context.streamIcons.voice20), ), builder: (context, state, recordButton) => switch (state) { // Show only the record button if the recording is not in progress. RecordStateIdle() => RecordStateIdleContent( - state: state, - recordButton: recordButton, - ), + state: state, + recordButton: recordButton, + ), RecordStateRecordingHold() => RecordStateHoldRecordingContent( - state: state, - recordButton: recordButton, - cancelThreshold: cancelRecordThreshold, - ), + state: state, + recordButton: recordButton, + cancelThreshold: cancelRecordThreshold, + ), RecordStateRecordingLocked() => RecordStateLockedRecordingContent( - state: state, - onRecordEnd: () async { - await feedback.onRecordFinish(context); - return onRecordFinish?.call(); - }, - onRecordPause: () async { - await feedback.onRecordPause(context); - return onRecordPause?.call(); - }, - onRecordCancel: () async { - await feedback.onRecordCancel(context); - return onRecordCancel?.call(); - }, - onRecordStop: () async { - await feedback.onRecordStop(context); - return onRecordStop?.call(); - }, - ), + state: state, + onRecordEnd: () async { + await feedback.onRecordFinish(context); + return onRecordFinish?.call(); + }, + onRecordPause: () async { + await feedback.onRecordPause(context); + return onRecordPause?.call(); + }, + onRecordCancel: () async { + await feedback.onRecordCancel(context); + return onRecordCancel?.call(); + }, + onRecordStop: () async { + await feedback.onRecordStop(context); + return onRecordStop?.call(); + }, + ), RecordStateStopped() => RecordStateStoppedContent( - state: state, - onRecordCancel: () async { - await feedback.onRecordCancel(context); - return onRecordCancel?.call(); - }, - onRecordFinish: () async { - await feedback.onRecordFinish(context); - return onRecordFinish?.call(); - }, - ), + state: state, + onRecordCancel: () async { + await feedback.onRecordCancel(context); + return onRecordCancel?.call(); + }, + onRecordFinish: () async { + await feedback.onRecordFinish(context); + return onRecordFinish?.call(); + }, + ), }, ), ); @@ -448,20 +448,20 @@ class RecordStateLockedRecordingContent extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ StreamMessageInputIconButton( - icon: const StreamSvgIcon(icon: StreamSvgIcons.delete), + icon: Icon(context.streamIcons.delete20), color: theme.colorTheme.accentPrimary, onPressed: onRecordCancel, ), StreamMessageInputIconButton( - icon: const StreamSvgIcon(icon: StreamSvgIcons.stop), + icon: Icon(context.streamIcons.stopFill20), color: theme.colorTheme.accentError, onPressed: onRecordStop, ), StreamMessageInputIconButton( - icon: const StreamSvgIcon(icon: StreamSvgIcons.checkSend), + icon: Icon(context.streamIcons.checkmark20), color: theme.colorTheme.accentPrimary, onPressed: onRecordEnd, - ) + ), ], ), ], @@ -498,8 +498,7 @@ class RecordStateStoppedContent extends StatefulWidget { final VoidCallback? onRecordFinish; @override - State createState() => - _RecordStateStoppedContentState(); + State createState() => _RecordStateStoppedContentState(); } class _RecordStateStoppedContentState extends State { @@ -607,15 +606,15 @@ class _RecordStateStoppedContentState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ StreamMessageInputIconButton( - icon: const StreamSvgIcon(icon: StreamSvgIcons.delete), + icon: Icon(context.streamIcons.delete20), color: theme.colorTheme.accentPrimary, onPressed: widget.onRecordCancel, ), StreamMessageInputIconButton( - icon: const StreamSvgIcon(icon: StreamSvgIcons.checkSend), + icon: Icon(context.streamIcons.checkmark20), color: theme.colorTheme.accentPrimary, onPressed: widget.onRecordFinish, - ) + ), ], ), ], @@ -643,29 +642,31 @@ class SwipeToLockButton extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); + final streamIcons = context.streamIcons; + final colorScheme = context.streamColorScheme; + return Container( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), - color: theme.colorTheme.inputBg, + color: colorScheme.backgroundElevation1, + border: Border.all(color: colorScheme.borderDefault), + boxShadow: context.streamBoxShadow.elevation1, ), child: Column( spacing: 8, mainAxisSize: MainAxisSize.min, children: [ - StreamSvgIcon( - icon: StreamSvgIcons.lock, - size: kDefaultMessageInputIconSize, - color: switch (isLocked) { - true => theme.colorTheme.accentPrimary, - false => theme.colorTheme.textLowEmphasis, - }, + Icon( + isLocked ? streamIcons.lock20 : streamIcons.unlock20, + size: 20, + color: colorScheme.textPrimary, ), if (!isLocked) ...[ - StreamSvgIcon( - icon: StreamSvgIcons.up, - color: theme.colorTheme.textLowEmphasis, + Icon( + streamIcons.chevronUp20, + size: 20, + color: colorScheme.textPrimary, ), ], ], @@ -718,24 +719,24 @@ class PlaybackControlButton extends StatelessWidget { }, icon: switch (state) { TrackState.loading => Builder( - builder: (context) { - final iconTheme = IconTheme.of(context); - return SizedBox.fromSize( - size: Size.square(iconTheme.size!), - child: Padding( - padding: const EdgeInsets.all(8), - child: CircularProgressIndicator.adaptive( - valueColor: AlwaysStoppedAnimation( - theme.colorTheme.accentPrimary, - ), + builder: (context) { + final iconTheme = IconTheme.of(context); + return SizedBox.fromSize( + size: Size.square(iconTheme.size!), + child: Padding( + padding: const EdgeInsets.all(8), + child: CircularProgressIndicator.adaptive( + valueColor: AlwaysStoppedAnimation( + theme.colorTheme.accentPrimary, ), ), - ); - }, - ), - TrackState.idle => const StreamSvgIcon(icon: StreamSvgIcons.play), - TrackState.paused => const StreamSvgIcon(icon: StreamSvgIcons.play), - TrackState.playing => const StreamSvgIcon(icon: StreamSvgIcons.pause), + ), + ); + }, + ), + TrackState.idle => Icon(context.streamIcons.playFill20), + TrackState.paused => Icon(context.streamIcons.playFill20), + TrackState.playing => Icon(context.streamIcons.pauseFill20), }, ); } @@ -763,8 +764,8 @@ class PlaybackTimerIndicator extends StatelessWidget { final theme = StreamChatTheme.of(context); return Row( children: [ - StreamSvgIcon( - icon: StreamSvgIcons.mic, + Icon( + context.streamIcons.voice20, size: kDefaultMessageInputIconSize, color: switch (duration.inSeconds) { > 0 => theme.colorTheme.accentError, @@ -854,8 +855,8 @@ class SlideToCancelIndicator extends StatelessWidget { ), ), const SizedBox(width: 8), - StreamSvgIcon( - icon: StreamSvgIcons.left, + Icon( + context.streamIcons.chevronLeft20, color: theme.colorTheme.textLowEmphasis, ), ], @@ -949,13 +950,16 @@ class _SlideTransitionWidgetState extends State @override Widget build(BuildContext context) { - final position = Tween( - begin: widget.begin, - end: widget.end, - ).animate(CurvedAnimation( - parent: _controller, - curve: widget.curve, - )); + final position = + Tween( + begin: widget.begin, + end: widget.end, + ).animate( + CurvedAnimation( + parent: _controller, + curve: widget.curve, + ), + ); return SlideTransition( position: position, @@ -985,8 +989,8 @@ class HoldToRecordInfoTooltip extends StatelessWidget { Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); - const recordButtonWidth = kDefaultMessageInputIconSize + - kDefaultMessageInputIconPadding * 2; // right, left padding. + const recordButtonWidth = + kDefaultMessageInputIconSize + kDefaultMessageInputIconPadding * 2; // right, left padding. const arrowSize = Size(recordButtonWidth / 2, 6); diff --git a/packages/stream_chat_flutter/lib/src/message_input/clear_input_item_button.dart b/packages/stream_chat_flutter/lib/src/message_input/clear_input_item_button.dart index ddfb0c20e0..33b2ebaacb 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/clear_input_item_button.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/clear_input_item_button.dart @@ -34,9 +34,9 @@ class ClearInputItemButton extends StatelessWidget { // ignore: deprecated_member_use _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), child: Center( - child: StreamSvgIcon( + child: Icon( + context.streamIcons.xmark20, size: 24, - icon: StreamSvgIcons.close, color: _streamChatTheme.colorTheme.barsBg, ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_input/command_button.dart b/packages/stream_chat_flutter/lib/src/message_input/command_button.dart index 1e8c050cb0..e20029213c 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/command_button.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/command_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/message_input/stream_message_input_icon_button.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template commandButton} /// The button that allows a user to use commands in a chat. @@ -53,7 +53,7 @@ class CommandButton extends StatelessWidget { color: color, iconSize: size, onPressed: onPressed, - icon: icon ?? const StreamSvgIcon(icon: StreamSvgIcons.lightning), + icon: icon ?? Icon(context.streamIcons.bolt20), ); } } 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/dm_checkbox_list_tile.dart b/packages/stream_chat_flutter/lib/src/message_input/dm_checkbox_list_tile.dart index e9b0d7c372..7307c66ee8 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/dm_checkbox_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/dm_checkbox_list_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.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/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template dmCheckboxListTile} /// A widget that represents a checkbox list tile for direct message input. @@ -25,9 +25,7 @@ class DmCheckboxListTile extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); - final textTheme = theme.textTheme; - final colorTheme = theme.colorTheme; + final textTheme = context.streamTextTheme; const visualDensity = VisualDensity( vertical: VisualDensity.minimumDensity, @@ -35,27 +33,13 @@ class DmCheckboxListTile extends StatelessWidget { ); final checkbox = ExcludeFocus( - child: CheckboxTheme( - data: CheckboxThemeData( - overlayColor: WidgetStatePropertyAll( - colorTheme.accentPrimary.withAlpha(kRadialReactionAlpha), - ), - ), - child: Checkbox( - value: value, - visualDensity: visualDensity, - activeColor: colorTheme.accentPrimary, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - side: BorderSide(width: 2, color: colorTheme.textLowEmphasis), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3)), - onChanged: switch (onChanged) { - final onChanged? => (value) { - if (value == null) return; - return onChanged.call(value); - }, - _ => null, - }, - ), + child: StreamCheckbox( + value: value, + size: StreamCheckboxSize.sm, + onChanged: switch (onChanged) { + final onChanged? => onChanged.call, + _ => null, + }, ), ); @@ -70,10 +54,7 @@ class DmCheckboxListTile extends StatelessWidget { contentPadding: contentPadding, title: Text( context.translations.alsoSendAsDirectMessageLabel, - style: textTheme.footnote.copyWith( - // ignore: deprecated_member_use - color: colorTheme.textHighEmphasis.withOpacity(0.5), - ), + style: textTheme.metadataDefault, ), ), ); 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..bed4365e5f 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 @@ -68,12 +68,9 @@ class StreamQuotedMessageWidget extends StatelessWidget { const SizedBox(width: 8), if (message.user != null) StreamUserAvatar( + size: .sm, user: message.user!, - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - showOnlineStatus: false, + showOnlineIndicator: false, ), ]; return Padding( @@ -113,11 +110,9 @@ class _QuotedMessage extends StatelessWidget { bool get _containsText => message.text?.isNotEmpty == true; - bool get _containsLinkAttachment => - message.attachments.any((it) => it.type == AttachmentType.urlPreview); + bool get _containsLinkAttachment => message.attachments.any((it) => it.type == AttachmentType.urlPreview); - bool get _isGiphy => message.attachments - .any((element) => element.type == AttachmentType.giphy); + bool get _isGiphy => message.attachments.any((element) => element.type == AttachmentType.giphy); bool get _isDeleted => message.isDeleted || message.deletedAt != null; @@ -151,9 +146,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), ), ), ]; @@ -168,19 +171,18 @@ class _QuotedMessage extends StatelessWidget { ), if (msg.text!.isNotEmpty && !_isGiphy) Flexible( - child: textBuilder?.call(context, msg) ?? - StreamMessageText( - message: msg, + child: + textBuilder?.call(context, msg) ?? + StreamMarkdownMessage( + data: msg.replaceMentions().text ?? '', messageTheme: isOnlyEmoji && _containsText ? messageTheme.copyWith( - messageTextStyle: - messageTheme.messageTextStyle?.copyWith( + messageTextStyle: messageTheme.messageTextStyle?.copyWith( fontSize: 32, ), ) : messageTheme.copyWith( - messageTextStyle: - messageTheme.messageTextStyle?.copyWith( + messageTextStyle: messageTheme.messageTextStyle?.copyWith( fontSize: 12, ), ), @@ -216,8 +218,7 @@ class _QuotedMessage extends StatelessWidget { child: Row( spacing: 8, mainAxisSize: MainAxisSize.min, - mainAxisAlignment: - reverse ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisAlignment: reverse ? MainAxisAlignment.end : MainAxisAlignment.start, children: reverse ? children.reversed.toList() : children, ), ); @@ -262,8 +263,7 @@ class _ParseAttachments extends StatelessWidget { var clipBehavior = Clip.none; ShapeDecoration? decoration; - if (attachment.type != AttachmentType.file && - attachment.type != AttachmentType.voiceRecording) { + if (attachment.type != AttachmentType.file && attachment.type != AttachmentType.voiceRecording) { clipBehavior = Clip.hardEdge; decoration = ShapeDecoration( shape: RoundedRectangleBorder( diff --git a/packages/stream_chat_flutter/lib/src/message_input/quoting_message_top_area.dart b/packages/stream_chat_flutter/lib/src/message_input/quoting_message_top_area.dart index 74f26d9dac..292e2663f0 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/quoting_message_top_area.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/quoting_message_top_area.dart @@ -37,7 +37,7 @@ class QuotingMessageTopArea extends StatelessWidget { StreamMessageInputIconButton( iconSize: 24, color: _streamChatTheme.colorTheme.disabled, - icon: const StreamSvgIcon(icon: StreamSvgIcons.reply), + icon: Icon(context.streamIcons.reply20), onPressed: null, ), Text( @@ -47,7 +47,7 @@ class QuotingMessageTopArea extends StatelessWidget { StreamMessageInputIconButton( iconSize: 24, color: _streamChatTheme.colorTheme.textLowEmphasis, - icon: const StreamSvgIcon(icon: StreamSvgIcons.closeSmall), + icon: Icon(context.streamIcons.xmark16), onPressed: onQuotedMessageCleared?.call, ), ], diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment.dart new file mode 100644 index 0000000000..ef3f6dcedd --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment.dart @@ -0,0 +1,286 @@ +part of 'stream_message_composer_attachment_list.dart'; + +/// A widget that renders a single attachment in the message composer. +/// +/// Delegates rendering to a custom builder registered via +/// [StreamChatComponentBuilder], or falls back to +/// [DefaultMessageComposerAttachment]. +class StreamMessageComposerAttachment extends StatelessWidget { + /// Creates a [StreamMessageComposerAttachment]. + StreamMessageComposerAttachment({ + super.key, + required Attachment attachment, + ValueSetter? onRemovePressed, + StreamAudioPlaylistController? audioPlaylistController, + }) : props = StreamMessageComposerAttachmentProps( + attachment: attachment, + onRemovePressed: onRemovePressed, + audioPlaylistController: audioPlaylistController, + ); + + /// The properties for the message composer attachment. + final StreamMessageComposerAttachmentProps props; + + @override + Widget build(BuildContext context) { + return context.chatComponentBuilder()?.call(context, props) ?? + DefaultMessageComposerAttachment(props: props); + } +} + +/// Properties passed to [StreamMessageComposerAttachment] and its default +/// implementation [DefaultMessageComposerAttachment]. +class StreamMessageComposerAttachmentProps { + /// Creates a [StreamMessageComposerAttachmentProps]. + const StreamMessageComposerAttachmentProps({ + required this.attachment, + required this.onRemovePressed, + required this.audioPlaylistController, + }); + + /// The attachment to display. + final Attachment attachment; + + /// Callback called when the remove button is pressed. + final ValueSetter? onRemovePressed; + + /// Controller used for audio/voice-recording attachment playback. + final StreamAudioPlaylistController? audioPlaylistController; +} + +/// Default implementation of a message composer attachment widget. +/// +/// Renders file, audio/voice-recording, or media attachments depending on the +/// attachment type. +class DefaultMessageComposerAttachment extends StatelessWidget { + /// Creates a [DefaultMessageComposerAttachment]. + const DefaultMessageComposerAttachment({super.key, required this.props}); + + /// The properties used to render this attachment. + final StreamMessageComposerAttachmentProps props; + + /// The attachment to display. + Attachment get attachment => props.attachment; + + /// Callback called when the remove button is pressed. + ValueSetter? get onRemovePressed => props.onRemovePressed; + + /// Controller used for audio/voice-recording attachment playback. + StreamAudioPlaylistController? get audioPlaylistController => props.audioPlaylistController; + + @override + Widget build(BuildContext context) { + if (attachment.type == AttachmentType.file) { + return SizedBox( + width: 268, + child: MessageComposerFileAttachment( + fileTypeIcon: StreamFileTypeIcon.fromMimeType(mimeType: attachment.file?.mediaType?.mimeType ?? ''), + title: attachment.title ?? context.translations.fileText, + subtitle: _FileAttachmentSubtitle(attachment: attachment), + onRemovePressed: onRemovePressed != null ? () => onRemovePressed!(attachment) : null, + ), + ); + } + + if (attachment.type == AttachmentType.audio || attachment.type == AttachmentType.voiceRecording) { + if (audioPlaylistController == null) { + return const SizedBox.shrink(); + } + + final hasTrack = audioPlaylistController!.value.tracks.any((it) => it.key == attachment); + + if (!hasTrack) { + return const SizedBox.shrink(); + } + + final trackIndex = audioPlaylistController!.value.tracks.indexWhere((it) => it.key == attachment); + + return SizedBox( + width: 268, + child: MessageInputVoiceRecordingAttachment( + attachment: attachment, + index: trackIndex, + controller: audioPlaylistController!, + onRemovePressed: onRemovePressed, + ), + ); + } + + return StreamMediaAttachmentBuilder( + attachment: attachment, + onRemovePressed: onRemovePressed, + ); + } +} + +class _FileAttachmentSubtitle extends StatelessWidget { + const _FileAttachmentSubtitle({ + required this.attachment, + }); + + final Attachment attachment; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final size = attachment.file?.size ?? attachment.extraData['file_size']; + final textStyle = theme.textTheme.footnote.copyWith( + color: theme.colorTheme.textLowEmphasis, + ); + return attachment.uploadState.when( + preparing: () => Text(fileSize(size), style: textStyle), + inProgress: (sent, total) => StreamUploadProgressIndicator( + uploaded: sent, + total: total, + showBackground: false, + textStyle: textStyle, + progressIndicatorColor: theme.colorTheme.accentPrimary, + ), + success: () => Text(fileSize(size), style: textStyle), + failed: (_) => Text( + context.translations.uploadErrorLabel, + style: textStyle, + ), + ); + } +} + +/// Widget used to display the list of voice recording type attachments added to +/// the message input. +class MessageInputVoiceRecordingAttachment extends StatelessWidget { + /// Creates a new MessageInputVoiceRecordingAttachments widget. + const MessageInputVoiceRecordingAttachment({ + super.key, + required this.attachment, + required this.index, + required this.controller, + this.onRemovePressed, + }); + + /// Attachment to display. + final Attachment attachment; + + /// Index of the track in the playlist. + final int index; + + /// Controller to use to control the audio playback. + final StreamAudioPlaylistController controller; + + /// Callback called when the remove button is pressed. + final ValueSetter? onRemovePressed; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller, + builder: (context, state, _) { + final track = state.tracks.firstWhereOrNull((it) => it.key == attachment); + if (track == null) return const SizedBox.shrink(); + + return StreamMessageComposerAttachmentContainer( + onRemovePressed: switch (onRemovePressed) { + final callback? => () => callback(attachment), + _ => null, + }, + child: StreamVoiceRecordingAttachment( + title: 'Voice Message', + showTitle: true, + track: track, + speed: state.speed, + onTrackPause: controller.pause, + onChangeSpeed: controller.setSpeed, + onTrackPlay: () async { + // Play the track directly if it is already loaded. + if (state.currentIndex == index) return controller.play(); + // Otherwise, load the track first and then play it. + return controller.skipToItem(index); + }, + // Only allow seeking if the current track is the one being + // interacted with. + onTrackSeekStart: (_) async { + if (state.currentIndex != index) return; + return controller.pause(); + }, + onTrackSeekEnd: (_) async { + if (state.currentIndex != index) return; + return controller.play(); + }, + onTrackSeekChanged: (progress) async { + if (state.currentIndex != index) return; + + final duration = track.duration.inMicroseconds; + final seekPosition = (duration * progress).toInt(); + final seekDuration = Duration(microseconds: seekPosition); + + return controller.seek(seekDuration); + }, + ), + ); + }, + ); + } +} + +/// Widget used to display a media type attachment item. +class StreamMediaAttachmentBuilder extends StatelessWidget { + /// Creates a new media attachment item. + const StreamMediaAttachmentBuilder({super.key, required this.attachment, this.onRemovePressed}); + + /// The media attachment to display. + final Attachment attachment; + + /// Callback called when the remove button is pressed. + final ValueSetter? onRemovePressed; + + @override + Widget build(BuildContext context) { + final durationSecs = attachment.extraData['duration'] as num?; + final videoDuration = durationSecs != null ? Duration(seconds: durationSecs.round()) : null; + + final mediaBadge = attachment.type == AttachmentType.video + ? StreamMediaBadge(type: MediaBadgeType.video, duration: videoDuration) + : null; + + return Container( + key: Key(attachment.id), + child: MessageComposerMediaFileAttachment( + mediaBadge: mediaBadge, + onRemovePressed: onRemovePressed != null ? () => onRemovePressed!(attachment) : null, + child: StreamMediaAttachmentThumbnail( + media: attachment, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + ), + ), + ); + } +} + +/// Material Button used for removing attachments. +class RemoveAttachmentButton extends StatelessWidget { + /// Creates a new remove attachment button. + const RemoveAttachmentButton({super.key, this.onPressed}); + + /// Callback when the remove attachment button is pressed. + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + final colorTheme = theme.colorTheme; + + return IconButton.filled( + onPressed: onPressed, + color: colorTheme.barsBg, + padding: EdgeInsets.zero, + icon: Icon(context.streamIcons.xmark20), + style: IconButton.styleFrom( + minimumSize: const Size(24, 24), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + // ignore: deprecated_member_use + backgroundColor: colorTheme.textHighEmphasis.withOpacity(0.6), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment_list.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment_list.dart new file mode 100644 index 0000000000..166ba13fd5 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_composer_attachment_list.dart @@ -0,0 +1,208 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/audio/audio_playlist_controller.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +part 'stream_message_composer_attachment.dart'; + +/// {@template stream_message_input_attachment_list} +/// Widget used to display the list of attachments added to the message input. +/// +/// By default, it displays the list of file attachments and media attachments +/// separately. +/// +/// You can customize the list of file attachments and media attachments using +/// [fileAttachmentListBuilder] and [attachmentListBuilder] respectively. +/// +/// You can also customize the attachment item using [fileAttachmentBuilder] and +/// [mediaAttachmentBuilder] respectively. +/// +/// You can override the default action of removing an attachment by providing +/// [onRemovePressed]. +/// {@endtemplate} +class StreamMessageComposerAttachmentList extends StatelessWidget { + /// {@macro stream_message_input_attachment_list} + StreamMessageComposerAttachmentList({ + super.key, + required Iterable attachments, + ValueSetter? onRemovePressed, + }) : props = StreamMessageComposerAttachmentListProps(attachments: attachments, onRemovePressed: onRemovePressed); + + /// List of attachments to display thumbnails for. + /// + /// Open graph should be filtered out. + final StreamMessageComposerAttachmentListProps props; + + @override + Widget build(BuildContext context) { + return context.chatComponentBuilder()?.call(context, props) ?? + DefaultMessageComposerAttachmentList(props: props); + } +} + +/// Properties for [StreamMessageComposerAttachmentList]. +class StreamMessageComposerAttachmentListProps { + /// Creates a new instance of [StreamMessageComposerAttachmentListProps]. + const StreamMessageComposerAttachmentListProps({ + required this.attachments, + this.onRemovePressed, + }); + + /// List of attachments to display thumbnails for. + /// + /// Open graph should be filtered out. + final Iterable attachments; + + /// Callback called when the remove button is pressed. + final ValueSetter? onRemovePressed; +} + +/// Default implementation of [StreamMessageComposerAttachmentList]. +class DefaultMessageComposerAttachmentList extends StatefulWidget { + /// {@macro stream_message_composer_attachment_list} + const DefaultMessageComposerAttachmentList({ + super.key, + required this.props, + }); + + /// Properties for the [DefaultMessageComposerAttachmentList]. + final StreamMessageComposerAttachmentListProps props; + + /// List of attachments to display thumbnails for. + /// + /// Open graph should be filtered out. + Iterable get attachments => props.attachments; + + /// Callback called when the remove button is pressed. + ValueSetter? get onRemovePressed => props.onRemovePressed; + + List get _audioAttachments => + attachments.where((it) => it.type == AttachmentType.audio || it.type == AttachmentType.voiceRecording).toList(); + + @override + State createState() => _DefaultMessageComposerAttachmentListState(); +} + +class _DefaultMessageComposerAttachmentListState extends State { + late List _audioAttachments = widget._audioAttachments; + + StreamAudioPlaylistController? _controller; + late final _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _updateController(); + } + + @override + void didUpdateWidget( + covariant DefaultMessageComposerAttachmentList oldWidget, + ) { + super.didUpdateWidget(oldWidget); + final equals = const ListEquality().equals; + final newAudioAttachments = widget._audioAttachments; + if (!equals(newAudioAttachments, _audioAttachments)) { + // If the attachments have changed, update the playlist. + _audioAttachments = newAudioAttachments; + _updateController(); + } + if (oldWidget.attachments.length < widget.attachments.length) { + // If an attachment has been added, scroll to the end. + WidgetsBinding.instance.addPostFrameCallback( + (_) { + if (!_scrollController.hasClients) return; + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + }, + ); + } + } + + void _updateController() { + if (_audioAttachments.isNotEmpty) { + if (_controller == null) { + _controller = StreamAudioPlaylistController(_audioAttachments.toPlaylist()); + _controller!.initialize(); + } else { + _controller!.updatePlaylist(_audioAttachments.toPlaylist()); + } + } else if (_controller != null) { + _controller!.dispose(); + _controller = null; + } + } + + @override + void dispose() { + _controller?.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final attachmentsList = widget.attachments.toList(); + + return MessageInputMediaAttachments( + scrollController: _scrollController, + attachments: attachmentsList, + audioPlaylistController: _controller, + onRemovePressed: widget.onRemovePressed, + ); + } +} + +/// Widget used to display the list of media type attachments added to the +/// message input. +class MessageInputMediaAttachments extends StatelessWidget { + /// Creates a new MediaAttachments widget. + const MessageInputMediaAttachments({ + super.key, + required this.attachments, + this.audioPlaylistController, + this.onRemovePressed, + this.scrollController, + }); + + /// List of media type attachments to display thumbnails for. + /// + /// Only attachments of type `image`, `video` and `giphy` are supported. Shows + /// a placeholder for other types of attachments. + final List attachments; + + /// Callback called when the remove button is pressed. + final ValueSetter? onRemovePressed; + + /// Controller to use to control the audio playback. + final StreamAudioPlaylistController? audioPlaylistController; + + /// Scroll controller to use to control the scroll position. + final ScrollController? scrollController; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 80, + child: ListView( + controller: scrollController, + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: context.streamSpacing.xs), + cacheExtent: 104 * 10, // Cache 10 items ahead. + children: attachments + .map( + (attachment) => StreamMessageComposerAttachment( + attachment: attachment, + onRemovePressed: onRemovePressed, + audioPlaylistController: audioPlaylistController, + ), + ) + .toList(), + ), + ); + } +} 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 2277f8a511..c25ead0730 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 @@ -1,19 +1,10 @@ import 'dart:async'; -import 'dart:math'; +import 'dart:math' as math; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget_builder.dart'; -import 'package:stream_chat_flutter/src/message_input/attachment_button.dart'; -import 'package:stream_chat_flutter/src/message_input/command_button.dart'; -import 'package:stream_chat_flutter/src/message_input/dm_checkbox_list_tile.dart'; -import 'package:stream_chat_flutter/src/message_input/quoting_message_top_area.dart'; -import 'package:stream_chat_flutter/src/message_input/stream_message_input_icon_button.dart'; import 'package:stream_chat_flutter/src/message_input/tld.dart'; -import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; -import 'package:stream_chat_flutter/src/misc/gradient_box_border.dart'; -import 'package:stream_chat_flutter/src/misc/simple_safe_area.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; const _kCommandTrigger = '/'; @@ -21,10 +12,11 @@ const _kMentionTrigger = '@'; /// Signature for the function that determines if a [matchedUri] should be /// previewed as an OG Attachment. -typedef OgPreviewFilter = bool Function( - Uri matchedUri, - String messageText, -); +typedef OgPreviewFilter = + bool Function( + Uri matchedUri, + String messageText, + ); /// Different types of hints that can be shown in [StreamMessageInput]. enum HintType { @@ -46,12 +38,6 @@ enum HintType { /// [type]. typedef HintGetter = String? Function(BuildContext context, HintType type); -/// The signature for the function that builds the list of actions. -typedef ActionsBuilder = List Function( - BuildContext context, - List defaultActions, -); - /// Inactive state: /// /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input.png) @@ -100,104 +86,45 @@ class StreamMessageInput extends StatefulWidget { super.key, this.onMessageSent, this.preMessageSending, - this.maxHeight = 150, - this.maxLines, - this.minLines, - this.textInputAction, - this.keyboardType, - this.textCapitalization = TextCapitalization.sentences, - this.disableAttachments = false, this.messageInputController, - this.actionsBuilder, - this.spaceBetweenActions = 0, - this.actionsLocation = ActionsLocation.left, - this.attachmentListBuilder, - this.fileAttachmentListBuilder, - this.mediaAttachmentListBuilder, - this.voiceRecordingAttachmentListBuilder, - this.fileAttachmentBuilder, - this.mediaAttachmentBuilder, - this.voiceRecordingAttachmentBuilder, this.focusNode, - this.sendButtonLocation = SendButtonLocation.outside, - this.autofocus = false, - this.hideSendAsDm = false, + this.disableAttachments = false, + this.maxAttachmentSize = kDefaultMaxAttachmentSize, + this.canAlsoSendToChannelFromThread = true, this.enableVoiceRecording = false, this.sendVoiceRecordingAutomatically = false, this.voiceRecordingFeedback = const AudioRecorderFeedback(), - Widget? idleSendIcon, - @Deprecated("Use 'idleSendIcon' instead") Widget? idleSendButton, - Widget? activeSendIcon, - @Deprecated("Use 'activeSendIcon' instead") Widget? activeSendButton, - this.showCommandsButton = true, this.userMentionsTileBuilder, - this.maxAttachmentSize = kDefaultMaxAttachmentSize, this.onError, - this.attachmentLimit = 10, + this.attachmentLimit, this.allowedAttachmentPickerTypes = AttachmentPickerType.values, this.onAttachmentLimitExceed, - this.attachmentButtonBuilder, - this.commandButtonBuilder, this.customAutocompleteTriggers = const [], this.mentionAllAppUsers = false, - this.sendButtonBuilder, - this.quotedMessageBuilder, - this.quotedMessageAttachmentThumbnailBuilders, this.shouldKeepFocusAfterMessage, this.validator = _defaultValidator, this.restorationId, this.enableSafeArea, - this.elevation, - this.shadow, - this.autoCorrect = true, this.enableMentionsOverlay = true, this.onQuotedMessageCleared, - this.enableActionAnimation = true, - this.sendMessageKeyPredicate = _defaultSendMessageKeyPredicate, - this.clearQuotedMessageKeyPredicate = - _defaultClearQuotedMessageKeyPredicate, 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.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; - - /// The predicate used to clear the quoted message on desktop/web - final KeyEventPredicate clearQuotedMessageKeyPredicate; - - /// If true the message input will animate the actions while you type - final bool enableActionAnimation; + this.attachmentPickerOptionsBuilder, + this.onAttachmentPickerResult, + this.sendMessageKeyPredicate = _defaultSendMessageKeyPredicate, + this.clearQuotedMessageKeyPredicate = _defaultClearQuotedMessageKeyPredicate, + this.textInputAction, + this.keyboardType, + this.textCapitalization = TextCapitalization.sentences, + this.autofocus = false, + this.autoCorrect = true, + }); /// List of triggers for showing autocomplete. final Iterable customAutocompleteTriggers; - /// Max attachment size in bytes: - /// - Defaults to 20 MB - /// - Do not set it if you're using our default CDN - final int maxAttachmentSize; - /// Function called after sending the message. final void Function(Message)? onMessageSent; @@ -206,36 +133,30 @@ class StreamMessageInput extends StatefulWidget { /// Use this to transform the message. final FutureOr Function(Message)? preMessageSending; - /// Maximum Height for the TextField to grow before it starts scrolling. - final double maxHeight; - - /// The maximum lines of text the input can span. - final int? maxLines; - - /// The minimum lines of text the input can span. - final int? minLines; - - /// The type of action button to use for the keyboard. - final TextInputAction? textInputAction; - - /// The keyboard type assigned to the TextField. - final TextInputType? keyboardType; + /// The text controller of the TextField. + final StreamMessageInputController? messageInputController; - /// {@macro flutter.widgets.editableText.textCapitalization} - final TextCapitalization textCapitalization; + /// The focus node associated to the TextField. + final FocusNode? focusNode; /// If true the attachments button will not be displayed. + /// + /// Defaults to false. final bool disableAttachments; - /// Use this property to hide/show the commands button. - final bool showCommandsButton; + /// Max attachment size in bytes. + /// + /// Defaults to 100 MB. + final int maxAttachmentSize; - /// Hide send as dm checkbox. - final bool hideSendAsDm; + /// Show the checkbox to send the message as a direct message to the channel. + /// + /// Defaults to true. + final bool canAlsoSendToChannelFromThread; /// If true the voice recording button will be displayed. /// - /// Defaults to true. + /// Defaults to false. final bool enableVoiceRecording; /// If True, the voice recording will be sent automatically after the user @@ -274,81 +195,6 @@ class StreamMessageInput extends StatefulWidget { /// ``` final AudioRecorderFeedback voiceRecordingFeedback; - /// The text controller of the TextField. - final StreamMessageInputController? messageInputController; - - /// List of action widgets. - final ActionsBuilder? actionsBuilder; - - /// Space between the actions. - final double spaceBetweenActions; - - /// The location of the custom actions. - final ActionsLocation actionsLocation; - - /// Builder used to build the attachment list present in the message input. - /// - /// In case you want to customize only sub-parts of the attachment list, - /// consider using [fileAttachmentListBuilder], [mediaAttachmentListBuilder]. - final AttachmentListBuilder? attachmentListBuilder; - - /// Builder used to build the file type attachment list. - /// - /// In case you want to customize the attachment item, consider using - /// [fileAttachmentBuilder]. - final AttachmentListBuilder? fileAttachmentListBuilder; - - /// Builder used to build the media type attachment list. - /// - /// In case you want to customize the attachment item, consider using - /// [mediaAttachmentBuilder]. - final AttachmentListBuilder? mediaAttachmentListBuilder; - - /// Builder used to build the voice recording attachment list. - /// - /// In case you want to customize the attachment item, consider using - /// [voiceRecordingAttachmentBuilder]. - final AttachmentListBuilder? voiceRecordingAttachmentListBuilder; - - /// Builder used to build the file attachment item. - final AttachmentItemBuilder? fileAttachmentBuilder; - - /// Builder used to build the media attachment item. - final AttachmentItemBuilder? mediaAttachmentBuilder; - - /// Builder used to build the voice recording attachment item. - final AttachmentItemBuilder? voiceRecordingAttachmentBuilder; - - /// Map that defines a thumbnail builder for an attachment type. - /// - /// This is used to build the thumbnail for the attachment in the quoted - /// message. - final Map? - quotedMessageAttachmentThumbnailBuilders; - - /// The focus node associated to the TextField. - final FocusNode? focusNode; - - /// The location of the send button - final SendButtonLocation sendButtonLocation; - - /// Autofocus property passed to the TextField - final bool autofocus; - - /// 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; @@ -356,7 +202,7 @@ class StreamMessageInput extends StatefulWidget { final ErrorListener? onError; /// A limit for the no. of attachments that can be sent with a single message. - final int attachmentLimit; + final int? attachmentLimit; /// The list of allowed attachment types which can be picked using the /// attachment button. @@ -369,29 +215,11 @@ class StreamMessageInput extends StatefulWidget { /// This will override the default error alert behaviour. final AttachmentLimitExceedListener? onAttachmentLimitExceed; - /// Builder for customizing the attachment button. - /// - /// The builder contains the default [AttachmentButton] that can be customized - /// by calling `.copyWith`. - final AttachmentButtonBuilder? attachmentButtonBuilder; - - /// Builder for customizing the command button. - /// - /// The builder contains the default [CommandButton] that can be customized by - /// calling `.copyWith`. - final CommandButtonBuilder? commandButtonBuilder; - /// When enabled mentions search users across the entire app. /// /// Defaults to false. final bool mentionAllAppUsers; - /// Builder for creating send button - final MessageRelatedBuilder? sendButtonBuilder; - - /// Builder for building quoted message - final Widget Function(BuildContext, Message)? quotedMessageBuilder; - /// Defines if the [StreamMessageInput] loses focuses after a message is sent. /// The default behaviour keeps focus until a command is enabled. final bool? shouldKeepFocusAfterMessage; @@ -405,16 +233,6 @@ class StreamMessageInput extends StatefulWidget { /// Wrap [StreamMessageInput] with a [SafeArea widget] final bool? enableSafeArea; - /// Elevation of the [StreamMessageInput] - final double? elevation; - - /// Shadow for the [StreamMessageInput] widget - final BoxShadow? shadow; - - /// Disable autoCorrect by passing false - /// autoCorrect is enabled by default - final bool autoCorrect; - /// Disable the mentions overlay by passing false /// Enabled by default final bool enableMentionsOverlay; @@ -429,9 +247,6 @@ class StreamMessageInput extends StatefulWidget { /// Returns the hint text for the message input. final HintGetter hintGetter; - /// {@macro flutter.widgets.editableText.contentInsertionConfiguration} - final ContentInsertionConfiguration? contentInsertionConfiguration; - /// If True, allows you to use the system’s default media picker instead of /// the custom media picker provided by the library. This can be beneficial /// for several reasons: @@ -444,30 +259,60 @@ 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; - /// Padding for the message input. + /// Builder for customizing the attachment picker options. + /// + /// The builder receives the [BuildContext] and a list of default options + /// that can be modified or extended. + /// + /// If not provided, the default options are presented. + final AttachmentPickerOptionsBuilder? attachmentPickerOptionsBuilder; + + /// Callback that is called when the attachment picker result is received. /// - /// Defaults to `EdgeInsets.all(8)`. - final EdgeInsets padding; + /// Return `true` if the result is handled. Otherwise, return `false` to + /// allow the result to be handled internally. + final OnAttachmentPickerResult? onAttachmentPickerResult; + + /// Predicate to determine if the current key event should trigger sending + /// the message. Defaults to Enter on non-mobile platforms (without Shift). + final KeyEventPredicate sendMessageKeyPredicate; + + /// Predicate to determine if the current key event should clear the quoted + /// message. Defaults to Escape on non-mobile platforms. + final KeyEventPredicate clearQuotedMessageKeyPredicate; - /// Margin for the message input. Allows overriding the default computed - /// margin. + /// The type of action button to use for the keyboard. + final TextInputAction? textInputAction; + + /// The keyboard type assigned to the TextField. + final TextInputType? keyboardType; + + /// {@macro flutter.widgets.editableText.textCapitalization} + final TextCapitalization textCapitalization; + + /// Autofocus property passed to the TextField. + final bool autofocus; + + /// Whether to enable autocorrect. /// - /// Defaults to null, and margin is applied based on action and send button - /// locations. - final EdgeInsets? textInputMargin; + /// Defaults to true. + final bool autoCorrect; + + static bool _defaultSendMessageKeyPredicate(FocusNode node, KeyEvent event) { + if (CurrentPlatform.isAndroid || CurrentPlatform.isIos) return false; + if (HardwareKeyboard.instance.isShiftPressed) return false; + return event.logicalKey == LogicalKeyboardKey.enter && event is KeyDownEvent; + } + + static bool _defaultClearQuotedMessageKeyPredicate(FocusNode node, KeyEvent event) { + if (CurrentPlatform.isAndroid || CurrentPlatform.isIos) return false; + return event.logicalKey == LogicalKeyboardKey.escape && event is KeyDownEvent; + } static String? _defaultHintGetter( BuildContext context, @@ -501,61 +346,34 @@ class StreamMessageInput extends StatefulWidget { return hasText || hasAttachments || hasPoll; } - static bool _defaultSendMessageKeyPredicate( - FocusNode node, - KeyEvent event, - ) { - // Do not handle the event if the user is using a mobile device. - if (CurrentPlatform.isAndroid || CurrentPlatform.isIos) return false; - - // Do not send the message if the shift key is pressed. Generally, this - // means the user is trying to add a new line. - if (HardwareKeyboard.instance.isShiftPressed) return false; - - // Otherwise, send the message when the user presses the enter key. - final isEnterKeyPressed = event.logicalKey == LogicalKeyboardKey.enter; - return isEnterKeyPressed && event is KeyDownEvent; - } - - static bool _defaultClearQuotedMessageKeyPredicate( - FocusNode node, - KeyEvent event, - ) { - // Do not handle the event if the user is using a mobile device. - if (CurrentPlatform.isAndroid || CurrentPlatform.isIos) return false; - - // Otherwise, Clear the quoted message when the user presses the escape key. - final isEscapeKeyPressed = event.logicalKey == LogicalKeyboardKey.escape; - return isEscapeKeyPressed && event is KeyDownEvent; - } - @override StreamMessageInputState createState() => StreamMessageInputState(); } /// State of [StreamMessageInput] class StreamMessageInputState extends State - with RestorationMixin { + with RestorationMixin, SingleTickerProviderStateMixin { bool get _commandEnabled => _effectiveController.message.command != null; - bool _actionsShrunk = false; + bool get _isPickerVisible => _pickerController != null; + StreamAttachmentPickerController? _pickerController; + StreamSubscription? _customResultSubscription; + bool _isSyncingControllers = false; + + late final AnimationController _pickerAnimationController; + late final CurvedAnimation _pickerAnimation; late StreamChatThemeData _streamChatTheme; late StreamMessageInputThemeData _messageInputTheme; - bool get _hasQuotedMessage => - _effectiveController.message.quotedMessage != null; - bool get _isEditing => !_effectiveController.message.state.isInitial; late final _audioRecorderController = StreamAudioRecorderController(); - FocusNode get _effectiveFocusNode => - widget.focusNode ?? (_focusNode ??= FocusNode()); + FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); FocusNode? _focusNode; - StreamMessageInputController get _effectiveController => - widget.messageInputController ?? _controller!.value; + StreamMessageInputController get _effectiveController => widget.messageInputController ?? _controller!.value; StreamRestorableMessageInputController? _controller; void _createLocalController([Message? message]) { @@ -572,15 +390,27 @@ class StreamMessageInputState extends State void _initialiseEffectiveController() { _effectiveController + ..removeListener(_onChangedThrottled) ..removeListener(_onChangedDebounced) + ..addListener(_onChangedThrottled) ..addListener(_onChangedDebounced); } StreamSubscription? _draftStreamSubscription; + StreamSubscription? _messageUpdatedSubscription; + StreamSubscription? _messageDeletedSubscription; @override void initState() { super.initState(); + _pickerAnimationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + _pickerAnimation = CurvedAnimation( + parent: _pickerAnimationController, + curve: Curves.easeInOut, + ); if (widget.messageInputController == null) { _createLocalController(); } else { @@ -615,6 +445,36 @@ class StreamMessageInputState extends State _draftStreamSubscription = draftStream?.distinct().listen(_onDraftUpdate); } + + // Keeps the composer in sync with remote message changes. + _messageUpdatedSubscription = channel.on(EventType.messageUpdated).listen(_onMessageUpdated); + _messageDeletedSubscription = channel.on(EventType.messageDeleted).listen(_onMessageDeleted); + } + + void _onMessageUpdated(Event event) { + final updatedMessage = event.message; + if (updatedMessage == null) return; + + if (_effectiveController.message.quotedMessageId == updatedMessage.id) { + _effectiveController.quotedMessage = updatedMessage; + } + + if (_isEditing && _effectiveController.message.id == updatedMessage.id) { + _effectiveController.editMessage(updatedMessage); + } + } + + void _onMessageDeleted(Event event) { + final deletedMessageId = event.message?.id; + if (deletedMessageId == null) return; + + if (_effectiveController.message.quotedMessageId == deletedMessageId) { + widget.onQuotedMessageCleared?.call(); + } + + if (_isEditing && _effectiveController.message.id == deletedMessageId) { + _effectiveController.cancelEditMessage(); + } } void _onDraftUpdate(Draft? draft) { @@ -623,7 +483,12 @@ class StreamMessageInputState extends State // Otherwise, update the controller with the draft message. if (draft.message case final draftMessage) { - _effectiveController.message = draftMessage.toMessage(); + _effectiveController.message = draftMessage + .copyWith( + quotedMessage: draftMessage.quotedMessage ?? draft.quotedMessage, + parentId: draftMessage.parentId ?? draft.parentId, + ) + .toMessage(); } } @@ -637,11 +502,9 @@ class StreamMessageInputState extends State @override void didUpdateWidget(covariant StreamMessageInput oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.messageInputController == null && - oldWidget.messageInputController != null) { + if (widget.messageInputController == null && oldWidget.messageInputController != null) { _createLocalController(oldWidget.messageInputController!.message); - } else if (widget.messageInputController != null && - oldWidget.messageInputController == null) { + } else if (widget.messageInputController != null && oldWidget.messageInputController == null) { unregisterFromRestoration(_controller!); _controller!.dispose(); _controller = null; @@ -665,13 +528,39 @@ class StreamMessageInputState extends State @override String? get restorationId => widget.restorationId; - // ignore: no-empty-block - void _focusNodeListener() {} + void _focusNodeListener() { + if (_effectiveFocusNode.hasFocus && _isPickerVisible) { + _hidePicker(); + } + } + + KeyEventResult _handleKeyPressed(FocusNode node, KeyEvent event) { + if (widget.sendMessageKeyPredicate(node, event)) { + sendMessage(); + return KeyEventResult.handled; + } + if (widget.clearQuotedMessageKeyPredicate(node, event)) { + final hasQuote = _effectiveController.message.quotedMessage != null; + if (hasQuote && _effectiveController.text.isEmpty) { + _effectiveController.clearQuotedMessage(); + widget.onQuotedMessageCleared?.call(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } + return KeyEventResult.ignored; + } @override Widget build(BuildContext context) { bool canSendOrUpdateMessage(List capabilities) { var result = capabilities.contains(ChannelCapability.sendMessage); + + final insideThread = _effectiveController.message.parentId != null; + if (insideThread) { + result |= capabilities.contains(ChannelCapability.sendReply); + } + if (_isEditing) { result |= capabilities.contains(ChannelCapability.updateOwnMessage); result |= capabilities.contains(ChannelCapability.updateAnyMessage); @@ -683,31 +572,46 @@ class StreamMessageInputState extends State final channel = StreamChannel.of(context).channel; final messageInput = switch (_buildAutocompleteMessageInput(context)) { final messageInput when channel.state != null => BetterStreamBuilder( - stream: channel.ownCapabilitiesStream.map(canSendOrUpdateMessage), - initialData: canSendOrUpdateMessage(channel.ownCapabilities), - builder: (context, enabled) { - // Allow the user to send messages if the user has the permission to - // send messages or if the user is editing a message. - if (enabled) return messageInput; - - // Otherwise, show the no permission message. - return _buildNoPermissionMessage(context); - }, - ), + stream: channel.ownCapabilitiesStream.map(canSendOrUpdateMessage), + initialData: canSendOrUpdateMessage(channel.ownCapabilities), + builder: (context, enabled) { + // Allow the user to send messages if the user has the permission to + // send messages or if the user is editing a message. + if (enabled) return messageInput; + + // Otherwise, show the no permission message. + return _buildNoPermissionMessage(context); + }, + ), final messageInput => messageInput, }; - final shadow = widget.shadow ?? _messageInputTheme.shadow; - final elevation = widget.elevation ?? _messageInputTheme.elevation; + final spacing = context.streamSpacing; + final safeAreaEnabled = widget.enableSafeArea ?? _messageInputTheme.enableSafeArea ?? true; + final viewPadding = MediaQuery.paddingOf(context); + return Material( - elevation: elevation ?? 8, child: DecoratedBox( decoration: BoxDecoration( - color: _messageInputTheme.inputBackgroundColor, - boxShadow: [if (shadow != null) shadow], + color: context.streamColorScheme.backgroundElevation1, ), - child: SimpleSafeArea( - enabled: widget.enableSafeArea ?? _messageInputTheme.enableSafeArea, + child: AnimatedBuilder( + animation: _pickerAnimation, + builder: (context, child) { + final safeAreaPadding = safeAreaEnabled + ? EdgeInsets.lerp( + EdgeInsets.only( + left: viewPadding.left, + top: viewPadding.top, + right: viewPadding.right, + bottom: math.max(viewPadding.bottom, spacing.md), + ), + EdgeInsets.zero, + _pickerAnimation.value, + )! + : EdgeInsets.zero; + return Padding(padding: safeAreaPadding, child: child); + }, child: Center(heightFactor: 1, child: messageInput), ), ), @@ -724,47 +628,48 @@ class StreamMessageInputState extends State StreamAutocompleteTrigger( trigger: _kCommandTrigger, triggerOnlyAtStart: true, - optionsViewBuilder: ( - context, - autocompleteQuery, - messageEditingController, - ) { - final query = autocompleteQuery.query; - return StreamCommandAutocompleteOptions( - query: query, - channel: StreamChannel.of(context).channel, - onCommandSelected: (command) { - _effectiveController.command = command.name; - // removing the overlay after the command is selected - StreamAutocomplete.of(context).closeSuggestions(); + optionsViewBuilder: + ( + context, + autocompleteQuery, + messageEditingController, + ) { + final query = autocompleteQuery.query; + return StreamCommandAutocompleteOptions( + query: query, + channel: StreamChannel.of(context).channel, + onCommandSelected: (command) { + _effectiveController.command = command.name; + // removing the overlay after the command is selected + StreamAutocomplete.of(context).closeSuggestions(); + }, + ); }, - ); - }, ), if (widget.enableMentionsOverlay) StreamAutocompleteTrigger( trigger: _kMentionTrigger, - optionsViewBuilder: ( - context, - autocompleteQuery, - messageEditingController, - ) { - final query = autocompleteQuery.query; - return StreamMentionAutocompleteOptions( - query: query, - channel: StreamChannel.of(context).channel, - mentionAllAppUsers: widget.mentionAllAppUsers, - mentionsTileBuilder: widget.userMentionsTileBuilder, - onMentionUserTap: (user) { - // adding the mentioned user to the controller. - _effectiveController.addMentionedUser(user); - - // accepting the autocomplete option. - StreamAutocomplete.of(context) - .acceptAutocompleteOption(user.name); + optionsViewBuilder: + ( + context, + autocompleteQuery, + messageEditingController, + ) { + final query = autocompleteQuery.query; + return StreamMentionAutocompleteOptions( + query: query, + channel: StreamChannel.of(context).channel, + mentionAllAppUsers: widget.mentionAllAppUsers, + mentionsTileBuilder: widget.userMentionsTileBuilder, + onMentionUserTap: (user) { + // adding the mentioned user to the controller. + _effectiveController.addMentionedUser(user); + + // accepting the autocomplete option. + StreamAutocomplete.of(context).acceptAutocompleteOption(user.name); + }, + ); }, - ); - }, ), ], ); @@ -775,60 +680,113 @@ class StreamMessageInputState extends State StreamMessageEditingController controller, FocusNode focusNode, ) { + final currentUserId = StreamChat.of(context).currentUser?.id; + return StreamMessageValueListenableBuilder( valueListenable: controller, - builder: (context, value, _) => Padding( - padding: widget.padding, + builder: (context, value, _) => PopScope( + canPop: !_isPickerVisible, + onPopInvokedWithResult: (didPop, _) { + if (!didPop) _hidePicker(); + }, child: Column( - spacing: 8, mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildTopMessageArea(context), - _buildTextField(context), - _buildDmCheckbox(context), - ].nonNulls.toList(), + children: [ + DropTarget( + onDragDone: (details) async { + final attachments = []; + for (final file in details.files) { + attachments.add(await file.toAttachment(type: AttachmentType.file)); + } + if (attachments.isNotEmpty) _addAttachments(attachments); + }, + onDragEntered: (_) {}, + onDragExited: (_) {}, + child: Focus( + skipTraversal: true, + onKeyEvent: _handleKeyPressed, + child: StreamChatMessageComposer( + controller: controller, + currentUserId: currentUserId, + onAttachmentButtonPressed: widget.disableAttachments ? null : _onAttachmentButtonPressed, + isPickerOpen: _isPickerVisible, + placeholder: _getHint(context) ?? '', + focusNode: focusNode, + onSendPressed: sendMessage, + canAlsoSendToChannel: _shouldShowSendToChannelCheckbox(), + audioRecorderController: widget.enableVoiceRecording ? _audioRecorderController : null, + sendVoiceRecordingAutomatically: widget.sendVoiceRecordingAutomatically, + feedback: widget.voiceRecordingFeedback, + onQuotedMessageCleared: () { + _effectiveController.clearQuotedMessage(); + widget.onQuotedMessageCleared?.call(); + }, + textInputAction: widget.textInputAction, + keyboardType: widget.keyboardType, + textCapitalization: widget.textCapitalization, + autofocus: widget.autofocus, + autocorrect: widget.autoCorrect, + ), + ), + ), + SizeTransition( + sizeFactor: _pickerAnimation, + axisAlignment: -1, + child: _buildInlineAttachmentPicker(context), + ), + ], ), ), ); } - Widget? _buildTopMessageArea(BuildContext context) { - if (_hasQuotedMessage && !_isEditing) { - // Ensure this doesn't show on web & desktop - return PlatformWidgetBuilder( - mobile: (context, child) => child, - child: QuotingMessageTopArea( - hasQuotedMessage: _hasQuotedMessage, - onQuotedMessageCleared: widget.onQuotedMessageCleared, - ), - ); - } + Widget _buildInlineAttachmentPicker(BuildContext context) { + if (!_isPickerVisible) return const SizedBox.shrink(); - if (_effectiveController.ogAttachment != null) { - return OGAttachmentPreview( - attachment: _effectiveController.ogAttachment!, - onDismissPreviewPressed: () { - _effectiveController.clearOGAttachment(); - _effectiveFocusNode.unfocus(); - }, - ); - } + final allowedTypes = _getAllowedAttachmentPickerTypes(); - return null; + final messageInputTheme = StreamMessageInputTheme.of(context); + final isWebOrDesktop = switch (CurrentPlatform.type) { + PlatformType.android || PlatformType.ios => false, + _ => true, + }; + final useSystemPicker = + widget.useSystemAttachmentPicker || (messageInputTheme.useSystemAttachmentPicker ?? false) || isWebOrDesktop; + + final child = useSystemPicker + ? systemAttachmentPickerBuilder( + context: context, + controller: _pickerController!, + allowedTypes: allowedTypes, + pollConfig: widget.pollConfig, + optionsBuilder: widget.attachmentPickerOptionsBuilder, + onError: _onPickerError, + onPollCreated: _onPollCreated, + ) + : tabbedAttachmentPickerBuilder( + context: context, + controller: _pickerController!, + allowedTypes: allowedTypes, + pollConfig: widget.pollConfig, + optionsBuilder: widget.attachmentPickerOptionsBuilder, + onError: _onPickerError, + onPollCreated: _onPollCreated, + onCommandSelected: _onCommandSelectedFromPicker, + ); + + return SizedBox(height: 333, child: child); } - Widget? _buildDmCheckbox(BuildContext context) { - if (widget.hideSendAsDm) return null; + void _onCommandSelectedFromPicker(Command command) { + _hidePicker(); + _effectiveController.command = command.name; + } - final insideThread = _effectiveController.message.parentId != null; - if (!insideThread) return null; + bool _shouldShowSendToChannelCheckbox() { + if (!widget.canAlsoSendToChannelFromThread) return false; - return DmCheckboxListTile( - value: _effectiveController.showInChannel, - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - onChanged: (value) => _effectiveController.showInChannel = value, - ); + final insideThread = _effectiveController.message.parentId != null; + return insideThread; } Widget _buildNoPermissionMessage(BuildContext context) { @@ -841,419 +799,130 @@ class StreamMessageInputState extends State ); } - Widget _buildTextField(BuildContext context) { - return ValueListenableBuilder( - valueListenable: _audioRecorderController, - builder: (context, state, _) { - final isAudioRecordingFlowActive = state is! RecordStateIdle; + Future _onPollCreated(Poll poll) async { + _hidePicker(); - return Row( - children: [ - if (!isAudioRecordingFlowActive) ...[ - if (!_commandEnabled && - widget.actionsLocation == ActionsLocation.left) - _buildExpandActionsButton(context), - const SizedBox(width: 4), - Expanded(child: _buildTextInput(context)), - const SizedBox(width: 4), - if (!_commandEnabled && - widget.actionsLocation == ActionsLocation.right) - _buildExpandActionsButton(context), - if (widget.sendButtonLocation == SendButtonLocation.outside) - _buildSendButton(context), - ], - if (widget.enableVoiceRecording) - Expanded( - // This is to make sure the audio recorder button will be given - // the full width when it's visible. - flex: isAudioRecordingFlowActive ? 1 : 0, - child: StreamAudioRecorderButton( - recordState: state, - feedback: widget.voiceRecordingFeedback, - onRecordStart: _audioRecorderController.startRecord, - onRecordCancel: _audioRecorderController.cancelRecord, - onRecordStop: _audioRecorderController.stopRecord, - onRecordLock: _audioRecorderController.lockRecord, - onRecordDragUpdate: _audioRecorderController.dragRecord, - onRecordStartCancel: () { - // Show a message to the user to hold to record. - _audioRecorderController.showInfo( - context.translations.holdToRecordLabel, - ); - }, - onRecordFinish: () async { - // Finish the recording session and add the audio to the - // message input controller. - final audio = await _audioRecorderController.finishRecord(); - if (audio != null) { - _effectiveController.addAttachment(audio); - } - - // Once the recording is finished, cancel the recorder. - _audioRecorderController.cancelRecord(discardTrack: false); - - // Send the message if the user has enabled the option to - // send the voice recording automatically. - if (widget.sendVoiceRecordingAutomatically) { - return sendMessage(); - } - }, - ), - ), - ], - ); - }, - ); + final channel = StreamChannel.maybeOf(context)?.channel; + if (channel == null) return; + + return channel.sendPoll(poll).ignore(); } - Widget _buildSendButton(BuildContext context) { - if (widget.sendButtonBuilder case final builder?) { - return builder(context, _effectiveController); - } + // 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; - return StreamMessageSendButton( - onSendMessage: sendMessage, - timeOut: _effectiveController.cooldownTimeOut, - isIdle: !widget.validator(_effectiveController.message), - idleSendIcon: widget.idleSendIcon, - activeSendIcon: widget.activeSendIcon, - ); - } + // 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; - Widget _buildExpandActionsButton(BuildContext context) { - return AnimatedCrossFade( - duration: const Duration(milliseconds: 200), - crossFadeState: switch (widget.enableActionAnimation && _actionsShrunk) { - true => CrossFadeState.showFirst, - false => CrossFadeState.showSecond, - }, - layoutBuilder: (top, topKey, bottom, bottomKey) => Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Positioned(key: bottomKey, top: 0, child: bottom), - Positioned(key: topKey, child: top), - ], - ), - firstChild: StreamMessageInputIconButton( - color: _messageInputTheme.expandButtonColor, - icon: Transform.rotate( - angle: (widget.actionsLocation == ActionsLocation.right || - widget.actionsLocation == ActionsLocation.rightInside) - ? pi - : 0, - child: const StreamSvgIcon(icon: StreamSvgIcons.emptyCircleRight), - ), - onPressed: () { - if (_actionsShrunk) { - setState(() => _actionsShrunk = false); - } - }, - ), - secondChild: widget.disableAttachments && - !widget.showCommandsButton && - !(widget.actionsBuilder != null) - ? const Empty() - : Row( - spacing: widget.spaceBetweenActions, - mainAxisSize: MainAxisSize.min, - children: _actionsList(), - ), - ); + // Otherwise, check if the user has the permission to send polls. + final channel = StreamChannel.of(context).channel; + return channel.config?.polls == true && channel.canSendPoll; + }); + + return allowedTypes.toList(growable: false); } - List _actionsList() { - final channel = StreamChannel.of(context).channel; - final defaultActions = [ - if (!widget.disableAttachments && channel.canUploadFile) - _buildAttachmentButton(context), - if (widget.showCommandsButton && - !_isEditing && - channel.state != null && - channel.config?.commands.isNotEmpty == true) - _buildCommandButton(context), - ]; - - if (widget.actionsBuilder case final builder?) { - return builder(context, defaultActions); - } + /// Toggles the inline attachment picker visibility. + void _onAttachmentButtonPressed() => _isPickerVisible ? _hidePicker() : _showPicker(); - return defaultActions; - } + void _showPicker() { + if (_isPickerVisible) { + _pickerAnimationController.forward(); + return; + } - Widget _buildAttachmentButton(BuildContext context) { - final defaultButton = AttachmentButton( - color: _messageInputTheme.actionButtonIdleColor, - onPressed: _onAttachmentButtonPressed, - ); + setState(() { + final attachmentLimit = widget.attachmentLimit; + _pickerController = attachmentLimit != null + ? StreamAttachmentPickerController( + initialAttachments: _effectiveController.attachments, + initialPoll: _effectiveController.poll, + maxAttachmentCount: attachmentLimit, + maxAttachmentSize: widget.maxAttachmentSize, + ) + : StreamAttachmentPickerController( + initialAttachments: _effectiveController.attachments, + initialPoll: _effectiveController.poll, + maxAttachmentSize: widget.maxAttachmentSize, + ); + _pickerController!.addListener(_syncPickerToMessage); + _effectiveController.addListener(_syncMessageToPicker); + _customResultSubscription = _pickerController!.customResults.listen(_onCustomResult); - return widget.attachmentButtonBuilder?.call(context, defaultButton) ?? - defaultButton; + if (_effectiveFocusNode.hasFocus) { + _effectiveFocusNode.unfocus(); + } + }); + _pickerAnimationController.forward(); } - Future _sendPoll(Poll poll, Channel channel) { - return channel.sendPoll(poll); + void _hidePicker() { + if (!_isPickerVisible) return; + _pickerAnimationController.reverse().then((_) { + if (mounted) setState(_disposePickerResources); + }); } - Future _updatePoll(Poll poll, Channel channel) { - return channel.updatePoll(poll); + void _disposePickerResources() { + _customResultSubscription?.cancel(); + _customResultSubscription = null; + _pickerController?.removeListener(_syncPickerToMessage); + _effectiveController.removeListener(_syncMessageToPicker); + _pickerController?.dispose(); + _pickerController = null; } - Future _deletePoll(Poll poll, Channel channel) { - return channel.deletePoll(poll); + Future _onCustomResult(CustomAttachmentPickerResult result) async { + final handled = await widget.onAttachmentPickerResult?.call(result) ?? false; + if (handled && mounted) _hidePicker(); } - Future _createOrUpdatePoll( - Poll? old, - Poll? current, - ) 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; - - // If old is null, i.e., there was no poll before, create the poll. - if (old == null) return _sendPoll(current!, channel); - - // If current is null, i.e., the poll is removed, delete the poll. - if (current == null) return _deletePoll(old, channel); + /// Copies picker attachments into the message controller when the user + /// selects or removes items in the picker. + void _syncPickerToMessage() { + if (_isSyncingControllers) return; + _isSyncingControllers = true; - // Otherwise, update the poll. - return _updatePoll(current, channel); + try { + _effectiveController.attachments = _pickerController?.value.attachments ?? []; + } finally { + _isSyncingControllers = false; + } } - /// Handle the platform-specific logic for selecting files. - /// - /// On mobile, this will open the file selection bottom sheet. On desktop, - /// this will open the native file system and allow the user to select one - /// or more files. - Future _onAttachmentButtonPressed() async { - final initialPoll = _effectiveController.poll; - final initialAttachments = _effectiveController.attachments; + /// Removes picker selections that the user deleted from the composer preview. + void _syncMessageToPicker() { + if (_isSyncingControllers) return; - // 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 pickerController = _pickerController; + if (pickerController == null) return; - final channel = StreamChannel.maybeOf(context)?.channel; - if (channel == null) return true; + final messageIds = _effectiveController.attachments.map((a) => a.id).toSet(); + final pickerIds = pickerController.value.attachments.map((a) => a.id).toSet(); - if (channel.config?.polls == true && channel.canSendPoll) return false; - - return true; - }); - - final messageInputTheme = StreamMessageInputTheme.of(context); - final useSystemPicker = widget.useSystemAttachmentPicker || - (messageInputTheme.useSystemAttachmentPicker ?? false); - - final value = await showStreamAttachmentPickerModalBottomSheet( - context: context, - onError: widget.onError, - allowedTypes: allowedTypes, - pollConfig: widget.pollConfig, - initialPoll: initialPoll, - initialAttachments: initialAttachments, - useSystemAttachmentPicker: useSystemPicker, - ); + final removedIds = pickerIds.difference(messageIds); + if (removedIds.isEmpty) return; - if (value == null || value is! AttachmentPickerValue) return; - - // Add the attachments to the controller. - _effectiveController.attachments = value.attachments; - - // Create or update the poll. - await _createOrUpdatePoll(initialPoll, value.poll); - } - - Widget _buildTextInput(BuildContext context) { - final margin = (widget.sendButtonLocation == SendButtonLocation.inside - ? const EdgeInsets.only(right: 8) - : EdgeInsets.zero) + - (widget.actionsLocation != ActionsLocation.left || _commandEnabled - ? const EdgeInsets.only(left: 8) - : EdgeInsets.zero); - - return DropTarget( - onDragDone: (details) async { - final files = details.files; - final attachments = []; - for (final file in files) { - final attachment = await file.toAttachment(type: AttachmentType.file); - attachments.add(attachment); - } - - if (attachments.isNotEmpty) _addAttachments(attachments); - }, - onDragEntered: (details) { - setState(() {}); - }, - onDragExited: (details) {}, - child: Container( - margin: widget.textInputMargin ?? margin, - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: _messageInputTheme.borderRadius, - color: _messageInputTheme.inputBackgroundColor, - border: GradientBoxBorder( - gradient: _effectiveFocusNode.hasFocus - ? _messageInputTheme.activeBorderGradient! - : _messageInputTheme.idleBorderGradient!, - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildReplyToMessage(), - _buildAttachments(), - LimitedBox( - maxHeight: widget.maxHeight, - child: Focus( - skipTraversal: true, - onKeyEvent: _handleKeyPressed, - child: StreamMessageTextField( - key: const Key('messageInputText'), - maxLines: widget.maxLines, - minLines: widget.minLines, - textInputAction: widget.textInputAction, - onSubmitted: (_) => sendMessage(), - keyboardType: widget.keyboardType, - controller: _effectiveController, - focusNode: _effectiveFocusNode, - style: _messageInputTheme.inputTextStyle, - autofocus: widget.autofocus, - textAlignVertical: TextAlignVertical.center, - decoration: _getInputDecoration(context), - textCapitalization: widget.textCapitalization, - autocorrect: widget.autoCorrect, - contentInsertionConfiguration: - widget.contentInsertionConfiguration, - ), - ), - ), - ], - ), - ), - ); - } - - KeyEventResult _handleKeyPressed(FocusNode node, KeyEvent event) { - // Check for send message key. - if (widget.sendMessageKeyPredicate(node, event)) { - sendMessage(); - return KeyEventResult.handled; - } - - // Check for clear quoted message key. - if (widget.clearQuotedMessageKeyPredicate(node, event)) { - if (_hasQuotedMessage && _effectiveController.text.isEmpty) { - widget.onQuotedMessageCleared?.call(); + _isSyncingControllers = true; + try { + for (final id in removedIds) { + pickerController.removeAttachmentById(id); } - return KeyEventResult.handled; + } finally { + _isSyncingControllers = false; } - - // Return ignored to allow other key events to be handled. - return KeyEventResult.ignored; } - InputDecoration _getInputDecoration(BuildContext context) { - final passedDecoration = _messageInputTheme.inputDecoration; - return InputDecoration( - isDense: true, - hintText: _getHint(context), - hintStyle: _messageInputTheme.inputTextStyle!.copyWith( - color: _streamChatTheme.colorTheme.textLowEmphasis, - ), - border: const OutlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - errorBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - disabledBorder: const OutlineInputBorder( - borderSide: BorderSide( - color: Colors.transparent, - ), - ), - contentPadding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), - prefixIcon: _commandEnabled - ? Container( - margin: const EdgeInsets.all(6), - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.accentPrimary, - borderRadius: _messageInputTheme.borderRadius?.add( - BorderRadius.circular(6), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const StreamSvgIcon( - size: 16, - color: Colors.white, - icon: StreamSvgIcons.lightning, - ), - Text( - _effectiveController.message.command!.toUpperCase(), - style: _streamChatTheme.textTheme.footnoteBold.copyWith( - color: Colors.white, - ), - ), - ], - ), - ) - : (widget.actionsLocation == ActionsLocation.leftInside - ? Row( - mainAxisSize: MainAxisSize.min, - children: [_buildExpandActionsButton(context)], - ) - : null), - suffixIconConstraints: const BoxConstraints.tightFor(height: 40), - prefixIconConstraints: const BoxConstraints.tightFor(height: 40), - suffixIcon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (_commandEnabled) - Padding( - padding: const EdgeInsets.only(right: 8), - child: StreamMessageInputIconButton( - iconSize: 24, - color: _messageInputTheme.actionButtonIdleColor, - icon: const StreamSvgIcon(icon: StreamSvgIcons.closeSmall), - onPressed: _effectiveController.clear, - ), - ), - if (!_commandEnabled && - widget.actionsLocation == ActionsLocation.rightInside) - _buildExpandActionsButton(context), - if (widget.sendButtonLocation == SendButtonLocation.inside) - _buildSendButton(context), - ].nonNulls.toList(), - ), - ).merge(passedDecoration); + void _onPickerError(AttachmentPickerError error) { + widget.onError?.call(error.error, error.stackTrace); } - late final _onChangedDebounced = debounce( + late final _onChangedThrottled = throttle( () { if (!mounted) return; @@ -1262,25 +931,24 @@ class StreamMessageInputState extends State final value = _effectiveController.text.trim(); if (value.isNotEmpty && channel.canUseTypingEvents) { - // Notify the server that the user started typing. channel.keyStroke(_effectiveController.message.parentId).onError( (error, stackTrace) { widget.onError?.call(error!, stackTrace); }, ); } + }, + const Duration(milliseconds: 350), + ); - int actionsLength; - if (widget.actionsBuilder != null) { - actionsLength = widget.actionsBuilder!(context, []).length; - } else { - actionsLength = 0; - } - if (widget.showCommandsButton) actionsLength += 1; - if (!widget.disableAttachments) actionsLength += 1; + late final _onChangedDebounced = debounce( + () { + if (!mounted) return; - setState(() => _actionsShrunk = value.isNotEmpty && actionsLength > 1); + final channel = StreamChannel.maybeOf(context)?.channel; + if (channel == null) return; + final value = _effectiveController.text.trim(); _checkContainsUrl(value, channel); }, const Duration(milliseconds: 350), @@ -1322,8 +990,7 @@ class StreamMessageInputState extends State final _parsedMatch = Uri.tryParse(it.group(0) ?? '')?.withScheme; if (_parsedMatch == null) return false; - return _parsedMatch.host.split('.').last.isValidTLD() && - widget.ogPreviewFilter.call(_parsedMatch, value); + return _parsedMatch.host.split('.').last.isValidTLD() && widget.ogPreviewFilter.call(_parsedMatch, value); }).toList(); // Reset the og attachment if the text doesn't contain any url @@ -1341,19 +1008,20 @@ class StreamMessageInputState extends State final client = StreamChat.maybeOf(context)?.client; if (client == null) return; - _enrichUrlOperation = CancelableOperation.fromFuture( - _enrichUrl(firstMatchedUrl, client), - ).then( - (ogAttachment) { - final attachment = Attachment.fromOGAttachment(ogAttachment); - _effectiveController.setOGAttachment(attachment); - }, - onError: (error, stackTrace) { - // Reset the ogAttachment if there was an error - _effectiveController.clearOGAttachment(); - widget.onError?.call(error, stackTrace); - }, - ); + _enrichUrlOperation = + CancelableOperation.fromFuture( + _enrichUrl(firstMatchedUrl, client), + ).then( + (ogAttachment) { + final attachment = Attachment.fromOGAttachment(ogAttachment); + _effectiveController.setOGAttachment(attachment); + }, + onError: (error, stackTrace) { + // Reset the ogAttachment if there was an error + _effectiveController.clearOGAttachment(); + widget.onError?.call(error, stackTrace); + }, + ); } final _ogAttachmentCache = {}; @@ -1374,127 +1042,22 @@ class StreamMessageInputState extends State return response; } - Widget _buildReplyToMessage() { - if (!_hasQuotedMessage) return const Empty(); - final quotedMessage = _effectiveController.message.quotedMessage!; - - final quotedMessageBuilder = widget.quotedMessageBuilder; - if (quotedMessageBuilder != null) { - return quotedMessageBuilder( - context, - _effectiveController.message.quotedMessage!, - ); - } - - final containsUrl = quotedMessage.attachments.any((it) { - return it.type == AttachmentType.urlPreview; - }); - - return StreamQuotedMessageWidget( - reverse: true, - showBorder: !containsUrl, - message: quotedMessage, - messageTheme: _streamChatTheme.otherMessageTheme, - onQuotedMessageClear: widget.onQuotedMessageCleared, - attachmentThumbnailBuilders: - widget.quotedMessageAttachmentThumbnailBuilders, - ); - } - - Widget _buildAttachments() { - final attachments = _effectiveController.attachments; - final nonOGAttachments = attachments.where((it) { - return it.titleLink == null; - }).toList(growable: false); - - // If there are no attachments, return an empty widget - if (nonOGAttachments.isEmpty) return const Empty(); - - // If the user has provided a custom attachment list builder, use that. - final attachmentListBuilder = widget.attachmentListBuilder; - if (attachmentListBuilder != null) { - return attachmentListBuilder( - context, - nonOGAttachments, - _onAttachmentRemovePressed, - ); - } - - // Otherwise, use the default attachment list builder. - return LimitedBox( - maxHeight: 240, - child: StreamMessageInputAttachmentList( - attachments: nonOGAttachments, - onRemovePressed: _onAttachmentRemovePressed, - fileAttachmentListBuilder: widget.fileAttachmentListBuilder, - mediaAttachmentListBuilder: widget.mediaAttachmentListBuilder, - voiceRecordingAttachmentBuilder: widget.voiceRecordingAttachmentBuilder, - fileAttachmentBuilder: widget.fileAttachmentBuilder, - mediaAttachmentBuilder: widget.mediaAttachmentBuilder, - voiceRecordingAttachmentListBuilder: - widget.voiceRecordingAttachmentListBuilder, - ), - ); - } - - // Default callback for removing an attachment. - Future _onAttachmentRemovePressed(Attachment attachment) async { - final file = attachment.file; - final uploadState = attachment.uploadState; - - if (file != null && !uploadState.isSuccess && !isWeb) { - await StreamAttachmentHandler.instance.deleteAttachmentFile( - attachmentFile: file, - ); - } - - _effectiveController.removeAttachmentById(attachment.id); - } - - Widget _buildCommandButton(BuildContext context) { - final s = _effectiveController.text.trim(); - final isCommandOptionsVisible = s.startsWith(_kCommandTrigger); - final defaultButton = CommandButton( - color: s.isNotEmpty - ? _streamChatTheme.colorTheme.disabled - : (isCommandOptionsVisible - ? _messageInputTheme.actionButtonColor! - : _messageInputTheme.actionButtonIdleColor!), - onPressed: () async { - // Clear the text if the commands options are already visible. - if (isCommandOptionsVisible) { - _effectiveController.clear(); - _effectiveFocusNode.unfocus(); - } else { - // This triggers the [StreamAutocomplete] to show the command trigger. - _effectiveController.textEditingValue = const TextEditingValue( - text: _kCommandTrigger, - selection: TextSelection.collapsed(offset: _kCommandTrigger.length), - ); - _effectiveFocusNode.requestFocus(); - } - }, - ); - - return widget.commandButtonBuilder?.call(context, defaultButton) ?? - defaultButton; - } - /// Adds an attachment to the [messageInputController.attachments] map void _addAttachments(Iterable attachments) { - final limit = widget.attachmentLimit; - final length = _effectiveController.attachments.length + attachments.length; - if (length > limit) { - final onAttachmentLimitExceed = widget.onAttachmentLimitExceed; - if (onAttachmentLimitExceed != null) { - return onAttachmentLimitExceed( - widget.attachmentLimit, + if (widget.attachmentLimit case final limit?) { + final length = _effectiveController.attachments.length + attachments.length; + if (length > limit) { + final onAttachmentLimitExceed = widget.onAttachmentLimitExceed; + if (onAttachmentLimitExceed != null) { + return onAttachmentLimitExceed( + limit, + context.translations.attachmentLimitExceedError(limit), + ); + } + return _showErrorAlert( context.translations.attachmentLimitExceedError(limit), ); } - return _showErrorAlert( - context.translations.attachmentLimitExceedError(limit), - ); } for (final attachment in attachments) { _effectiveController.addAttachment(attachment); @@ -1506,6 +1069,8 @@ class StreamMessageInputState extends State if (_effectiveController.isSlowModeActive) return; if (!widget.validator(_effectiveController.message)) return; + _hidePicker(); + final streamChannel = StreamChannel.maybeOf(context); if (streamChannel == null) return; @@ -1513,12 +1078,13 @@ class StreamMessageInputState extends State var message = _effectiveController.value; if (!channel.canSendLinks && - _urlRegex.allMatches(message.text ?? '').any((element) => - element.group(0)?.split('.').last.isValidTLD() == true)) { + _urlRegex + .allMatches(message.text ?? '') + .any((element) => element.group(0)?.split('.').last.isValidTLD() == true)) { showInfoBottomSheet( context, - icon: StreamSvgIcon( - icon: StreamSvgIcons.error, + icon: Icon( + context.streamIcons.exclamationCircleFill20, color: StreamChatTheme.of(context).colorTheme.accentError, size: 24, ), @@ -1565,14 +1131,17 @@ class StreamMessageInputState extends State try { // Note: edited messages which are bounced back with an error needs to be // sent as new messages as the backend doesn't store them. - final resp = await switch (_isEditing && !message.isBouncedWithError) { + // Use message.state directly rather than _isEditing, because the + // controller is reset before this method is called. + final isEditing = !message.state.isInitial; + final resp = await switch (isEditing && !message.isBouncedWithError) { true => channel.updateMessage(message), false => channel.sendMessage(message), }; // We don't want to start the cooldown if an already sent message is // being edited. - if (!_isEditing) { + if (!isEditing) { _effectiveController.startCooldown(channel.getRemainingCooldown()); } @@ -1659,88 +1228,21 @@ class StreamMessageInputState extends State @override void dispose() { - _effectiveController.removeListener(_onChangedDebounced); + _pickerAnimation.dispose(); + _pickerAnimationController.dispose(); + _disposePickerResources(); + _effectiveController + ..removeListener(_onChangedThrottled) + ..removeListener(_onChangedDebounced); _controller?.dispose(); _effectiveFocusNode.removeListener(_focusNodeListener); _focusNode?.dispose(); _onChangedDebounced.cancel(); + _onChangedThrottled.cancel(); _audioRecorderController.dispose(); _draftStreamSubscription?.cancel(); + _messageUpdatedSubscription?.cancel(); + _messageDeletedSubscription?.cancel(); super.dispose(); } } - -/// Preview of an Open Graph attachment. -class OGAttachmentPreview extends StatelessWidget { - /// Returns a new instance of [OGAttachmentPreview] - const OGAttachmentPreview({ - super.key, - required this.attachment, - this.onDismissPreviewPressed, - }); - - /// The attachment to be rendered. - final Attachment attachment; - - /// Called when the dismiss button is pressed. - final VoidCallback? onDismissPreviewPressed; - - @override - Widget build(BuildContext context) { - final chatTheme = StreamChatTheme.of(context); - final textTheme = chatTheme.textTheme; - final colorTheme = chatTheme.colorTheme; - - final attachmentTitle = attachment.title; - final attachmentText = attachment.text; - - return Row( - children: [ - Padding( - padding: const EdgeInsets.all(8), - child: StreamSvgIcon( - icon: StreamSvgIcons.link, - color: colorTheme.accentPrimary, - ), - ), - Expanded( - child: Container( - decoration: BoxDecoration( - border: Border( - left: BorderSide( - color: colorTheme.accentPrimary, - width: 2, - ), - ), - ), - padding: const EdgeInsets.only(left: 6), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (attachmentTitle != null) - Text( - attachmentTitle.trim(), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: textTheme.body.copyWith(fontWeight: FontWeight.w700), - ), - if (attachmentText != null) - Text( - attachmentText, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: textTheme.body.copyWith(fontWeight: FontWeight.w400), - ), - ], - ), - ), - ), - IconButton( - visualDensity: VisualDensity.compact, - icon: const StreamSvgIcon(icon: StreamSvgIcons.closeSmall), - onPressed: onDismissPreviewPressed, - ), - ], - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input_attachment_list.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_input_attachment_list.dart deleted file mode 100644 index b3fa41e960..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_input_attachment_list.dart +++ /dev/null @@ -1,478 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/attachment/file_attachment.dart'; -import 'package:stream_chat_flutter/src/attachment/thumbnail/media_attachment_thumbnail.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/icons/stream_svg_icon.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/utils.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; - -/// WidgetBuilder used to build the message input attachment list. -/// -/// see more: -/// - [StreamMessageInputAttachmentList] -typedef AttachmentListBuilder = Widget Function( - BuildContext context, - List attachments, - ValueSetter? onRemovePressed, -); - -/// WidgetBuilder used to build the message input attachment item. -/// -/// see more: -/// - [StreamMessageInputAttachmentList] -typedef AttachmentItemBuilder = Widget Function( - BuildContext context, - Attachment attachment, - ValueSetter? onRemovePressed, -); - -/// {@template stream_message_input_attachment_list} -/// Widget used to display the list of attachments added to the message input. -/// -/// By default, it displays the list of file attachments and media attachments -/// separately. -/// -/// You can customize the list of file attachments and media attachments using -/// [fileAttachmentListBuilder] and [mediaAttachmentListBuilder] respectively. -/// -/// You can also customize the attachment item using [fileAttachmentBuilder] and -/// [mediaAttachmentBuilder] respectively. -/// -/// You can override the default action of removing an attachment by providing -/// [onRemovePressed]. -/// {@endtemplate} -class StreamMessageInputAttachmentList extends StatelessWidget { - /// {@macro stream_message_input_attachment_list} - const StreamMessageInputAttachmentList({ - super.key, - required this.attachments, - this.onRemovePressed, - this.fileAttachmentBuilder, - this.mediaAttachmentBuilder, - this.voiceRecordingAttachmentBuilder, - this.fileAttachmentListBuilder, - this.mediaAttachmentListBuilder, - this.voiceRecordingAttachmentListBuilder, - }); - - /// List of attachments to display thumbnails for. - /// - /// Open graph should be filtered out. - final Iterable attachments; - - /// Builder used to build the file attachment item. - final AttachmentItemBuilder? fileAttachmentBuilder; - - /// Builder used to build the media attachment item. - final AttachmentItemBuilder? mediaAttachmentBuilder; - - /// Builder used to build the voice recording attachment item. - final AttachmentItemBuilder? voiceRecordingAttachmentBuilder; - - /// Builder used to build the file attachment list. - final AttachmentListBuilder? fileAttachmentListBuilder; - - /// Builder used to build the media attachment list. - final AttachmentListBuilder? mediaAttachmentListBuilder; - - /// Builder used to build the voice recording attachment list. - final AttachmentListBuilder? voiceRecordingAttachmentListBuilder; - - /// Callback called when the remove button is pressed. - final ValueSetter? onRemovePressed; - - @override - Widget build(BuildContext context) { - final groupedAttachments = attachments.groupListsBy((it) => it.type); - final (:files, :media, :voices) = ( - files: [...?groupedAttachments[AttachmentType.file]], - voices: [...?groupedAttachments[AttachmentType.voiceRecording]], - media: [ - ...?groupedAttachments[AttachmentType.image], - ...?groupedAttachments[AttachmentType.video], - ...?groupedAttachments[AttachmentType.giphy], - ...?groupedAttachments[AttachmentType.audio], - ], - ); - - // If there are no attachments, return an empty widget. - if (files.isEmpty && media.isEmpty && voices.isEmpty) { - return const Empty(); - } - - return SingleChildScrollView( - padding: const EdgeInsets.only(top: 6), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (media.isNotEmpty) - Flexible( - child: switch (mediaAttachmentListBuilder) { - final builder? => builder(context, media, onRemovePressed), - _ => MessageInputMediaAttachments( - attachments: media, - attachmentBuilder: mediaAttachmentBuilder, - onRemovePressed: onRemovePressed, - ), - }, - ), - if (voices.isNotEmpty) - Flexible( - child: switch (voiceRecordingAttachmentListBuilder) { - final builder? => builder(context, voices, onRemovePressed), - _ => MessageInputVoiceRecordingAttachments( - attachments: voices, - attachmentBuilder: voiceRecordingAttachmentBuilder, - onRemovePressed: onRemovePressed, - ), - }, - ), - if (files.isNotEmpty) - Flexible( - child: switch (fileAttachmentListBuilder) { - final builder? => builder(context, files, onRemovePressed), - _ => MessageInputFileAttachments( - attachments: files, - attachmentBuilder: fileAttachmentBuilder, - onRemovePressed: onRemovePressed, - ), - }, - ), - ].insertBetween( - Divider( - height: 16, - indent: 16, - endIndent: 16, - thickness: 1, - color: StreamChatTheme.of(context).colorTheme.disabled, - ), - ), - ), - ); - } -} - -/// Widget used to display the list of file type attachments added to the -/// message input. -class MessageInputFileAttachments extends StatelessWidget { - /// Creates a new FileAttachments widget. - const MessageInputFileAttachments({ - super.key, - required this.attachments, - this.attachmentBuilder, - this.onRemovePressed, - }); - - /// List of file type attachments to display thumbnails for. - final List attachments; - - /// Builder used to build the file type attachment item. - final AttachmentItemBuilder? attachmentBuilder; - - /// Callback called when the remove button is pressed. - final ValueSetter? onRemovePressed; - - @override - Widget build(BuildContext context) { - return ListView( - reverse: true, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.symmetric(horizontal: 8), - children: attachments.reversed.map( - (attachment) { - // If a custom builder is provided, use it. - final builder = attachmentBuilder; - if (builder != null) { - return builder(context, attachment, onRemovePressed); - } - - // Otherwise, use the default builder. - return StreamFileAttachment( - message: Message(), // Dummy message - file: attachment, - constraints: BoxConstraints.loose(Size( - MediaQuery.of(context).size.width * 0.65, - 56, - )), - trailing: Padding( - padding: const EdgeInsets.all(8), - child: RemoveAttachmentButton( - onPressed: onRemovePressed != null - ? () => onRemovePressed!(attachment) - : null, - ), - ), - ); - }, - ).insertBetween(const SizedBox(height: 8)), - ); - } -} - -/// Widget used to display the list of voice recording type attachments added to -/// the message input. -class MessageInputVoiceRecordingAttachments extends StatefulWidget { - /// Creates a new MessageInputVoiceRecordingAttachments widget. - const MessageInputVoiceRecordingAttachments({ - super.key, - required this.attachments, - this.attachmentBuilder, - this.onRemovePressed, - }); - - /// List of voice recording type attachments to display thumbnails for. - /// - /// Only attachments of type [AttachmentType.voiceRecording] are supported. - final List attachments; - - /// Builder used to build the voice recording type attachment item. - final AttachmentItemBuilder? attachmentBuilder; - - /// Callback called when the remove button is pressed. - final ValueSetter? onRemovePressed; - - @override - State createState() => - _MessageInputVoiceRecordingAttachmentsState(); -} - -class _MessageInputVoiceRecordingAttachmentsState - extends State { - late final _controller = StreamAudioPlaylistController( - widget.attachments.toPlaylist(), - ); - - @override - void initState() { - super.initState(); - _controller.initialize(); - } - - @override - void didUpdateWidget( - covariant MessageInputVoiceRecordingAttachments oldWidget, - ) { - super.didUpdateWidget(oldWidget); - final equals = const ListEquality().equals; - if (!equals(widget.attachments, oldWidget.attachments)) { - // If the attachments have changed, update the playlist. - _controller.updatePlaylist(widget.attachments.toPlaylist()); - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: _controller, - builder: (context, state, _) { - return MediaQuery.removePadding( - context: context, - // Workaround for the bottom padding issue. - // Link: https://github.com/flutter/flutter/issues/156149 - removeTop: true, - removeBottom: true, - child: ListView.separated( - shrinkWrap: true, - padding: const EdgeInsets.symmetric(horizontal: 8), - physics: const NeverScrollableScrollPhysics(), - itemCount: state.tracks.length, - separatorBuilder: (_, __) => const SizedBox(height: 8), - itemBuilder: (context, index) { - final track = state.tracks[index]; - - return StreamVoiceRecordingAttachment( - track: track, - speed: state.speed, - trailingBuilder: (_, __, ___, ____) { - final attachment = widget.attachments[index]; - return RemoveAttachmentButton( - onPressed: switch (widget.onRemovePressed) { - final callback? => () => callback(attachment), - _ => null, - }, - ); - }, - onTrackPause: _controller.pause, - onChangeSpeed: _controller.setSpeed, - onTrackPlay: () async { - // Play the track directly if it is already loaded. - if (state.currentIndex == index) return _controller.play(); - // Otherwise, load the track first and then play it. - return _controller.skipToItem(index); - }, - // Only allow seeking if the current track is the one being - // interacted with. - onTrackSeekStart: (_) async { - if (state.currentIndex != index) return; - return _controller.pause(); - }, - onTrackSeekEnd: (_) async { - if (state.currentIndex != index) return; - return _controller.play(); - }, - onTrackSeekChanged: (progress) async { - if (state.currentIndex != index) return; - - final duration = track.duration.inMicroseconds; - final seekPosition = (duration * progress).toInt(); - final seekDuration = Duration(microseconds: seekPosition); - - return _controller.seek(seekDuration); - }, - ); - }, - ), - ); - }, - ); - } -} - -/// Widget used to display the list of media type attachments added to the -/// message input. -class MessageInputMediaAttachments extends StatelessWidget { - /// Creates a new MediaAttachments widget. - const MessageInputMediaAttachments({ - super.key, - required this.attachments, - this.attachmentBuilder, - this.onRemovePressed, - }); - - /// List of media type attachments to display thumbnails for. - /// - /// Only attachments of type `image`, `video` and `giphy` are supported. Shows - /// a placeholder for other types of attachments. - final List attachments; - - /// Builder used to build the media type attachment item. - final AttachmentItemBuilder? attachmentBuilder; - - /// Callback called when the remove button is pressed. - final ValueSetter? onRemovePressed; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 104, - child: ListView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8), - cacheExtent: 104 * 10, // Cache 10 items ahead. - children: attachments.map( - (attachment) { - // If a custom builder is provided, use it. - final builder = attachmentBuilder; - if (builder != null) { - return builder(context, attachment, onRemovePressed); - } - - return StreamMediaAttachmentBuilder( - attachment: attachment, - onRemovePressed: onRemovePressed, - ); - }, - ).insertBetween(const SizedBox(width: 8)), - ), - ); - } -} - -/// Widget used to display a media type attachment item. -class StreamMediaAttachmentBuilder extends StatelessWidget { - /// Creates a new media attachment item. - const StreamMediaAttachmentBuilder( - {super.key, required this.attachment, this.onRemovePressed}); - - /// The media attachment to display. - final Attachment attachment; - - /// Callback called when the remove button is pressed. - final ValueSetter? onRemovePressed; - - @override - Widget build(BuildContext context) { - final colorTheme = StreamChatTheme.of(context).colorTheme; - final shape = RoundedRectangleBorder( - side: BorderSide( - color: colorTheme.borders, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: BorderRadius.circular(14), - ); - - return Container( - key: Key(attachment.id), - clipBehavior: Clip.hardEdge, - decoration: ShapeDecoration(shape: shape), - child: AspectRatio( - aspectRatio: 1, - child: Stack( - alignment: Alignment.center, - children: [ - StreamMediaAttachmentThumbnail( - media: attachment, - width: double.infinity, - height: double.infinity, - fit: BoxFit.cover, - ), - if (attachment.type == AttachmentType.video) - const Positioned( - left: 8, - bottom: 8, - child: StreamSvgIcon(icon: StreamSvgIcons.videoCall), - ), - Positioned( - top: 8, - right: 8, - child: RemoveAttachmentButton( - onPressed: onRemovePressed != null - ? () => onRemovePressed!(attachment) - : null, - ), - ), - ], - ), - ), - ); - } -} - -/// Material Button used for removing attachments. -class RemoveAttachmentButton extends StatelessWidget { - /// Creates a new remove attachment button. - const RemoveAttachmentButton({super.key, this.onPressed}); - - /// Callback when the remove attachment button is pressed. - final VoidCallback? onPressed; - - @override - Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); - final colorTheme = theme.colorTheme; - - return IconButton.filled( - onPressed: onPressed, - color: colorTheme.barsBg, - padding: EdgeInsets.zero, - icon: const StreamSvgIcon(icon: StreamSvgIcons.close), - style: IconButton.styleFrom( - minimumSize: const Size(24, 24), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - // ignore: deprecated_member_use - backgroundColor: colorTheme.textHighEmphasis.withOpacity(0.6), - ), - ); - } -} 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..f55c790eb0 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; @@ -83,12 +52,12 @@ class StreamMessageSendButton extends StatelessWidget { final idleIcon = switch (idleSendIcon) { final idleIcon? => idleIcon, - _ => const StreamSvgIcon(icon: StreamSvgIcons.sendMessage), + _ => Icon(context.streamIcons.send20), }; final activeIcon = switch (activeSendIcon) { final activeIcon? => activeIcon, - _ => const StreamSvgIcon(icon: StreamSvgIcons.circleUp), + _ => Icon(context.streamIcons.arrowUp20), }; final theme = StreamMessageInputTheme.of(context); diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index 0279be811d..b6e9391a79 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -9,12 +9,7 @@ import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; export 'package:flutter/services.dart' - show - TextInputType, - TextInputAction, - TextCapitalization, - SmartQuotesType, - SmartDashesType; + show TextInputType, TextInputAction, TextCapitalization, SmartQuotesType, SmartDashesType; /// A widget the wraps the [TextField] and adds some StreamChat specifics. class StreamMessageTextField extends StatefulWidget { @@ -119,42 +114,36 @@ class StreamMessageTextField extends StatefulWidget { this.scribbleEnabled = true, this.enableIMEPersonalizedLearning = true, this.contentInsertionConfiguration, - }) : assert(obscuringCharacter.length == 1, ''), - smartDashesType = smartDashesType ?? - (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), - smartQuotesType = smartQuotesType ?? - (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), - assert(maxLines == null || maxLines > 0, ''), - assert(minLines == null || minLines > 0, ''), - assert( - (maxLines == null) || (minLines == null) || (maxLines >= minLines), - "minLines can't be greater than maxLines", - ), - assert( - !expands || (maxLines == null && minLines == null), - 'minLines and maxLines must be null when expands is true.', - ), - assert(!obscureText || maxLines == 1, - 'Obscured fields cannot be multiline.'), - assert( - maxLength == null || - maxLength == TextField.noMaxLength || - maxLength > 0, - 'maxLength must be null or a positive integer.'), - - // Assert the following instead of setting it directly to avoid - // surprising the user by silently changing the value they set. - assert( - !identical(textInputAction, TextInputAction.newline) || - maxLines == 1 || - !identical(keyboardType, TextInputType.text), - 'Use keyboardType TextInputType.multiline when using ' - 'TextInputAction.newline on a multiline TextField.', - ), - keyboardType = keyboardType ?? - (maxLines == 1 ? TextInputType.text : TextInputType.multiline), - enableInteractiveSelection = - enableInteractiveSelection ?? (!readOnly || !obscureText); + }) : assert(obscuringCharacter.length == 1, ''), + smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), + smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), + assert(maxLines == null || maxLines > 0, ''), + assert(minLines == null || minLines > 0, ''), + assert( + (maxLines == null) || (minLines == null) || (maxLines >= minLines), + "minLines can't be greater than maxLines", + ), + assert( + !expands || (maxLines == null && minLines == null), + 'minLines and maxLines must be null when expands is true.', + ), + assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'), + assert( + maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0, + 'maxLength must be null or a positive integer.', + ), + + // Assert the following instead of setting it directly to avoid + // surprising the user by silently changing the value they set. + assert( + !identical(textInputAction, TextInputAction.newline) || + maxLines == 1 || + !identical(keyboardType, TextInputType.text), + 'Use keyboardType TextInputType.multiline when using ' + 'TextInputAction.newline on a multiline TextField.', + ), + keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), + enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText); /// Controls the message being edited. /// @@ -522,93 +511,78 @@ class StreamMessageTextField extends StatefulWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty( - 'controller', controller, - defaultValue: null)) - ..add(DiagnosticsProperty('focusNode', focusNode, - defaultValue: null)) + ..add(DiagnosticsProperty('controller', controller, defaultValue: null)) + ..add(DiagnosticsProperty('focusNode', focusNode, defaultValue: null)) ..add(DiagnosticsProperty('enabled', enabled, defaultValue: null)) - ..add(DiagnosticsProperty('decoration', decoration, - defaultValue: const InputDecoration())) - ..add(DiagnosticsProperty('keyboardType', keyboardType, - defaultValue: TextInputType.text)) + ..add(DiagnosticsProperty('decoration', decoration, defaultValue: const InputDecoration())) + ..add(DiagnosticsProperty('keyboardType', keyboardType, defaultValue: TextInputType.text)) ..add(DiagnosticsProperty('style', style, defaultValue: null)) - ..add(DiagnosticsProperty('autofocus', autofocus, - defaultValue: false)) - ..add(DiagnosticsProperty( - 'obscuringCharacter', obscuringCharacter, - defaultValue: '•')) - ..add(DiagnosticsProperty('obscureText', obscureText, - defaultValue: false)) - ..add(DiagnosticsProperty('autocorrect', autocorrect, - defaultValue: true)) - ..add(EnumProperty('smartDashesType', smartDashesType, - defaultValue: - obscureText ? SmartDashesType.disabled : SmartDashesType.enabled)) - ..add(EnumProperty('smartQuotesType', smartQuotesType, - defaultValue: - obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled)) - ..add(DiagnosticsProperty('enableSuggestions', enableSuggestions, - defaultValue: true)) + ..add(DiagnosticsProperty('autofocus', autofocus, defaultValue: false)) + ..add(DiagnosticsProperty('obscuringCharacter', obscuringCharacter, defaultValue: '•')) + ..add(DiagnosticsProperty('obscureText', obscureText, defaultValue: false)) + ..add(DiagnosticsProperty('autocorrect', autocorrect, defaultValue: true)) + ..add( + EnumProperty( + 'smartDashesType', + smartDashesType, + defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled, + ), + ) + ..add( + EnumProperty( + 'smartQuotesType', + smartQuotesType, + defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled, + ), + ) + ..add(DiagnosticsProperty('enableSuggestions', enableSuggestions, defaultValue: true)) ..add(IntProperty('maxLines', maxLines, defaultValue: 1)) ..add(IntProperty('minLines', minLines, defaultValue: null)) ..add(DiagnosticsProperty('expands', expands, defaultValue: false)) ..add(IntProperty('maxLength', maxLength, defaultValue: null)) - ..add(EnumProperty( - 'maxLengthEnforcement', maxLengthEnforcement, - defaultValue: null)) - ..add(EnumProperty('textInputAction', textInputAction, - defaultValue: null)) - ..add(EnumProperty( - 'textCapitalization', textCapitalization, - defaultValue: TextCapitalization.none)) - ..add(EnumProperty('textAlign', textAlign, - defaultValue: TextAlign.start)) - ..add(DiagnosticsProperty( - 'textAlignVertical', textAlignVertical, - defaultValue: null)) - ..add(EnumProperty('textDirection', textDirection, - defaultValue: null)) + ..add(EnumProperty('maxLengthEnforcement', maxLengthEnforcement, defaultValue: null)) + ..add(EnumProperty('textInputAction', textInputAction, defaultValue: null)) + ..add( + EnumProperty( + 'textCapitalization', + textCapitalization, + defaultValue: TextCapitalization.none, + ), + ) + ..add(EnumProperty('textAlign', textAlign, defaultValue: TextAlign.start)) + ..add(DiagnosticsProperty('textAlignVertical', textAlignVertical, defaultValue: null)) + ..add(EnumProperty('textDirection', textDirection, defaultValue: null)) ..add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)) ..add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)) - ..add(DiagnosticsProperty('cursorRadius', cursorRadius, - defaultValue: null)) + ..add(DiagnosticsProperty('cursorRadius', cursorRadius, defaultValue: null)) ..add(ColorProperty('cursorColor', cursorColor, defaultValue: null)) - ..add(DiagnosticsProperty( - 'keyboardAppearance', keyboardAppearance, - defaultValue: null)) - ..add(DiagnosticsProperty( - 'scrollPadding', scrollPadding, - defaultValue: const EdgeInsets.all(20))) - ..add(FlagProperty('selectionEnabled', - value: selectionEnabled, - defaultValue: true, - ifFalse: 'selection disabled')) - ..add(DiagnosticsProperty( - 'selectionControls', selectionControls, - defaultValue: null)) - ..add(DiagnosticsProperty( - 'scrollController', scrollController, - defaultValue: null)) - ..add(DiagnosticsProperty('scrollPhysics', scrollPhysics, - defaultValue: null)) - ..add(DiagnosticsProperty('clipBehavior', clipBehavior, - defaultValue: Clip.hardEdge)) - ..add(DiagnosticsProperty('scribbleEnabled', scribbleEnabled, - defaultValue: true)) - ..add(DiagnosticsProperty( - 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, - defaultValue: true)) - ..add(DiagnosticsProperty( - 'contentInsertionConfiguration', contentInsertionConfiguration, - defaultValue: null)); + ..add(DiagnosticsProperty('keyboardAppearance', keyboardAppearance, defaultValue: null)) + ..add( + DiagnosticsProperty('scrollPadding', scrollPadding, defaultValue: const EdgeInsets.all(20)), + ) + ..add( + FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'), + ) + ..add(DiagnosticsProperty('selectionControls', selectionControls, defaultValue: null)) + ..add(DiagnosticsProperty('scrollController', scrollController, defaultValue: null)) + ..add(DiagnosticsProperty('scrollPhysics', scrollPhysics, defaultValue: null)) + ..add(DiagnosticsProperty('clipBehavior', clipBehavior, defaultValue: Clip.hardEdge)) + ..add(DiagnosticsProperty('scribbleEnabled', scribbleEnabled, defaultValue: true)) + ..add( + DiagnosticsProperty('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true), + ) + ..add( + DiagnosticsProperty( + 'contentInsertionConfiguration', + contentInsertionConfiguration, + defaultValue: null, + ), + ); } } -class _StreamMessageTextFieldState extends State - with RestorationMixin { - StreamMessageInputController get _effectiveController => - widget.controller ?? _controller!.value; +class _StreamMessageTextFieldState extends State with RestorationMixin { + StreamMessageInputController get _effectiveController => widget.controller ?? _controller!.value; StreamRestorableMessageInputController? _controller; @override @@ -653,63 +627,62 @@ class _StreamMessageTextFieldState extends State @override Widget build(BuildContext context) => TextField( - controller: _effectiveController.textFieldController, - focusNode: widget.focusNode, - decoration: widget.decoration, - keyboardType: widget.keyboardType, - textInputAction: widget.textInputAction ?? - (widget.keyboardType == TextInputType.multiline - ? TextInputAction.newline - : TextInputAction.send), - textCapitalization: widget.textCapitalization, - style: widget.style, - strutStyle: widget.strutStyle, - textAlign: widget.textAlign, - textAlignVertical: widget.textAlignVertical, - textDirection: widget.textDirection, - readOnly: widget.readOnly, - showCursor: widget.showCursor, - autofocus: widget.autofocus, - obscuringCharacter: widget.obscuringCharacter, - obscureText: widget.obscureText, - autocorrect: widget.autocorrect, - smartDashesType: widget.smartDashesType, - smartQuotesType: widget.smartQuotesType, - enableSuggestions: widget.enableSuggestions, - maxLines: widget.maxLines, - minLines: widget.minLines, - expands: widget.expands, - maxLength: widget.maxLength, - maxLengthEnforcement: widget.maxLengthEnforcement, - onEditingComplete: widget.onEditingComplete, - onSubmitted: widget.onSubmitted, - onAppPrivateCommand: widget.onAppPrivateCommand, - inputFormatters: widget.inputFormatters, - enabled: widget.enabled, - cursorWidth: widget.cursorWidth, - cursorHeight: widget.cursorHeight, - cursorRadius: widget.cursorRadius, - cursorColor: widget.cursorColor, - selectionHeightStyle: widget.selectionHeightStyle, - selectionWidthStyle: widget.selectionWidthStyle, - keyboardAppearance: widget.keyboardAppearance, - scrollPadding: widget.scrollPadding, - dragStartBehavior: widget.dragStartBehavior, - enableInteractiveSelection: widget.enableInteractiveSelection, - selectionControls: widget.selectionControls, - onTap: widget.onTap, - mouseCursor: widget.mouseCursor, - buildCounter: widget.buildCounter, - scrollController: widget.scrollController, - scrollPhysics: widget.scrollPhysics, - autofillHints: widget.autofillHints, - clipBehavior: widget.clipBehavior, - restorationId: widget.restorationId, - // ignore: deprecated_member_use - scribbleEnabled: widget.scribbleEnabled, - enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - contentInsertionConfiguration: widget.contentInsertionConfiguration, - ); + controller: _effectiveController.textFieldController, + focusNode: widget.focusNode, + decoration: widget.decoration, + keyboardType: widget.keyboardType, + textInputAction: + widget.textInputAction ?? + (widget.keyboardType == TextInputType.multiline ? TextInputAction.newline : TextInputAction.send), + textCapitalization: widget.textCapitalization, + style: widget.style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + textDirection: widget.textDirection, + readOnly: widget.readOnly, + showCursor: widget.showCursor, + autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + enableSuggestions: widget.enableSuggestions, + maxLines: widget.maxLines, + minLines: widget.minLines, + expands: widget.expands, + maxLength: widget.maxLength, + maxLengthEnforcement: widget.maxLengthEnforcement, + onEditingComplete: widget.onEditingComplete, + onSubmitted: widget.onSubmitted, + onAppPrivateCommand: widget.onAppPrivateCommand, + inputFormatters: widget.inputFormatters, + enabled: widget.enabled, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorColor: widget.cursorColor, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + keyboardAppearance: widget.keyboardAppearance, + scrollPadding: widget.scrollPadding, + dragStartBehavior: widget.dragStartBehavior, + enableInteractiveSelection: widget.enableInteractiveSelection, + selectionControls: widget.selectionControls, + onTap: widget.onTap, + mouseCursor: widget.mouseCursor, + buildCounter: widget.buildCounter, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + autofillHints: widget.autofillHints, + clipBehavior: widget.clipBehavior, + restorationId: widget.restorationId, + // ignore: deprecated_member_use + scribbleEnabled: widget.scribbleEnabled, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + contentInsertionConfiguration: widget.contentInsertionConfiguration, + ); @override void dispose() { diff --git a/packages/stream_chat_flutter/lib/src/message_input/tld.dart b/packages/stream_chat_flutter/lib/src/message_input/tld.dart index 2bfa52578f..f340af4719 100644 --- a/packages/stream_chat_flutter/lib/src/message_input/tld.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/tld.dart @@ -2,9 +2,7 @@ extension TLDString on String { /// Returns true if the string is a valid TLD. bool isValidTLD() => - isNotEmpty && - tlds.containsKey(this[0].toUpperCase()) && - tlds[this[0].toUpperCase()]!.contains(toUpperCase()); + isNotEmpty && tlds.containsKey(this[0].toUpperCase()) && tlds[this[0].toUpperCase()]!.contains(toUpperCase()); } /// List of valid TLDs. 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..908d0d9dda 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 @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -16,14 +18,12 @@ 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.fadeNearInlineDivider = true, this.dateDividerBuilder, }); - /// true if this is a thread conversation - @Deprecated('No longer used, Will be removed in future versions.') - final bool isThreadConversation; + /// Viewport-fraction over which the floating divider fades out + static const _fadeRange = 0.05; /// A [ValueListenable] that provides the positions of items in the list view. final ValueListenable> itemPositionListener; @@ -38,6 +38,12 @@ class FloatingDateDivider extends StatelessWidget { /// loaders, headers, and footers. final int itemCount; + /// Whether this divider fades out when an inline date divider for the same + /// date approaches it in the viewport. + /// + /// Defaults to true. + final bool fadeNearInlineDivider; + /// A optional builder function that creates a widget to display the date /// divider. /// @@ -64,18 +70,130 @@ class FloatingDateDivider extends StatelessWidget { // Offset the index to account for two extra items // (loader and footer) at the bottom of the ListView. - final message = messages.elementAtOrNull(index - 2); + final messageIndex = index - 2; + final message = messages.elementAtOrNull(messageIndex); if (message == null) return const Empty(); - if (dateDividerBuilder case final builder?) { - return builder.call(message.createdAt.toLocal()); - } + final divider = switch (dateDividerBuilder) { + final builder? => builder.call(message.createdAt.toLocal()), + _ => StreamDateDivider(dateTime: message.createdAt.toLocal()), + }; + + if (!fadeNearInlineDivider) return divider; + + final opacity = _floatingDividerOpacity( + positions, + index, + messageIndex, + ); - return StreamDateDivider(dateTime: message.createdAt.toLocal()); + if (opacity <= 0) return const Empty(); + if (opacity >= 1) return divider; + + return Opacity(opacity: opacity, child: divider); }, ); } + double _floatingDividerOpacity( + Iterable positions, + int itemIndex, + int messageIndex, + ) { + final messageDate = messages[messageIndex].createdAt.toLocal(); + + final bool hasDateDividerAbove; + final bool hasDateDividerBelow; + + if (reverse) { + hasDateDividerAbove = + messageIndex >= messages.length - 1 || + !_isSameDay( + messageDate, + messages[messageIndex + 1].createdAt.toLocal(), + ); + hasDateDividerBelow = + messageIndex > 0 && + !_isSameDay( + messageDate, + messages[messageIndex - 1].createdAt.toLocal(), + ); + } else { + hasDateDividerAbove = + messageIndex > 0 && + !_isSameDay( + messageDate, + messages[messageIndex - 1].createdAt.toLocal(), + ); + hasDateDividerBelow = + messageIndex < messages.length - 1 && + !_isSameDay( + messageDate, + messages[messageIndex + 1].createdAt.toLocal(), + ); + } + + if (!hasDateDividerAbove && !hasDateDividerBelow) return 1; + + for (final p in positions) { + if (p.index != itemIndex) continue; + + var opacity = 1.0; + + if (reverse) { + // Fade as the inline divider ABOVE becomes visible + // (trailing edge = top of item, 1.0 = viewport top). + if (hasDateDividerAbove && p.itemTrailingEdge < 1) { + opacity = clampDouble( + (p.itemTrailingEdge - (1.0 - _fadeRange)) / _fadeRange, + 0, + 1, + ); + } + + // Fade as the inline divider BELOW approaches the viewport top + // (leading edge = bottom of item, approaching 1.0). + if (hasDateDividerBelow) { + final t = clampDouble( + ((1.0 - _fadeRange) - p.itemLeadingEdge) / _fadeRange, + 0, + 1, + ); + opacity = min(opacity, t); + } + } else { + // Fade as the inline divider ABOVE becomes visible + // (leading edge = top of item, 0.0 = viewport top). + if (hasDateDividerAbove && p.itemLeadingEdge > 0) { + opacity = clampDouble( + (_fadeRange - p.itemLeadingEdge) / _fadeRange, + 0, + 1, + ); + } + + // Fade as the inline divider BELOW approaches the viewport top + // (trailing edge = bottom of item, approaching 0.0). + if (hasDateDividerBelow) { + final t = clampDouble( + (p.itemTrailingEdge - _fadeRange) / _fadeRange, + 0, + 1, + ); + opacity = min(opacity, t); + } + } + + return opacity; + } + + return 1; + } + + static bool _isSameDay(DateTime a, DateTime b) { + return a.year == b.year && a.month == b.month && a.day == b.day; + } + // Returns True if the item index is a valid message index and not one of the // special items (like header, footer, loaders, etc.). bool _isValidMessageIndex(int index) { diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/message_details.dart b/packages/stream_chat_flutter/lib/src/message_list_view/message_details.dart index 36ffd887bf..cc5c99eccc 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/message_details.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/message_details.dart @@ -13,10 +13,8 @@ class MessageDetails { this.index, ) { isMyMessage = message.user?.id == currentUserId; - isLastUser = index + 1 < messages.length && - message.user?.id == messages[index + 1].user?.id; - isNextUser = - index - 1 >= 0 && message.user!.id == messages[index - 1].user?.id; + isLastUser = index + 1 < messages.length && message.user?.id == messages[index + 1].user?.id; + isNextUser = index - 1 >= 0 && message.user!.id == messages[index - 1].user?.id; } /// True if the message belongs to the current user 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 24fcaa3725..6c2c9767b5 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 @@ -1,4 +1,3 @@ -// ignore_for_file: lines_longer_than_80_chars import 'dart:async'; import 'dart:math'; @@ -9,12 +8,14 @@ import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positi import 'package:stream_chat_flutter/src/message_list_view/floating_date_divider.dart'; import 'package:stream_chat_flutter/src/message_list_view/loading_indicator.dart'; import 'package:stream_chat_flutter/src/message_list_view/mlv_utils.dart'; +import 'package:stream_chat_flutter/src/message_list_view/stream_message_list_empty_state.dart'; +import 'package:stream_chat_flutter/src/message_list_view/stream_message_list_skeleton_loading.dart'; import 'package:stream_chat_flutter/src/message_list_view/thread_separator.dart'; -import 'package:stream_chat_flutter/src/message_list_view/unread_indicator_button.dart'; import 'package:stream_chat_flutter/src/message_list_view/unread_messages_separator.dart'; import 'package:stream_chat_flutter/src/message_widget/ephemeral_message.dart'; import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Spacing Types (These are properties of a message to help inform the decision /// of how much space / which widget to build after it) @@ -36,6 +37,21 @@ enum SpacingType { defaultSpacing, } +/// Signature for a function that builds a message widget from its +/// [StreamMessageWidgetProps]. +/// +/// Receives the [BuildContext], the [Message] data, and the pre-configured +/// [StreamMessageWidgetProps] with all list-level callbacks already wired in. +/// +/// Use [DefaultStreamMessage] to build the default UI, optionally modifying +/// the props via [StreamMessageWidgetProps.copyWith] first. +typedef StreamMessageWidgetBuilder = + Widget Function( + BuildContext context, + Message message, + StreamMessageWidgetProps defaultProps, + ); + /// {@template streamMessageListView} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview_paint.png) @@ -84,7 +100,7 @@ class StreamMessageListView extends StatefulWidget { const StreamMessageListView({ super.key, this.showScrollToBottom = true, - this.showUnreadCountOnScrollToBottom = false, + this.showUnreadCountOnScrollToBottom = true, this.scrollToBottomBuilder, this.showUnreadIndicator = true, this.unreadIndicatorBuilder, @@ -94,6 +110,17 @@ class StreamMessageListView extends StatefulWidget { this.parentMessage, this.threadBuilder, this.onThreadTap, + this.onViewInChannelTap, + this.onEditMessageTap, + this.onReplyTap, + this.onShowMessage, + this.attachmentActionsModalBuilder, + this.swipeToReply = false, + this.onUserAvatarTap, + this.onReactionsTap, + this.onQuotedMessageTap, + this.onMessageLinkTap, + this.onUserMentionTap, this.dateDividerBuilder, this.floatingDateDividerBuilder, // we need to use ClampingScrollPhysics to avoid the list view to bounce @@ -123,6 +150,7 @@ class StreamMessageListView extends StatefulWidget { this.onModeratedMessageTap, this.onMessageLongPress, this.showFloatingDateDivider = true, + this.fadeFloatingDateDividerNearInline = true, this.threadSeparatorBuilder, this.unreadMessagesSeparatorBuilder, this.messageListController, @@ -138,8 +166,22 @@ class StreamMessageListView extends StatefulWidget { /// dismiss the keyboard automatically. final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; - /// {@macro messageBuilder} - final MessageBuilder? messageBuilder; + /// Optional builder for per-instance message customization. + /// + /// When set, this builder is called for each regular message with + /// pre-configured [StreamMessageWidgetProps] that have all list-level + /// callbacks already wired in. Use [StreamMessageWidgetProps.copyWith] + /// to modify properties, and [DefaultStreamMessage] to build the default + /// widget. + /// + /// For app-wide customization, use [StreamComponentFactory] instead. + final StreamMessageWidgetBuilder? messageBuilder; + + /// Optional builder for the parent message at the top of a thread. + /// + /// Works the same as [messageBuilder] but is called for the parent + /// message only. + final StreamMessageWidgetBuilder? parentMessageBuilder; /// Whether the view scrolls in the reading direction. /// @@ -168,9 +210,6 @@ class StreamMessageListView extends StatefulWidget { /// {@macro moderatedMessageBuilder} final ModeratedMessageBuilder? moderatedMessageBuilder; - /// {@macro parentMessageBuilder} - final ParentMessageBuilder? parentMessageBuilder; - /// {@macro threadBuilder} final ThreadBuilder? threadBuilder; @@ -180,6 +219,79 @@ class StreamMessageListView extends StatefulWidget { /// built using [threadBuilder] final ThreadTapCallback? onThreadTap; + /// Called when the "View" button on the "Also sent in channel" annotation + /// is tapped inside a thread view. + /// + /// Use this to navigate to the channel screen and scroll to / highlight + /// the given [Message]. + /// + /// When null and the thread was opened via the default [threadBuilder] + /// navigation, the thread screen is automatically popped and the channel + /// list scrolls to the message. Provide this callback to override that + /// behaviour — for example when the thread is opened from a thread list + /// or deep link where popping would not land on the channel screen. + final void Function(Message message)? onViewInChannelTap; + + /// {@macro onEditMessageTap} + /// + /// If provided, the inline edit flow is used instead of the edit bottom sheet. + final void Function(Message)? onEditMessageTap; + + /// Called when the reply action is triggered on a message. + /// + /// Forwarded to each [StreamMessageWidget] in the list. + final void Function(Message)? onReplyTap; + + /// Called when the "show in chat" action is tapped in the full-screen + /// media gallery. + /// + /// Forwarded to each [StreamMessageWidget] in the list. + final ShowMessageCallback? onShowMessage; + + /// Widget builder for the attachment actions modal shown in the full-screen + /// media gallery. + /// + /// Forwarded to each [StreamMessageWidget] in the list. + final AttachmentActionsBuilder? attachmentActionsModalBuilder; + + /// Whether swiping a message triggers a quoted-reply action. + /// + /// Forwarded to each [StreamMessageWidget] in the list via + /// [StreamMessageWidgetProps.swipeToReply]. + /// + /// Defaults to false. + final bool swipeToReply; + + /// Called when a user avatar is tapped. + /// + /// Forwarded to each [StreamMessageWidget] in the list. + final void Function(User)? onUserAvatarTap; + + /// Called when the message reactions are tapped. + /// + /// Forwarded to each [StreamMessageWidget] in the list. + final void Function(Message)? onReactionsTap; + + /// Called when a quoted message is tapped. + /// + /// When provided, this callback is forwarded to each + /// [StreamMessageWidget] in the list. + /// + /// When null (the default), tapping a quoted message scrolls to it in + /// the list, loading it if necessary. + final void Function(Message quotedMessage)? onQuotedMessageTap; + + /// Called when a link is tapped in message text. + /// + /// Receives the [Message] containing the link and the tapped URL. + /// Forwarded to each [StreamMessageWidget] in the list. + final void Function(Message message, String url)? onMessageLinkTap; + + /// Called when a user mention is tapped in message text. + /// + /// Forwarded to each [StreamMessageWidget] in the list. + final void Function(User user)? onUserMentionTap; + /// If true will show a scroll to bottom button when /// the scroll offset is not zero final bool showScrollToBottom; @@ -207,7 +319,8 @@ class StreamMessageListView extends StatefulWidget { final Widget Function( int unreadCount, Future Function(int) scrollToBottomDefaultTapAction, - )? scrollToBottomBuilder; + )? + scrollToBottomBuilder; /// If true will show an indicator with number of unread messages /// that will scroll to latest read message when tapped and mark @@ -279,6 +392,12 @@ class StreamMessageListView extends StatefulWidget { /// Flag for showing the floating date divider final bool showFloatingDateDivider; + /// Whether the floating date divider fades out when an inline date divider + /// for the same date is near the top of the viewport. + /// + /// Only has an effect when [showFloatingDateDivider] is true. + final bool fadeFloatingDateDividerNearInline; + /// Function called when messages are fetched final Widget Function(BuildContext, List)? messageListBuilder; @@ -323,12 +442,10 @@ class StreamMessageListView extends StatefulWidget { final OnMessageLongPress? onMessageLongPress; /// Builder used to build the thread separator in case it's a thread view - final Function(BuildContext context, Message parentMessage)? - threadSeparatorBuilder; + final Function(BuildContext context, Message parentMessage)? threadSeparatorBuilder; /// Builder used to build the unread message separator - final Widget Function(BuildContext context, int unreadCount)? - unreadMessagesSeparatorBuilder; + final Widget Function(BuildContext context, int unreadCount)? unreadMessagesSeparatorBuilder; /// A [MessageListController] allows pagination. /// @@ -362,7 +479,7 @@ class StreamMessageListView extends StatefulWidget { class _StreamMessageListViewState extends State { ItemScrollController? _scrollController; - void Function(Message)? _onThreadTap; + void Function(Message parentMessage, Message? threadMessage)? _onThreadTap; final ValueNotifier _showScrollToBottom = ValueNotifier(false); late final ItemPositionsListener _itemPositionListener; int? _messageListLength; @@ -388,14 +505,14 @@ class _StreamMessageListViewState extends State { List messages = []; Map messagesIndex = {}; - bool initialMessageHighlightComplete = false; + String? _highlightedMessageId; + int _highlightGeneration = 0; bool _inBetweenList = false; late final _defaultController = MessageListController(); - MessageListController get _messageListController => - widget.messageListController ?? _defaultController; + MessageListController get _messageListController => widget.messageListController ?? _defaultController; StreamSubscription? _messageNewListener; StreamSubscription? _userReadListener; @@ -407,10 +524,8 @@ class _StreamMessageListViewState extends State { super.initState(); _scrollController = widget.scrollController ?? ItemScrollController(); - _itemPositionListener = - widget.itemPositionListener ?? ItemPositionsListener.create(); - _itemPositionListener.itemPositions - .addListener(_handleItemPositionsChanged); + _itemPositionListener = widget.itemPositionListener ?? ItemPositionsListener.create(); + _itemPositionListener.itemPositions.addListener(_handleItemPositionsChanged); _getOnThreadTap(); } @@ -433,13 +548,28 @@ class _StreamMessageListViewState extends State { unreadCount = streamChannel?.channel.state?.unreadCount ?? 0; _firstUnreadMessage = streamChannel?.getFirstUnreadMessage(); - initialIndex = getInitialIndex( - widget.initialScrollIndex, - streamChannel!, - widget.messageFilter, - ); - - initialAlignment = _initialAlignment; + final highlightMessageId = widget.highlightInitialMessage + ? (streamChannel?.initialMessageId ?? _ThreadHighlightScope.of(context)) + : null; + + if (highlightMessageId != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _moveToAndHighlight( + messages: messages, + messageId: highlightMessageId, + initialScrollIndex: widget.initialScrollIndex, + scrollTo: false, + ); + }); + } else { + initialIndex = getInitialIndex( + widget.initialScrollIndex, + streamChannel!, + widget.messageFilter, + ); + initialAlignment = _initialAlignment; + } if (_scrollController?.isAttached == true) { _scrollController?.jumpTo( @@ -448,14 +578,12 @@ class _StreamMessageListViewState extends State { ); } - _messageNewListener = - streamChannel!.channel.on(EventType.messageNew).listen((event) { + _messageNewListener = streamChannel!.channel.on(EventType.messageNew).listen((event) { if (_upToDate) { _bottomPaginationActive = false; } if (event.message?.parentId == widget.parentMessage?.id && - event.message!.user!.id == - streamChannel!.channel.client.state.currentUser!.id) { + event.message!.user!.id == streamChannel!.channel.client.state.currentUser!.id) { setState(() => unreadCount = 0); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -481,49 +609,93 @@ class _StreamMessageListViewState extends State { debouncedMarkThreadRead.cancel(); _messageNewListener?.cancel(); _userReadListener?.cancel(); - _itemPositionListener.itemPositions - .removeListener(_handleItemPositionsChanged); + _itemPositionListener.itemPositions.removeListener(_handleItemPositionsChanged); super.dispose(); } + void _highlightMessage(String messageId) { + setState(() { + _highlightedMessageId = messageId; + _highlightGeneration++; + }); + } + + Future _moveToAndHighlight({ + required List messages, + String? messageId, + int? initialScrollIndex, + bool scrollTo = true, + }) async { + if (messageId != null) { + final index = messages.indexWhere((m) => m.id == messageId); + + if (index >= 0) { + if (scrollTo) { + _scrollController?.scrollTo( + index: index + 2, // +2 to account for loader and footer + duration: const Duration(seconds: 1), + curve: Curves.easeInOut, + alignment: 0.1, + ); + } else { + _scrollController?.jumpTo( + index: index + 2, // +2 to account for loader and footer + alignment: 0.1, + ); + } + } else { + await streamChannel!.loadChannelAtMessage(messageId).then((_) async { + initialIndex = getInitialIndex( + initialScrollIndex, + streamChannel!, + widget.messageFilter, + messageId: messageId, + ); + initialAlignment = 0.1; + }); + } + } else if (initialScrollIndex != null) { + _scrollController?.jumpTo( + index: initialScrollIndex, + alignment: initialAlignment, + ); + } + + if (messageId != null) { + _highlightMessage(messageId); + } + } + @override Widget build(BuildContext context) { + // TODO: Revisit this nested Portal setup during desktop reactions refactor + // and remove the extra layer if a dedicated message-list portal label is + // no longer required. return Portal( labels: const [kPortalMessageListViewLabel], - child: ScaffoldMessenger( - child: MessageListCore( - paginationLimit: widget.paginationLimit, - messageFilter: widget.messageFilter, - loadingBuilder: widget.loadingBuilder ?? - (context) => const Center( - child: CircularProgressIndicator.adaptive(), - ), - emptyBuilder: widget.emptyBuilder ?? - (context) => Center( - child: Text( - context.translations.emptyChatMessagesText, - style: _streamTheme.textTheme.footnote.copyWith( - color: _streamTheme.colorTheme.textHighEmphasis - // ignore: deprecated_member_use - .withOpacity(0.5), - ), - ), - ), - messageListBuilder: widget.messageListBuilder ?? - (context, list) => _buildListView(list), - messageListController: _messageListController, - parentMessage: widget.parentMessage, - errorBuilder: widget.errorBuilder ?? - (BuildContext context, Object error) => Center( - child: Text( - context.translations.genericErrorText, - style: _streamTheme.textTheme.footnote.copyWith( - color: _streamTheme.colorTheme.textHighEmphasis - // ignore: deprecated_member_use - .withOpacity(0.5), - ), + child: Portal( + child: ScaffoldMessenger( + child: MessageListCore( + paginationLimit: widget.paginationLimit, + messageFilter: widget.messageFilter, + loadingBuilder: widget.loadingBuilder ?? (context) => const StreamMessageListSkeletonLoading(), + emptyBuilder: widget.emptyBuilder ?? (context) => const StreamMessageListEmptyState(), + messageListBuilder: widget.messageListBuilder ?? (context, list) => _buildListView(list), + messageListController: _messageListController, + parentMessage: widget.parentMessage, + errorBuilder: + widget.errorBuilder ?? + (BuildContext context, Object error) => Center( + child: Text( + context.translations.genericErrorText, + style: _streamTheme.textTheme.footnote.copyWith( + color: _streamTheme.colorTheme.textHighEmphasis + // ignore: deprecated_member_use + .withOpacity(0.5), ), ), + ), + ), ), ), ); @@ -543,8 +715,7 @@ class _StreamMessageListViewState extends State { final first = _itemPositionListener.itemPositions.value.first; final diff = newMessagesListLength - _messageListLength!; if (diff > 0) { - if (messages[0].user?.id != - streamChannel!.channel.client.state.currentUser?.id) { + if (messages[0].user?.id != streamChannel!.channel.client.state.currentUser?.id) { initialIndex = first.index + diff; initialAlignment = first.itemLeadingEdge; } @@ -555,10 +726,11 @@ class _StreamMessageListViewState extends State { _messageListLength = newMessagesListLength; - final itemCount = messages.length + // total messages - 2 + // top + bottom loading indicator - 2 + // header + footer - 1 // parent message + final itemCount = + messages.length + // total messages + 2 + // top + bottom loading indicator + 2 + // header + footer + 1 // parent message ; final child = Stack( @@ -666,7 +838,6 @@ class _StreamMessageListViewState extends State { // BottomLoader -> 1 (count-7) // Separator(Footer -> 8??30) -> 0 (count-8) // Footer -> 0 (count-8) - separatorBuilder: (context, i) { Widget maybeBuildWithUnreadMessagesSeparator({ required Message message, @@ -693,8 +864,7 @@ class _StreamMessageListViewState extends State { } if (widget.threadSeparatorBuilder != null) { - return widget.threadSeparatorBuilder! - .call(context, widget.parentMessage!); + return widget.threadSeparatorBuilder!.call(context, widget.parentMessage!); } return ThreadSeparator( @@ -702,9 +872,7 @@ class _StreamMessageListViewState extends State { ); } if (i == itemCount - 3) { - if (widget.reverse - ? widget.headerBuilder == null - : widget.footerBuilder == null) { + if (widget.reverse ? widget.headerBuilder == null : widget.footerBuilder == null) { if (messages.isNotEmpty) { final message = messages.last; return maybeBuildWithUnreadMessagesSeparator( @@ -719,9 +887,7 @@ class _StreamMessageListViewState extends State { return const SizedBox(height: 8); } if (i == 0) { - if (widget.reverse - ? widget.footerBuilder == null - : widget.headerBuilder == null) { + if (widget.reverse ? widget.footerBuilder == null : widget.headerBuilder == null) { return const SizedBox(height: 30); } return const SizedBox(height: 8); @@ -740,8 +906,7 @@ class _StreamMessageListViewState extends State { Widget separator; - final isPartOfThread = message.replyCount! > 0 || - message.showInChannel == true; + final isPartOfThread = message.replyCount! > 0 || message.showInChannel == true; final createdAt = Jiffy.parseFromDateTime( message.createdAt.toLocal(), @@ -759,8 +924,7 @@ class _StreamMessageListViewState extends State { unit: Unit.minute, ); - final isNextUserSame = - message.user!.id == nextMessage.user?.id; + final isNextUserSame = message.user!.id == nextMessage.user?.id; final isDeleted = message.isDeleted; final spacingRules = [ @@ -795,16 +959,13 @@ class _StreamMessageListViewState extends State { if (i == itemCount - 2) { if (widget.reverse) { - return widget.headerBuilder?.call(context) ?? - const Empty(); + return widget.headerBuilder?.call(context) ?? const Empty(); } else { - return widget.footerBuilder?.call(context) ?? - const Empty(); + return widget.footerBuilder?.call(context) ?? const Empty(); } } - final indicatorBuilder = - widget.paginationLoadingIndicatorBuilder; + final indicatorBuilder = widget.paginationLoadingIndicatorBuilder; if (i == itemCount - 3) { return LoadingIndicator( @@ -828,11 +989,9 @@ class _StreamMessageListViewState extends State { if (i == 0) { if (widget.reverse) { - return widget.footerBuilder?.call(context) ?? - const Empty(); + return widget.footerBuilder?.call(context) ?? const Empty(); } else { - return widget.headerBuilder?.call(context) ?? - const Empty(); + return widget.headerBuilder?.call(context) ?? const Empty(); } } @@ -857,6 +1016,7 @@ class _StreamMessageListViewState extends State { child: FloatingDateDivider( itemCount: itemCount, reverse: widget.reverse, + fadeNearInlineDivider: widget.fadeFloatingDateDividerNearInline, itemPositionListener: _itemPositionListener.itemPositions, messages: messages, dateDividerBuilder: switch (widget.floatingDateDividerBuilder) { @@ -892,10 +1052,8 @@ class _StreamMessageListViewState extends State { ], ); - final backgroundColor = - StreamMessageListViewTheme.of(context).backgroundColor; - final backgroundImage = - StreamMessageListViewTheme.of(context).backgroundImage; + final backgroundColor = StreamMessageListViewTheme.of(context).backgroundColor; + final backgroundImage = StreamMessageListViewTheme.of(context).backgroundImage; if (backgroundColor != null || backgroundImage != null) { return DecoratedBox( @@ -913,15 +1071,14 @@ class _StreamMessageListViewState extends State { Widget _buildUnreadMessagesSeparator(int unreadCount) { final unreadMessagesSeparator = widget.unreadMessagesSeparatorBuilder?.call(context, unreadCount) ?? - UnreadMessagesSeparator(unreadCount: unreadCount); + UnreadMessagesSeparator(unreadCount: unreadCount); return unreadMessagesSeparator; } Future _paginateData( StreamChannelState? channel, QueryDirection direction, - ) => - _messageListController.paginateData!(direction: direction); + ) => _messageListController.paginateData!(direction: direction); Future scrollToBottomDefaultTapAction(int unreadCount) async { // If the channel is not up to date, we need to reload it before scrolling @@ -992,108 +1149,52 @@ class _StreamMessageListViewState extends State { return switch (widget.dateDividerBuilder) { final builder? => builder(createdAt), _ => Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: StreamDateDivider(dateTime: createdAt), - ), + padding: const EdgeInsets.symmetric(vertical: 12), + child: StreamDateDivider(dateTime: createdAt), + ), }; } Widget buildParentMessage( Message message, ) { - 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); - - final hasUrlAttachment = - message.attachments.any((it) => it.type == AttachmentType.urlPreview); - - final attachmentBorderRadius = hasUrlAttachment - ? 8.0 - : hasFileAttachment - ? 12.0 - : 14.0; - - final borderSide = isOnlyEmoji ? BorderSide.none : null; - - final defaultMessageWidget = StreamMessageWidget( - showReplyMessage: false, - showResendMessage: false, - showThreadReplyMessage: false, - showCopyMessage: false, - showDeleteMessage: false, - showEditMessage: false, - showMarkUnreadMessage: false, + final parentMessageProps = StreamMessageWidgetProps( message: message, - reverse: isMyMessage, - showUsername: !isMyMessage, - padding: const EdgeInsets.all(8), - showSendingIndicator: false, - attachmentPadding: EdgeInsets.all( - hasUrlAttachment - ? 8 - : hasFileAttachment - ? 4 - : 2, - ), - attachmentShape: RoundedRectangleBorder( - side: BorderSide( - color: _streamTheme.colorTheme.borders, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(attachmentBorderRadius), - bottomLeft: isMyMessage - ? Radius.circular(attachmentBorderRadius) - : Radius.zero, - topRight: Radius.circular(attachmentBorderRadius), - bottomRight: isMyMessage - ? Radius.zero - : Radius.circular(attachmentBorderRadius), - ), - ), - borderRadiusGeometry: BorderRadius.only( - topLeft: const Radius.circular(16), - bottomLeft: isMyMessage ? const Radius.circular(16) : Radius.zero, - topRight: const Radius.circular(16), - bottomRight: isMyMessage ? Radius.zero : const Radius.circular(16), - ), - textPadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: isOnlyEmoji ? 0 : 16.0, - ), - borderSide: borderSide, - showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show, - messageTheme: isMyMessage - ? _streamTheme.ownMessageTheme - : _streamTheme.otherMessageTheme, + swipeToReply: widget.swipeToReply, + onThreadTap: _onThreadTap, 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, + onEditMessageTap: widget.onEditMessageTap, + onReplyTap: widget.onReplyTap, + onShowMessage: widget.onShowMessage, + attachmentActionsModalBuilder: widget.attachmentActionsModalBuilder, + onUserAvatarTap: widget.onUserAvatarTap, + onReactionsTap: widget.onReactionsTap, + onQuotedMessageTap: widget.onQuotedMessageTap, + onMessageLinkTap: widget.onMessageLinkTap, + onUserMentionTap: widget.onUserMentionTap, ); - if (widget.parentMessageBuilder != null) { - return widget.parentMessageBuilder!.call( - context, - widget.parentMessage, - defaultMessageWidget, - ); - } + final userId = StreamChat.of(context).currentUser!.id; + final isMyMessage = message.user?.id == userId; + + final contentKind = resolveContentKind(message); + final isInThread = widget.parentMessage != null; - return defaultMessageWidget; + return StreamMessageLayout( + data: StreamMessageLayoutData( + stackPosition: .single, + alignment: isMyMessage ? .end : .start, + listKind: isInThread ? .thread : .channel, + contentKind: contentKind, + ), + child: Builder( + builder: (context) => switch (widget.parentMessageBuilder) { + final builder? => builder.call(context, message, parentMessageProps), + _ => StreamMessageWidget.fromProps(props: parentMessageProps), + }, + ), + ); } Widget _buildScrollToBottom() { @@ -1112,60 +1213,45 @@ class _StreamMessageListViewState extends State { scrollToBottomDefaultTapAction, ); } - final showUnreadCount = unreadCount > 0 && - streamChannel!.channel.state!.members.any((e) => - e.userId == - streamChannel!.channel.client.state.currentUser!.id); + final showUnreadCount = + unreadCount > 0 && + streamChannel!.channel.state!.members.any( + (e) => e.userId == streamChannel!.channel.client.state.currentUser!.id, + ); return Positioned( - bottom: 8, - right: 8, + bottom: 16, + right: 16, width: 40, height: 40, child: Stack( clipBehavior: Clip.none, children: [ FloatingActionButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), backgroundColor: _streamTheme.colorTheme.barsBg, onPressed: () async { return scrollToBottomDefaultTapAction(unreadCount); }, child: widget.reverse - ? StreamSvgIcon( - icon: StreamSvgIcons.down, + ? Icon( + context.streamIcons.arrowDown20, color: _streamTheme.colorTheme.textHighEmphasis, ) - : StreamSvgIcon( - icon: StreamSvgIcons.up, + : Icon( + context.streamIcons.arrowUp20, color: _streamTheme.colorTheme.textHighEmphasis, ), ), if (showUnreadCount && widget.showUnreadCountOnScrollToBottom) Positioned( - left: 0, - right: 0, - top: -10, - child: Center( - child: Material( - borderRadius: BorderRadius.circular(8), - color: - StreamChatTheme.of(context).colorTheme.accentPrimary, - child: Padding( - padding: const EdgeInsets.only( - left: 5, - right: 5, - top: 2, - bottom: 2, - ), - child: Text( - '${unreadCount > 99 ? '99+' : unreadCount}', - style: const TextStyle( - fontSize: 11, - color: Colors.white, - ), - ), - ), - ), + right: -4, + top: -4, + child: StreamBadgeNotification( + label: '${unreadCount > 99 ? '99+' : unreadCount}', + size: StreamBadgeNotificationSize.sm, ), ), ], @@ -1221,226 +1307,76 @@ class _StreamMessageListViewState extends State { return buildModeratedMessage(message); } - final userId = StreamChat.of(context).currentUser!.id; - final isMyMessage = message.user?.id == userId; - final nextMessage = index - 1 >= 0 ? messages[index - 1] : null; - final isNextUserSame = - nextMessage != null && message.user!.id == nextMessage.user!.id; - - var hasTimeDiff = false; - if (nextMessage != null) { - final createdAt = Jiffy.parseFromDateTime(message.createdAt.toLocal()); - final nextCreatedAt = Jiffy.parseFromDateTime( - nextMessage.createdAt.toLocal(), - ); - - hasTimeDiff = !createdAt.isSame(nextCreatedAt, unit: Unit.minute); - } - - final hasVoiceRecordingAttachment = message.attachments - .any((it) => it.type == AttachmentType.voiceRecording); - - final hasFileAttachment = - message.attachments.any((it) => it.type == AttachmentType.file); - - final hasUrlAttachment = - message.attachments.any((it) => it.type == AttachmentType.urlPreview); - - final isThreadMessage = - message.parentId != null && message.showInChannel == true; - - final hasReplies = message.replyCount! > 0; - - final attachmentBorderRadius = hasUrlAttachment - ? 8.0 - : hasFileAttachment - ? 12.0 - : 14.0; - - final showTimeStamp = (!isThreadMessage || _isThreadConversation) && - !hasReplies && - (hasTimeDiff || !isNextUserSame); - - final showUsername = !isMyMessage && - (!isThreadMessage || _isThreadConversation) && - !hasReplies && - (hasTimeDiff || !isNextUserSame); - - final showMarkUnread = streamChannel?.channel.config?.readEvents == true && - !isMyMessage && - (!isThreadMessage || _isThreadConversation); - - final showUserAvatar = isMyMessage - ? DisplayWidget.gone - : (hasTimeDiff || !isNextUserSame) - ? DisplayWidget.show - : DisplayWidget.hide; - - final showSendingIndicator = - isMyMessage && (index == 0 || hasTimeDiff || !isNextUserSame); - - final showInChannelIndicator = !_isThreadConversation && isThreadMessage; - final showThreadReplyIndicator = !_isThreadConversation && hasReplies; - final isOnlyEmoji = message.text?.isOnlyEmoji ?? false; - - 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( + final messageWidgetProps = StreamMessageWidgetProps( message: message, - reverse: isMyMessage, - showReactions: !message.isDeleted, - padding: const EdgeInsets.symmetric(horizontal: 8), - showInChannelIndicator: showInChannelIndicator, - showThreadReplyIndicator: showThreadReplyIndicator, - showUsername: showUsername, - showTimestamp: showTimeStamp, - showSendingIndicator: showSendingIndicator, - showUserAvatar: showUserAvatar, - showMarkUnreadMessage: showMarkUnread, - onQuotedMessageTap: (quotedMessageId) async { - if (messages.map((e) => e.id).contains(quotedMessageId)) { - final index = messages.indexWhere((m) => m.id == quotedMessageId); - _scrollController?.scrollTo( - index: index + 2, // +2 to account for loader and footer - duration: const Duration(seconds: 1), - curve: Curves.easeInOut, - alignment: 0.1, - ); - } else { - await streamChannel! - .loadChannelAtMessage(quotedMessageId) - .then((_) async { - initialIndex = 21; // 19 + 2 | 19 is the index of the message - initialAlignment = 0.1; - }); - } - }, - showEditMessage: isMyMessage, - showDeleteMessage: isMyMessage, - showThreadReplyMessage: - !isThreadMessage && streamChannel?.channel.canSendReply == true, - showFlagButton: !isMyMessage, - borderSide: borderSide, + swipeToReply: widget.swipeToReply, onThreadTap: _onThreadTap, - attachmentShape: RoundedRectangleBorder( - side: BorderSide( - color: _streamTheme.colorTheme.borders, - strokeAlign: BorderSide.strokeAlignOutside, + onViewInChannelTap: _isThreadConversation + ? widget.onViewInChannelTap ?? (message) => Navigator.of(context).pop(message.id) + : null, + onMessageTap: widget.onMessageTap, + onMessageLongPress: widget.onMessageLongPress, + onEditMessageTap: widget.onEditMessageTap, + onReplyTap: widget.onReplyTap, + onShowMessage: switch (widget.onShowMessage) { + final onTap? => onTap, + _ => (message, _) => _moveToAndHighlight( + messageId: message.id, + messages: messages, ), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(attachmentBorderRadius), - bottomLeft: isMyMessage - ? Radius.circular(attachmentBorderRadius) - : Radius.circular( - (hasTimeDiff || !isNextUserSame) && - !(hasReplies || - isThreadMessage || - hasFileAttachment || - hasVoiceRecordingAttachment) - ? 0 - : attachmentBorderRadius, - ), - topRight: Radius.circular(attachmentBorderRadius), - bottomRight: isMyMessage - ? Radius.circular( - (hasTimeDiff || !isNextUserSame) && - !(hasReplies || - isThreadMessage || - hasFileAttachment || - hasVoiceRecordingAttachment) - ? 0 - : attachmentBorderRadius, - ) - : Radius.circular(attachmentBorderRadius), + }, + attachmentActionsModalBuilder: widget.attachmentActionsModalBuilder, + onUserAvatarTap: widget.onUserAvatarTap, + onReactionsTap: widget.onReactionsTap, + onMessageLinkTap: widget.onMessageLinkTap, + onUserMentionTap: widget.onUserMentionTap, + onQuotedMessageTap: switch (widget.onQuotedMessageTap) { + final onTap? => onTap, + _ => (quotedMessage) => _moveToAndHighlight( + messageId: quotedMessage.id, + messages: messages, ), + }, + ); + + final userId = StreamChat.of(context).currentUser!.id; + final isMyMessage = message.user?.id == userId; + final nextMessage = index - 1 >= 0 ? messages[index - 1] : null; + final prevMessage = index + 1 < messages.length ? messages[index + 1] : null; + + final contentKind = resolveContentKind(message); + final isInThread = widget.parentMessage != null; + final stackPosition = computeStackPosition(message: message, previous: prevMessage, next: nextMessage); + + Widget child = StreamMessageLayout( + data: StreamMessageLayoutData( + stackPosition: stackPosition, + alignment: isMyMessage ? .end : .start, + listKind: isInThread ? .thread : .channel, + contentKind: contentKind, ), - attachmentPadding: EdgeInsets.all( - hasUrlAttachment - ? 8 - : hasFileAttachment || hasVoiceRecordingAttachment - ? 4 - : 2, - ), - borderRadiusGeometry: BorderRadius.only( - topLeft: const Radius.circular(16), - bottomLeft: isMyMessage - ? const Radius.circular(16) - : Radius.circular( - (hasTimeDiff || !isNextUserSame) && - !(hasReplies || isThreadMessage) - ? 0 - : 16, - ), - topRight: const Radius.circular(16), - bottomRight: isMyMessage - ? Radius.circular( - (hasTimeDiff || !isNextUserSame) && - !(hasReplies || isThreadMessage) - ? 0 - : 16, - ) - : const Radius.circular(16), - ), - textPadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: isOnlyEmoji ? 0 : 16.0, + child: Builder( + builder: (context) => switch (widget.messageBuilder) { + final builder? => builder.call(context, message, messageWidgetProps), + _ => StreamMessageWidget.fromProps(props: messageWidgetProps), + }, ), - messageTheme: isMyMessage - ? _streamTheme.ownMessageTheme - : _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) { - messageWidget = widget.messageBuilder!( - context, - MessageDetails( - userId, - message, - messages, - index, - ), - messages, - messageWidget as StreamMessageWidget, - ); - } - - var child = messageWidget; - if (!initialMessageHighlightComplete && - widget.highlightInitialMessage && - isInitialMessage(message.id, streamChannel)) { - final colorTheme = _streamTheme.colorTheme; - final highlightColor = - widget.messageHighlightColor ?? colorTheme.highlight; + if (_highlightedMessageId == message.id) { + final colorScheme = context.streamColorScheme; + final highlightColor = widget.messageHighlightColor ?? colorScheme.backgroundHighlight; child = TweenAnimationBuilder( - tween: ColorTween( - begin: highlightColor, - // ignore: deprecated_member_use - end: colorTheme.barsBg.withOpacity(0), - ), + key: ValueKey('highlight-$_highlightGeneration'), + tween: ColorTween(begin: highlightColor, end: highlightColor.withValues(alpha: 0)), duration: const Duration(seconds: 3), - onEnd: () => initialMessageHighlightComplete = true, - builder: (_, color, child) => ColoredBox( - color: color!, - child: child, - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: child, - ), + onEnd: () { + if (_highlightedMessageId == message.id) { + setState(() => _highlightedMessageId = null); + } + }, + builder: (_, color, child) => ColoredBox(color: color!, child: child), + child: Padding(padding: const EdgeInsets.symmetric(vertical: 4), child: child), ); } @@ -1518,31 +1454,68 @@ 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, + _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 parentMessage, Message? threadMessage) { + onThreadTap( + parentMessage, + threadBuilder?.call(context, parentMessage), ); - }; - } 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), + }, + // 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 parentMessage, Message? threadMessage) async { + Widget 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: parentMessage, + stream: streamChannel!.channel.state?.messagesStream.map( + (it) => it.firstWhere((m) => m.id == parentMessage.id), ), + builder: (_, data) => threadBuilder(context, data), ), ), ); - }; - } + + if (threadMessage != null) { + threadPage = _ThreadHighlightScope( + messageId: threadMessage.id, + child: threadPage, + ); + } + + final result = await Navigator.of(context).push( + MaterialPageRoute(builder: (_) => threadPage), + ); + + if (result != null && mounted) { + _moveToAndHighlight(messageId: result, messages: messages); + } + }, + _ => null, + }; + } +} + +class _ThreadHighlightScope extends InheritedWidget { + const _ThreadHighlightScope({ + required this.messageId, + required super.child, + }); + + final String messageId; + + static String? of(BuildContext context) { + return context.findAncestorWidgetOfExactType<_ThreadHighlightScope>()?.messageId; } + + @override + bool updateShouldNotify(_ThreadHighlightScope oldWidget) => messageId != oldWidget.messageId; } diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/mlv_utils.dart b/packages/stream_chat_flutter/lib/src/message_list_view/mlv_utils.dart index 00177e0fb5..8313dd89dd 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/mlv_utils.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/mlv_utils.dart @@ -7,8 +7,9 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; int getInitialIndex( int? initialScrollIndex, StreamChannelState channelState, - bool Function(Message)? messageFilter, -) { + bool Function(Message)? messageFilter, { + String? messageId, +}) { if (initialScrollIndex != null) return initialScrollIndex; final channel = channelState.channel; @@ -16,17 +17,17 @@ int getInitialIndex( if (currentUser == null) return 0; final messages = [ - ...channelState.channel.state!.messages - .where(messageFilter ?? defaultMessageFilter(currentUser.id)) + ...channelState.channel.state!.messages.where(messageFilter ?? defaultMessageFilter(currentUser.id)), ].reversed.toList(growable: false); - // Return the initial message index if available. - if (channelState.initialMessageId case final initialMessageId?) { - final initialMessageIndex = messages.indexWhere( - (it) => it.id == initialMessageId, + // Return the target message index if available. + final targetMessageId = messageId ?? channelState.initialMessageId; + if (targetMessageId != null) { + final targetMessageIndex = messages.indexWhere( + (it) => it.id == targetMessageId, ); - if (initialMessageIndex != -1) return initialMessageIndex + 2; + if (targetMessageIndex != -1) return targetMessageIndex + 2; } // Otherwise, return the first unread message index if available. @@ -101,3 +102,67 @@ bool isElementAtIndexVisible( bool isInitialMessage(String id, StreamChannelState? channelState) { return channelState!.initialMessageId == id; } + +/// Computes the [StreamMessageStackPosition] for [message] based on its +/// [previous] and [next] neighbors in the message list. +/// +/// A new group starts when: +/// - The neighbor is null (first/last message) +/// - The sender changes +/// - The timestamps fall in different calendar minutes +/// - The neighbor is a system, ephemeral, or error message +StreamMessageStackPosition computeStackPosition({ + required Message message, + Message? previous, + Message? next, +}) { + final isFirst = _isGroupBoundary(message, previous); + final isLast = _isGroupBoundary(message, next); + + return switch ((isFirst, isLast)) { + (true, true) => StreamMessageStackPosition.single, + (true, false) => StreamMessageStackPosition.top, + (false, false) => StreamMessageStackPosition.middle, + (false, true) => StreamMessageStackPosition.bottom, + }; +} + +bool _isGroupBoundary(Message message, Message? neighbor) { + if (neighbor == null) return true; + if (message.user?.id != neighbor.user?.id) return true; + if (neighbor.isSystem || neighbor.isEphemeral || neighbor.isError) return true; + + final createdAt = Jiffy.parseFromDateTime(message.createdAt.toLocal()); + final neighborCreatedAt = Jiffy.parseFromDateTime(neighbor.createdAt.toLocal()); + if (!createdAt.isSame(neighborCreatedAt, unit: Unit.minute)) return true; + + return false; +} + +/// Returns the [StreamMessageContentKind] for [message] based on its text, +/// attachments, poll, and quoted reply. +/// +/// The result is [StreamMessageContentKind.singleAttachment] when: +/// - There is no text and no quoted reply +/// - There is exactly one attachment or a poll +/// +/// The result is [StreamMessageContentKind.jumbomoji] when: +/// - There is no quoted reply, no poll, and no attachments +/// - The text contains only 1-3 emoji graphemes +StreamMessageContentKind resolveContentKind(Message message) { + final hasText = message.text?.isNotEmpty == true; + final hasQuote = message.quotedMessage != null; + final hasPoll = message.poll != null; + final attachmentCount = message.attachments.length; + + if (!hasText && !hasQuote && (hasPoll || attachmentCount == 1)) { + return .singleAttachment; + } + + if (!hasQuote && attachmentCount == 0) { + final emojiCount = StreamMessageText.emojiCount(message.text); + if (emojiCount != null && emojiCount <= 3) return .jumbomoji; + } + + return .standard; +} diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/stream_message_list_empty_state.dart b/packages/stream_chat_flutter/lib/src/message_list_view/stream_message_list_empty_state.dart new file mode 100644 index 0000000000..4273856a0c --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_list_view/stream_message_list_empty_state.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A widget that is used to display the empty state of the message list. +class StreamMessageListEmptyState extends StatelessWidget { + /// Creates a new instance of the [StreamMessageListEmptyState]. + const StreamMessageListEmptyState({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + context.streamIcons.messageBubble32, + size: 32, + ), + SizedBox(height: context.streamSpacing.sm), + Text(context.translations.sendMessageToStartConversationText), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/stream_message_list_skeleton_loading.dart b/packages/stream_chat_flutter/lib/src/message_list_view/stream_message_list_skeleton_loading.dart new file mode 100644 index 0000000000..7a1c7f3780 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_list_view/stream_message_list_skeleton_loading.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// A shimmer loading placeholder for the message list view. +/// +/// Displays a skeleton UI with shimmer animation that mimics a chat +/// conversation with incoming (left-aligned) and outgoing (right-aligned) +/// message bubbles using [StreamSkeletonLoading] and [StreamSkeletonBox]. +class StreamMessageListSkeletonLoading extends StatelessWidget { + /// Creates a new instance of [StreamMessageListSkeletonLoading]. + const StreamMessageListSkeletonLoading({super.key}); + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + return StreamSkeletonLoading( + child: LayoutBuilder( + builder: (context, constraints) { + return Padding( + padding: EdgeInsets.all(spacing.md), + child: Column( + children: [ + _IncomingBubble(), + SizedBox(height: spacing.lg), + _OutgoingBubble(), + SizedBox(height: spacing.lg), + _IncomingBubble(), + SizedBox(height: spacing.lg), + _OutgoingBubble(), + SizedBox(height: spacing.lg), + _IncomingBubble(), + SizedBox(height: spacing.md), + ], + ), + ); + }, + ), + ); + } +} + +class _IncomingBubble extends StatelessWidget { + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const StreamSkeletonBox.circular(radius: 16), + SizedBox(width: spacing.xs), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StreamSkeletonBox( + height: 56, + borderRadius: BorderRadius.only( + topRight: context.streamRadius.xl, + bottomRight: context.streamRadius.xl, + topLeft: context.streamRadius.xl, + ), + ), + SizedBox(height: spacing.xs), + StreamSkeletonBox( + width: 56, + height: 12, + borderRadius: BorderRadius.all(context.streamRadius.max), + ), + SizedBox(height: spacing.xs), + ], + ), + ), + const Spacer( + flex: 1, + ), + ], + ); + } +} + +class _OutgoingBubble extends StatelessWidget { + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + return Row( + children: [ + const Spacer( + flex: 1, + ), + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + StreamSkeletonBox( + height: 56, + borderRadius: BorderRadius.all( + context.streamRadius.xl, + ), + ), + SizedBox(height: spacing.xs), + StreamSkeletonBox( + width: 56, + height: 12, + borderRadius: BorderRadius.all(context.streamRadius.max), + ), + ], + ), + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/thread_separator.dart b/packages/stream_chat_flutter/lib/src/message_list_view/thread_separator.dart index 186c4f75a6..39bf67e6b6 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/thread_separator.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/thread_separator.dart @@ -18,16 +18,27 @@ class ThreadSeparator extends StatelessWidget { @override Widget build(BuildContext context) { final replyCount = parentMessage!.replyCount!; - return DecoratedBox( - decoration: BoxDecoration( - gradient: StreamChatTheme.of(context).colorTheme.bgGradient, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - context.translations.threadSeparatorText(replyCount), - textAlign: TextAlign.center, - style: StreamChannelHeaderTheme.of(context).subtitleStyle, + final spacing = context.streamSpacing; + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + + return Padding( + padding: EdgeInsets.symmetric(vertical: spacing.xs), + child: DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.backgroundSurfaceSubtle, + border: Border( + top: BorderSide(color: colorScheme.borderSubtle), + bottom: BorderSide(color: colorScheme.borderSubtle), + ), + ), + child: Padding( + padding: EdgeInsets.all(spacing.xs), + child: Text( + context.translations.threadSeparatorText(replyCount), + textAlign: TextAlign.center, + style: textTheme.metadataEmphasis, + ), ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/unread_indicator_button.dart b/packages/stream_chat_flutter/lib/src/message_list_view/unread_indicator_button.dart index 6c66c118ce..3c79073550 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/unread_indicator_button.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/unread_indicator_button.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/components/stream_chat_component_builders.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'; -import 'package:svg_icon_widget/svg_icon_widget.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Function signature for handling the dismiss action on the unread indicator. typedef OnUnreadIndicatorDismissTap = Future Function(); @@ -18,11 +17,38 @@ typedef OnUnreadIndicatorTap = Future Function(String? lastReadMessageId); /// [unreadCount] is the number of unread messages. /// [onTap] is called when the indicator is tapped. /// [onDismissTap] is called when the dismiss action is triggered. -typedef UnreadIndicatorBuilder = Widget Function( - int unreadCount, - OnUnreadIndicatorTap onTap, - OnUnreadIndicatorDismissTap onDismissTap, -); +typedef UnreadIndicatorBuilder = + Widget Function( + int unreadCount, + OnUnreadIndicatorTap onTap, + OnUnreadIndicatorDismissTap onDismissTap, + ); + +/// Properties for configuring an [UnreadIndicatorButton]. +/// +/// This class holds all the configuration options for an unread indicator, +/// allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [UnreadIndicatorButton], which uses these properties. +class UnreadIndicatorProps { + /// Creates properties for an unread indicator. + const UnreadIndicatorProps({ + required this.unreadCount, + required this.onTap, + required this.onDismissTap, + }); + + /// The number of unread messages. + final int unreadCount; + + /// Callback triggered when the indicator is tapped. + final OnUnreadIndicatorTap onTap; + + /// Callback triggered when the dismiss button is tapped. + final OnUnreadIndicatorDismissTap onDismissTap; +} /// {@template unreadIndicatorButton} /// A button that displays the number of unread messages in a channel. @@ -52,7 +78,8 @@ class UnreadIndicatorButton extends StatelessWidget { /// Optional builder for customizing the appearance of the unread indicator. /// - /// If not provided, a default indicator will be built. + /// If not provided, falls back to [StreamComponentFactory], then to the + /// default indicator. final UnreadIndicatorBuilder? unreadIndicatorBuilder; @override @@ -67,46 +94,68 @@ class UnreadIndicatorButton extends StatelessWidget { final unreadCount = currentUserRead.unreadMessages; if (unreadCount <= 0) return const Empty(); + final props = UnreadIndicatorProps( + unreadCount: unreadCount, + onTap: onTap, + onDismissTap: onDismissTap, + ); + if (unreadIndicatorBuilder case final builder?) { return builder(unreadCount, onTap, onDismissTap); } - final theme = StreamChatTheme.of(context); - final textTheme = theme.textTheme; - final colorTheme = theme.colorTheme; + final factoryBuilder = context.chatComponentBuilder(); + if (factoryBuilder != null) return factoryBuilder(context, props); + + final colorTheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; return Material( - elevation: 4, + elevation: 3, clipBehavior: Clip.antiAlias, - color: colorTheme.textLowEmphasis, + color: colorTheme.backgroundElevation1, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18), + borderRadius: BorderRadiusDirectional.all(context.streamRadius.max), ), - child: InkWell( - onTap: () => onTap(currentUserRead.lastReadMessageId), - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 2, 8, 2), - child: Row( - children: [ - Text( - context.translations.unreadCountIndicatorLabel( - unreadCount: unreadCount, - ), - style: textTheme.body.copyWith(color: colorTheme.barsBg), - ), - const SizedBox(width: 12), - IconButton( - iconSize: 24, - icon: const SvgIcon(StreamSvgIcons.close), - padding: const EdgeInsets.all(4), - style: IconButton.styleFrom( - foregroundColor: colorTheme.barsBg, - minimumSize: const Size.square(24), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - onPressed: onDismissTap, + child: SizedBox( + height: 40, + child: InkWell( + onTap: () => onTap(currentUserRead.lastReadMessageId), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 2, 8, 2), + child: IntrinsicHeight( + child: Row( + children: [ + Icon( + context.streamIcons.arrowUp20, + size: 20, + ), + SizedBox(width: context.streamSpacing.xs), + Text( + context.translations.unreadCountIndicatorLabel( + unreadCount: unreadCount, + ), + style: textTheme.bodyEmphasis.copyWith(color: colorTheme.textSecondary), + ), + SizedBox(width: context.streamSpacing.md), + VerticalDivider( + color: colorTheme.borderDefault, + thickness: 1, + ), + IconButton( + iconSize: 20, + icon: Icon(context.streamIcons.xmark20), + padding: const EdgeInsets.all(5), + style: IconButton.styleFrom( + foregroundColor: colorTheme.textSecondary, + minimumSize: const Size.square(20), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: onDismissTap, + ), + ], ), - ], + ), ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_list_view/unread_messages_separator.dart b/packages/stream_chat_flutter/lib/src/message_list_view/unread_messages_separator.dart index b32c36d8eb..d3dd550f38 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view/unread_messages_separator.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view/unread_messages_separator.dart @@ -15,18 +15,26 @@ class UnreadMessagesSeparator extends StatelessWidget { @override Widget build(BuildContext context) { + final spacing = context.streamSpacing; + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: EdgeInsets.symmetric(vertical: spacing.xs), child: DecoratedBox( decoration: BoxDecoration( - gradient: StreamChatTheme.of(context).colorTheme.bgGradient, + color: colorScheme.backgroundSurfaceSubtle, + border: Border( + top: BorderSide(color: colorScheme.borderSubtle), + bottom: BorderSide(color: colorScheme.borderSubtle), + ), ), child: Padding( - padding: const EdgeInsets.all(8), + padding: EdgeInsets.all(spacing.xs), child: Text( context.translations.unreadMessagesSeparatorText(), textAlign: TextAlign.center, - style: StreamChannelHeaderTheme.of(context).subtitleStyle, + style: textTheme.metadataEmphasis, ), ), ), 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..7b63f838f9 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_modal/message_actions_modal.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.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.alignment, + this.showReactionPicker = false, + this.leadingInset = 0, + }); + + /// 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 widgets that will be displayed as actions in the modal. + /// + /// Typically built by [StreamMessageActionsBuilder] and optionally modified + /// by [StreamMessageWidget.actionsBuilder]. Each item is rendered directly + /// as a child of [StreamContextMenu]. + final List messageActions; + + /// The widget representing the message being acted upon. + /// + /// This is typically displayed in the content section of the modal as a + /// reference for the user. + final Widget messageWidget; + + /// Alignment of the modal content. + /// + /// When null (the default), falls back to + /// [StreamMessagePlacement.alignmentDirectionalOf]. + final AlignmentGeometry? alignment; + + /// 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; + + /// Horizontal offset applied to the header (reaction picker) and footer (actions menu) + /// to align them with the message bubble content rather than the full message row. + /// + /// Defaults to `0` (no offset). + final double leadingInset; + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + final effectiveAlignment = alignment ?? StreamMessageLayout.alignmentDirectionalOf(context); + + void onReactionPicked(Reaction reaction) { + final action = SelectReaction(message: message, reaction: reaction); + return Navigator.pop(context, action); + } + + final insetPadding = EdgeInsetsDirectional.only(start: leadingInset); + + return StreamMessageDialog( + spacing: spacing.xs, + alignment: effectiveAlignment, + headerBuilder: switch (showReactionPicker) { + true => (context) => Padding( + padding: insetPadding, + child: StreamMessageReactionPicker( + message: message, + onReactionPicked: onReactionPicked, + ), + ), + false => null, + }, + contentBuilder: (context) => IgnorePointer(child: messageWidget), + footerBuilder: (context) => Padding( + padding: insetPadding, + child: StreamContextMenu(children: messageActions), + ), + ); + } +} 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..55d3de761a --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_modal/message_modal.dart @@ -0,0 +1,125 @@ +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 is laid out as a [Column] with three optional sections: +/// header, content, and footer. It adjusts 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] and [footerBuilder] are optional and + /// can be used to add sections above and below the main content. + const StreamMessageDialog({ + super.key, + this.spacing = 8.0, + this.headerBuilder, + required this.contentBuilder, + this.footerBuilder, + this.useSafeArea = true, + this.insetAnimationDuration = const Duration(milliseconds: 100), + this.insetAnimationCurve = Curves.decelerate, + this.insetPadding = const EdgeInsets.all(16), + this.alignment = Alignment.center, + }); + + /// Vertical spacing between 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; + + /// Optional builder for the footer section of the dialog. + final WidgetBuilder? footerBuilder; + + /// 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), + contentBuilder(context), + if (footerBuilder case final builder?) Flexible(child: builder(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/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..97bdd821a2 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_modal/moderated_message_actions_modal.dart @@ -0,0 +1,75 @@ +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'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.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, + }); + + /// 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 [StreamContextMenuAction] object. + final List messageActions; + + @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: () => Navigator.pop(context, action.props.value), + isDestructiveAction: action.props.isDestructive, + child: action.props.label, + ), + ), + ]; + + return AlertDialog.adaptive( + clipBehavior: Clip.antiAlias, + backgroundColor: colorTheme.barsBg, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + icon: Icon(context.streamIcons.flag20), + 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/bottom_row.dart b/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart deleted file mode 100644 index 0b408aa750..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/bottom_row.dart +++ /dev/null @@ -1,302 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/message_widget/sending_indicator_builder.dart'; -import 'package:stream_chat_flutter/src/message_widget/thread_painter.dart'; -import 'package:stream_chat_flutter/src/message_widget/thread_participants.dart'; -import 'package:stream_chat_flutter/src/message_widget/username.dart'; -import 'package:stream_chat_flutter/src/misc/timestamp.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template bottomRow} -/// The bottom row of a [StreamMessageWidget]. -/// -/// Used in [MessageWidgetContent]. Should not be used elsewhere. -/// {@endtemplate} -class BottomRow extends StatelessWidget { - /// {@macro bottomRow} - const BottomRow({ - super.key, - required this.isDeleted, - required this.message, - required this.showThreadReplyIndicator, - required this.showInChannel, - required this.showTimeStamp, - required this.showUsername, - required this.showEditedLabel, - required this.reverse, - required this.showSendingIndicator, - required this.hasUrlAttachments, - required this.isGiphy, - required this.isOnlyEmoji, - required this.messageTheme, - required this.streamChatTheme, - required this.hasNonUrlAttachments, - required this.streamChat, - this.deletedBottomRowBuilder, - this.onThreadTap, - this.usernameBuilder, - this.sendingIndicatorBuilder, - }); - - /// {@macro messageIsDeleted} - final bool isDeleted; - - /// {@macro deletedBottomRowBuilder} - final Widget Function(BuildContext, Message)? deletedBottomRowBuilder; - - /// {@macro message} - final Message message; - - /// {@macro showThreadReplyIndicator} - final bool showThreadReplyIndicator; - - /// {@macro showInChannelIndicator} - final bool showInChannel; - - /// {@macro showTimestamp} - final bool showTimeStamp; - - /// {@macro showUsername} - final bool showUsername; - - /// {@macro showEdited} - final bool showEditedLabel; - - /// {@macro reverse} - final bool reverse; - - /// {@macro showSendingIndicator} - final bool showSendingIndicator; - - /// {@macro hasUrlAttachments} - final bool hasUrlAttachments; - - /// {@macro isGiphy} - final bool isGiphy; - - /// {@macro isOnlyEmoji} - final bool isOnlyEmoji; - - /// {@macro hasNonUrlAttachments} - final bool hasNonUrlAttachments; - - /// {@macro messageTheme} - final StreamMessageThemeData messageTheme; - - /// {@macro onThreadTap} - final void Function(Message)? onThreadTap; - - /// {@macro streamChatThemeData} - final StreamChatThemeData streamChatTheme; - - /// {@macro streamChat} - final StreamChatState streamChat; - - /// {@macro usernameBuilder} - final Widget Function(BuildContext, Message)? usernameBuilder; - - /// {@macro sendingIndicatorBuilder} - final Widget Function(BuildContext, Message)? sendingIndicatorBuilder; - - /// {@template copyWith} - /// Creates a copy of [BottomRow] with specified attributes - /// overridden. - /// {@endtemplate} - BottomRow copyWith({ - Key? key, - bool? isDeleted, - Message? message, - bool? showThreadReplyIndicator, - bool? showInChannel, - bool? showTimeStamp, - bool? showUsername, - bool? showEditedLabel, - bool? reverse, - bool? showSendingIndicator, - bool? hasUrlAttachments, - bool? isGiphy, - bool? isOnlyEmoji, - StreamMessageThemeData? messageTheme, - StreamChatThemeData? streamChatTheme, - bool? hasNonUrlAttachments, - StreamChatState? streamChat, - Widget Function(BuildContext, Message)? deletedBottomRowBuilder, - void Function(Message)? onThreadTap, - Widget Function(BuildContext, Message)? usernameBuilder, - Widget Function(BuildContext, Message)? sendingIndicatorBuilder, - }) => - BottomRow( - key: key ?? this.key, - isDeleted: isDeleted ?? this.isDeleted, - message: message ?? this.message, - showThreadReplyIndicator: - showThreadReplyIndicator ?? this.showThreadReplyIndicator, - showInChannel: showInChannel ?? this.showInChannel, - showTimeStamp: showTimeStamp ?? this.showTimeStamp, - showUsername: showUsername ?? this.showUsername, - showEditedLabel: showEditedLabel ?? this.showEditedLabel, - reverse: reverse ?? this.reverse, - showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator, - hasUrlAttachments: hasUrlAttachments ?? this.hasUrlAttachments, - isGiphy: isGiphy ?? this.isGiphy, - isOnlyEmoji: isOnlyEmoji ?? this.isOnlyEmoji, - messageTheme: messageTheme ?? this.messageTheme, - streamChatTheme: streamChatTheme ?? this.streamChatTheme, - hasNonUrlAttachments: hasNonUrlAttachments ?? this.hasNonUrlAttachments, - streamChat: streamChat ?? this.streamChat, - deletedBottomRowBuilder: - deletedBottomRowBuilder ?? this.deletedBottomRowBuilder, - onThreadTap: onThreadTap ?? this.onThreadTap, - usernameBuilder: usernameBuilder ?? this.usernameBuilder, - sendingIndicatorBuilder: - sendingIndicatorBuilder ?? this.sendingIndicatorBuilder, - ); - - @override - Widget build(BuildContext context) { - if (isDeleted) { - final deletedBottomRowBuilder = this.deletedBottomRowBuilder; - if (deletedBottomRowBuilder != null) { - return deletedBottomRowBuilder(context, message); - } - } - - final threadParticipants = message.threadParticipants?.take(2); - final showThreadParticipants = threadParticipants?.isNotEmpty == true; - final replyCount = message.replyCount; - final isEdited = message.messageTextUpdatedAt != null; - - var msg = context.translations.threadReplyLabel; - if (showThreadReplyIndicator && replyCount! > 1) { - msg = context.translations.threadReplyCountText(replyCount); - } - - Future _onThreadTap() async { - try { - var message = this.message; - if (showInChannel) { - final channel = StreamChannel.of(context); - message = await channel.getMessage(message.parentId!); - } - return onThreadTap?.call(message); - } catch (e, stk) { - debugPrint('Error while fetching message: $e, $stk'); - } - } - - const usernameKey = Key('username'); - - final children = [ - if (showSendingIndicator) - switch (sendingIndicatorBuilder) { - final builder? => builder(context, message), - _ => SendingIndicatorBuilder( - messageTheme: messageTheme, - message: message, - hasNonUrlAttachments: hasNonUrlAttachments, - streamChat: streamChat, - streamChatTheme: streamChatTheme, - ), - }, - if (showUsername) - switch (usernameBuilder) { - final builder? => builder(context, message), - _ => Username( - key: usernameKey, - message: message, - messageTheme: messageTheme, - ), - }, - if (showEditedLabel && isEdited) - Text( - context.translations.editedMessageLabel, - style: messageTheme.createdAtStyle, - ), - if (showTimeStamp) - StreamTimestamp( - date: message.createdAt.toLocal(), - style: messageTheme.createdAtStyle, - formatter: (context, date) { - if (messageTheme.createdAtFormatter case final formatter?) { - return formatter.call(context, date); - } - - return Jiffy.parseFromDateTime(date).jm; - }, - ), - ]; - - final showThreadTail = - (showThreadReplyIndicator || showInChannel) && !isOnlyEmoji; - - final threadIndicatorWidgets = [ - if (showThreadTail) - // Added builder to use the nearest context to get the right - // textScaleFactor value. - Builder( - builder: (context) { - return Padding( - padding: EdgeInsets.only( - bottom: context.textScaleFactor * - ((messageTheme.repliesStyle?.fontSize ?? 1) / 2), - ), - child: CustomPaint( - size: const Size(16, 32) * context.textScaleFactor, - painter: ThreadReplyPainter( - context: context, - color: messageTheme.messageBorderColor, - reverse: reverse, - ), - ), - ); - }, - ), - if (showInChannel || showThreadReplyIndicator) ...[ - if (showThreadParticipants) - SizedBox.fromSize( - size: Size((threadParticipants!.length * 8.0) + 8, 16), - child: ThreadParticipants( - threadParticipants: threadParticipants, - streamChatTheme: streamChatTheme, - ), - ), - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: _onThreadTap, - child: Text(msg, style: messageTheme.repliesStyle), - ), - ), - ], - ]; - - if (reverse) { - children.addAll(threadIndicatorWidgets.reversed); - } else { - children.insertAll(0, threadIndicatorWidgets); - } - - return Text.rich( - TextSpan( - children: [ - ...children.insertBetween(const SizedBox(width: 8)).map((child) { - final mediaQueryData = MediaQuery.of(context); - return WidgetSpan( - child: MediaQuery( - // Hardcoding the textScaleFactor to 1 to avoid the multiple - // resizing of the text. This is needed because the - // textScaleFactor is already applied to the textSpan. - // - // issue: https://github.com/GetStream/stream-chat-flutter/issues/1250 - // ignore: deprecated_member_use - data: mediaQueryData.copyWith(textScaleFactor: 1), - child: child, - ), - ); - }), - ], - ), - maxLines: 1, - textAlign: reverse ? TextAlign.right : TextAlign.left, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_annotations.dart b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_annotations.dart new file mode 100644 index 0000000000..8528f99646 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_annotations.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat_flutter/src/stream_chat.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +/// Displays contextual annotations for the given [message]. +/// +/// Annotations are shown in the following order when applicable: +/// +/// 1. **Saved for later** — when a reminder exists without a scheduled time. +/// 2. **Pinned** — when [Message.pinned] is true, showing who pinned it. +/// 3. **Show in channel / Replied to thread** — when [Message.showInChannel] +/// is true. The label adapts based on whether the message list is a +/// channel or thread view, and includes a tappable "View" link that +/// invokes [onViewChannelTap]. +/// 4. **Reminder** — when a reminder exists with a scheduled time. +/// +/// Returns `null` when no annotations apply, allowing [StreamColumn] to +/// collapse the widget and skip spacing automatically. +/// +/// See also: +/// +/// * [DefaultStreamMessage], which controls annotation visibility. +class StreamMessageAnnotations extends core.NullableStatelessWidget { + /// Creates message annotations for the given [message]. + const StreamMessageAnnotations({ + super.key, + required this.message, + this.onViewChannelTap, + }); + + /// The message whose annotations to display. + final Message message; + + /// Called when the "View" link in the show-in-channel annotation is tapped. + final VoidCallback? onViewChannelTap; + + @override + Widget? nullableBuild(BuildContext context) { + final translations = context.translations; + final icons = context.streamIcons; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + final crossAxisAlignment = core.StreamMessageLayout.crossAxisAlignmentOf(context); + + Widget? savedForLaterAnnotation; + if (message.reminder case final reminder? when reminder.remindAt == null) { + savedForLaterAnnotation = core.StreamMessageAnnotation( + leading: Icon(icons.save20, color: colorScheme.accentPrimary), + label: Text(translations.savedForLaterLabel, style: TextStyle(color: colorScheme.accentPrimary)), + ); + } + + Widget? pinnedAnnotation; + if (message.pinned case true) { + final currentUser = StreamChat.of(context).currentUser!; + pinnedAnnotation = core.StreamMessageAnnotation( + leading: Icon(icons.pin20), + label: Text( + translations.pinnedByUserText( + pinnedBy: message.pinnedBy ?? currentUser, + currentUser: currentUser, + ), + ), + ); + } + + Widget? showInChannelAnnotation; + if (message.showInChannel case true) { + final listKind = core.StreamMessageLayout.listKindOf(context); + final annotationLabel = switch (listKind) { + .channel => '${translations.repliedToThreadAnnotationLabel} · ', + .thread => '${translations.alsoSentInChannelAnnotationLabel} · ', + }; + + showInChannelAnnotation = core.StreamMessageAnnotation( + onTap: onViewChannelTap, + leading: Icon(icons.arrowUpRight20), + label: Text.rich( + TextSpan( + text: annotationLabel, + children: [ + TextSpan( + text: translations.viewLabel, + style: textTheme.metadataDefault.copyWith(color: colorScheme.textLink), + ), + ], + ), + ), + ); + } + + Widget? reminderAnnotation; + if (message.reminder?.remindAt?.toLocal() case final remindAt?) { + reminderAnnotation = core.StreamMessageAnnotation( + leading: Icon(icons.bell20), + label: Text.rich( + TextSpan( + text: '${translations.reminderSetLabel} · ', + children: [ + TextSpan( + text: translations.reminderAtText(Jiffy.parseFromDateTime(remindAt).jm), + style: textTheme.metadataDefault, + ), + ], + ), + ), + ); + } + + final children = [ + ?savedForLaterAnnotation, + ?pinnedAnnotation, + ?showInChannelAnnotation, + ?reminderAnnotation, + ]; + + if (children.isEmpty) return null; + + return core.StreamColumn( + mainAxisSize: .min, + crossAxisAlignment: crossAxisAlignment, + children: children, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_content.dart b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_content.dart new file mode 100644 index 0000000000..6e48cb7bdd --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_content.dart @@ -0,0 +1,239 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:stream_chat_flutter/src/attachment/builder/attachment_widget_builder.dart'; +import 'package:stream_chat_flutter/src/channel/stream_message_preview_text.dart'; +import 'package:stream_chat_flutter/src/message_widget/components/stream_message_deleted.dart'; +import 'package:stream_chat_flutter/src/message_widget/components/stream_message_reactions.dart'; +import 'package:stream_chat_flutter/src/message_widget/components/stream_message_text.dart'; +import 'package:stream_chat_flutter/src/message_widget/parse_attachments.dart'; +import 'package:stream_chat_flutter/src/utils/typedefs.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +/// Composes the main message content including the bubble, attachments, text, +/// and reactions. +/// +/// For deleted messages a [StreamMessageDeleted] placeholder is shown. +/// Otherwise the content displays attachments, message text, and reactions. +/// +/// The [annotation], [metadata], and [replies] slots are passed in from +/// [DefaultStreamMessage] and rendered in the appropriate positions via the +/// core [core.StreamMessageContent] layout. +/// +/// When the message consists of three or fewer emoji-only characters, the +/// bubble background is hidden so the emoji appear at a larger visual size. +/// +/// See also: +/// +/// * [StreamMessageReactions], which renders reactions around the bubble. +/// * [StreamMessageText], which renders the markdown message text. +/// * [DefaultStreamMessage], which hosts this widget. +class StreamMessageContent extends StatefulWidget { + /// Creates a message content widget for the given [message]. + const StreamMessageContent({ + super.key, + required this.message, + this.annotation, + this.errorBadge, + this.metadata, + this.replies, + this.attachmentBuilders, + this.onLinkTap, + this.onMentionTap, + this.onReactionsTap, + this.onQuotedMessageTap, + this.reactionSorting, + this.onShowMessage, + this.onReplyTap, + this.attachmentActionsModalBuilder, + }); + + /// The message to display. + final Message message; + + /// Optional annotation widget displayed above the message content column. + /// + /// Typically a [StreamMessageAnnotations] containing pinned, reminder, + /// or show-in-channel annotations. + final Widget? annotation; + + /// Optional error badge widget overlaid on the message bubble. + /// + /// When non-null, the badge is positioned at the top-end corner of the + /// bubble using a [Stack] with [PositionedDirectional]. + final Widget? errorBadge; + + /// Optional metadata widget displayed below the message content column. + /// + /// Typically a [StreamMessageMetadata] containing the author name, timestamp, + /// and sending status. + final Widget? metadata; + + /// Optional replies indicator widget displayed below the bubble. + /// + /// Typically a [core.StreamMessageReplies] showing reply count and + /// participant avatars. + final Widget? replies; + + /// Custom attachment builders for rendering message attachments. + /// + /// When non-null, these builders are passed to [ParseAttachments] and + /// take priority over the default builders. + final List? attachmentBuilders; + + /// Called when a link is tapped in the rendered message text. + /// + /// If null, tapping a link has no effect. + final MarkdownTapLinkCallback? onLinkTap; + + /// Called when a `@mention` is tapped in the rendered message text. + /// + /// If null, tapping a mention has no effect. + final core.MarkdownTapMentionCallback? onMentionTap; + + /// Called when the reactions area is tapped. + /// + /// If null, tapping reactions has no effect. + final VoidCallback? onReactionsTap; + + /// Called when the quoted message is tapped. + /// + /// If null, tapping the quoted message has no effect. + final void Function(Message quotedMessage)? onQuotedMessageTap; + + /// Controls how reaction groups are sorted when displayed. + /// + /// Passed through to [StreamMessageReactions.sorting]. + final Comparator? reactionSorting; + + /// Called when the "show in chat" action is tapped in the full-screen + /// media gallery. + final ShowMessageCallback? onShowMessage; + + /// Called when the reply action is tapped in the full-screen media gallery. + final void Function(Message)? onReplyTap; + + /// Widget builder for the attachment actions modal in the full-screen + /// media gallery. + final AttachmentActionsBuilder? attachmentActionsModalBuilder; + + @override + State createState() => _StreamMessageContentState(); +} + +class _StreamMessageContentState extends State { + // Tracks the rendered width of the attachments to constrain the bubble. + double? widthLimit; + late final attachmentsKey = GlobalKey(debugLabel: 'ParseAttachments'); + + // Measures the attachment width after layout and constrains the bubble. + void _updateWidthLimit() { + final attachmentContext = attachmentsKey.currentContext; + final renderBox = attachmentContext?.findRenderObject() as RenderBox?; + final attachmentsWidth = renderBox?.size.width; + + if (attachmentsWidth == null || attachmentsWidth == 0) return; + if (mounted) setState(() => widthLimit = attachmentsWidth); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + WidgetsBinding.instance.addPostFrameCallback((_) => _updateWidthLimit()); + } + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + final crossAxisAlignment = core.StreamMessageLayout.crossAxisAlignmentOf(context); + + if (widget.message.isDeleted) return const StreamMessageDeleted(); + + return core.StreamMessageContent( + header: widget.annotation, + footer: widget.metadata, + child: core.StreamColumn( + mainAxisSize: .min, + crossAxisAlignment: crossAxisAlignment, + children: [ + StreamMessageReactions( + message: widget.message, + sorting: widget.reactionSorting, + onPressed: widget.onReactionsTap, + child: Builder( + builder: (context) { + final bubbleContent = ConstrainedBox( + constraints: const BoxConstraints().copyWith(maxWidth: widthLimit), + child: core.StreamColumn( + spacing: spacing.xxs, + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ + if (widget.message.quotedMessage case final quotedMessage?) + // TODO: Refactor this with attachments + GestureDetector( + onTap: !quotedMessage.isDeleted && widget.onQuotedMessageTap != null + ? () => widget.onQuotedMessageTap!(quotedMessage) + : null, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: core.StreamMessageTheme( + data: core.StreamMessageThemeData( + incoming: core.StreamMessageStyle( + backgroundColor: context.streamColorScheme.backgroundSurfaceStrong, + ), + outgoing: core.StreamMessageStyle( + backgroundColor: context.streamColorScheme.brand.shade150, + ), + ), + child: core.MessageComposerReplyAttachment( + title: Text(quotedMessage.user?.name ?? ''), + subtitle: StreamMessagePreviewText(message: quotedMessage), + style: switch (core.StreamMessageLayout.messageAlignmentOf(context)) { + core.StreamMessageAlignment.start => .incoming, + core.StreamMessageAlignment.end => .outgoing, + }, + ), + ), + ), + ), + ParseAttachments( + key: attachmentsKey, + message: widget.message, + attachmentBuilders: widget.attachmentBuilders, + onShowMessage: widget.onShowMessage, + onReplyTap: widget.onReplyTap, + attachmentActionsModalBuilder: widget.attachmentActionsModalBuilder, + ), + if (widget.message.text case final text? when text.isNotEmpty) + StreamMessageText( + message: widget.message, + onLinkTap: widget.onLinkTap, + onMentionTap: widget.onMentionTap, + ), + ], + ), + ); + + final bubble = core.StreamMessageBubble(child: bubbleContent); + + if (widget.errorBadge case final errorBadge?) { + return Stack( + clipBehavior: .none, + children: [ + bubble, + PositionedDirectional(top: 8, end: -12, child: errorBadge), + ], + ); + } + + return bubble; + }, + ), + ), + if (widget.replies case final replies?) replies, + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_deleted.dart b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_deleted.dart new file mode 100644 index 0000000000..e810856a79 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_deleted.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +/// Displays a "Message deleted" indicator inside a message bubble. +/// +/// Shown in place of the normal message content when [Message.isDeleted] +/// is true. +/// +/// See also: +/// +/// * [StreamMessageScaffold], which shows this widget for deleted messages. +class StreamMessageDeleted extends StatelessWidget { + /// Creates a deleted message widget. + const StreamMessageDeleted({super.key}); + + @override + Widget build(BuildContext context) { + final icons = context.streamIcons; + final spacing = context.streamSpacing; + + return core.StreamMessageBubble( + padding: .symmetric( + horizontal: spacing.sm, + vertical: spacing.xs, + ), + child: Row( + spacing: spacing.xxs, + mainAxisSize: .min, + children: [ + Icon(icons.noSign16, size: 16), + core.StreamMessageText(padding: .zero, context.translations.messageDeletedLabel), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_metadata.dart b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_metadata.dart new file mode 100644 index 0000000000..916cd81235 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_metadata.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat_flutter/src/message_widget/components/stream_message_sending_status.dart'; +import 'package:stream_chat_flutter/src/misc/timestamp.dart'; +import 'package:stream_chat_flutter/src/stream_chat.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +/// Displays the message metadata containing the author name, sending status, +/// creation timestamp, and an edited indicator. +/// +/// The metadata can show up to four pieces depending on the message: +/// +/// * **Username** — for messages from other users. +/// * **Sending status** — for the current user's own messages. +/// * **Timestamp** — always shown, formatted as a short time string. +/// * **Edited label** — when the message text has been updated. +/// +/// See also: +/// +/// * [StreamMessageSendingStatus], which renders the sent/delivered/read +/// indicator. +/// * [DefaultStreamMessage], which controls metadata visibility. +class StreamMessageMetadata extends StatelessWidget { + /// Creates message metadata for the given [message]. + const StreamMessageMetadata({super.key, required this.message}); + + /// The message whose metadata to display. + final Message message; + + @override + Widget build(BuildContext context) { + final currentUser = StreamChat.of(context).currentUser; + final channelKind = core.StreamMessageLayout.channelKindOf(context); + + Widget? usernameWidget; + if (message.user case final user? when channelKind == .group && user.id != currentUser?.id) { + usernameWidget = Text(user.name, maxLines: 1, overflow: .ellipsis); + } + + Widget? statusWidget; + if (message.user case final user? when user.id == currentUser?.id) { + statusWidget = StreamMessageSendingStatus(message: message); + } + + final Widget timestampWidget; + if (message.createdAt case final createdAt) { + timestampWidget = StreamTimestamp( + date: createdAt.toLocal(), + formatter: (context, date) { + return Jiffy.parseFromDateTime(date).jm; + }, + ); + } + + Widget? editedWidget; + if (message.messageTextUpdatedAt != null) { + editedWidget = Text(context.translations.editedMessageLabel); + } + + return core.StreamMessageMetadata( + username: usernameWidget, + status: statusWidget, + timestamp: timestampWidget, + edited: editedWidget, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_reactions.dart b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_reactions.dart new file mode 100644 index 0000000000..ee8a8c78e6 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_reactions.dart @@ -0,0 +1,88 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_chat_configuration.dart'; +import 'package:stream_chat_flutter/src/utils/device_segmentation.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +/// Displays reaction groups for a message as emoji chips overlaid on, or +/// placed beneath, the [child] widget. +/// +/// Reaction icons are resolved through the +/// [StreamChatConfigurationData.reactionIconResolver]. Groups are sorted +/// using [sorting] (defaults to [ReactionSorting.byFirstReactionAt]). +/// +/// See also: +/// +/// * [StreamMessageScaffold], which hosts this widget around the bubble. +/// * [StreamChatConfigurationData.reactionIconResolver], which maps reaction +/// type strings to emoji content models. +class StreamMessageReactions extends StatelessWidget { + /// Creates a message reactions widget for the given [message]. + const StreamMessageReactions({ + super.key, + required this.message, + this.type, + this.position, + this.sorting, + this.onPressed, + this.child, + }); + + /// The message whose reactions to display. + final Message message; + + /// The visual type of the reactions display. + /// + /// Defaults to [core.StreamReactionsType.segmented] when null. + final core.StreamReactionsType? type; + + /// Where the reactions appear relative to the message bubble. + /// + /// Defaults to [core.StreamReactionsPosition.footer] on desktop and web, + /// and [core.StreamReactionsPosition.header] on mobile. + final core.StreamReactionsPosition? position; + + /// Controls how reaction groups are sorted when displayed. + /// + /// Defaults to [ReactionSorting.byFirstReactionAt] when null. + final Comparator? sorting; + + /// Called when the reactions area is pressed. + /// + /// If null, pressing the reactions area has no effect. + final VoidCallback? onPressed; + + /// The child widget (typically the message bubble) that reactions are + /// displayed on. + final Widget? child; + + @override + Widget build(BuildContext context) { + final config = StreamChatConfiguration.of(context); + final resolver = config.reactionIconResolver; + + final effectiveType = type ?? config.reactionType ?? core.StreamReactionsType.segmented; + final effectivePosition = position ?? config.reactionPosition ?? core.StreamReactionsPosition.header; + + final reactionGroups = message.reactionGroups?.entries; + final effectiveReactionSorting = sorting ?? ReactionSorting.byFirstReactionAt; + final sortedReactionGroups = reactionGroups?.sortedByCompare((it) => it.value, effectiveReactionSorting); + + final items = sortedReactionGroups?.map( + (group) => core.StreamReactionsItem( + count: group.value.count, + emoji: resolver.resolve(group.key), + ), + ); + + return core.StreamReactions( + type: effectiveType, + position: effectivePosition, + overlap: !isDesktopDeviceOrWeb, + onPressed: onPressed, + items: [...?items], + child: child, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_sending_status.dart b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_sending_status.dart new file mode 100644 index 0000000000..cbee70e320 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_sending_status.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/indicators/sending_indicator.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Displays the sending status of a message, including attachment upload +/// progress and sent/delivered/read indicators. +/// +/// While attachments are still uploading, a textual progress label is shown. +/// Once the message is fully sent, an icon indicates whether it has been +/// sent, delivered, or read. +/// +/// This widget is typically used inside [StreamMessageMetadata] and is only +/// shown for messages sent by the current user. +/// +/// See also: +/// +/// * [StreamSendingIndicator], which renders the sent/delivered/read icon. +/// * [StreamMessageMetadata], which hosts this widget. +class StreamMessageSendingStatus extends StatelessWidget { + /// Creates a sending status widget for the given [message]. + const StreamMessageSendingStatus({ + super.key, + required this.message, + }); + + /// The message whose sending status to display. + final Message message; + + @override + Widget build(BuildContext context) { + final hasNonUrlAttachments = message.attachments.any((it) => it.type != AttachmentType.urlPreview); + + if (hasNonUrlAttachments && message.state.isOutgoing) { + final totalAttachments = message.attachments.length; + final attachmentsToUpload = message.attachments.where((it) => !it.uploadState.isSuccess); + + if (attachmentsToUpload.isNotEmpty) { + return Text( + context.translations.attachmentsUploadProgressText( + remaining: attachmentsToUpload.length, + total: totalAttachments, + ), + ); + } + } + + final channel = StreamChannel.maybeOf(context)?.channel; + + return BetterStreamBuilder>( + stream: channel?.state?.readStream, + initialData: channel?.state?.read, + builder: (context, data) { + final readList = data.readsOf(message: message); + final isMessageRead = readList.isNotEmpty; + + final deliveriesList = data.deliveriesOf(message: message); + final isMessageDelivered = deliveriesList.isNotEmpty; + + return StreamSendingIndicator( + message: message, + isMessageRead: isMessageRead, + isMessageDelivered: isMessageDelivered, + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_text.dart b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_text.dart new file mode 100644 index 0000000000..f273f32ea2 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_widget/components/stream_message_text.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; +import 'package:stream_chat_flutter/src/stream_chat.dart'; +import 'package:stream_chat_flutter/src/utils/device_segmentation.dart'; +import 'package:stream_chat_flutter/src/utils/extensions.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +/// Displays the translated markdown message text, reacting to the current +/// user's language preference. +/// +/// The message text is translated into the current user's language, mention +/// syntax is replaced with display names, and the result is rendered as +/// markdown. +/// +/// The widget rebuilds automatically when the current user's language +/// changes, ensuring the displayed text stays in sync. +/// +/// On desktop and web the text is selectable; on mobile it is not. +/// +/// See also: +/// +/// * [StreamMessageScaffold], which hosts this widget inside a message bubble. +class StreamMessageText extends StatelessWidget { + /// Creates a message text widget for the given [message]. + const StreamMessageText({ + super.key, + required this.message, + this.onLinkTap, + this.onMentionTap, + }); + + /// The message whose text to display. + final Message message; + + /// Called when a link in the rendered markdown is tapped. + /// + /// If null, tapping a link has no effect. + final MarkdownTapLinkCallback? onLinkTap; + + /// Called when a `@mention` in the rendered markdown is tapped. + /// + /// Mentions use the `[text](mention:id)` format in the raw markdown. + /// If null, tapping a mention has no effect. + final core.MarkdownTapMentionCallback? onMentionTap; + + @override + Widget build(BuildContext context) { + final streamChat = StreamChat.of(context); + + return BetterStreamBuilder( + initialData: streamChat.currentUser?.language ?? 'en', + stream: streamChat.currentUserStream.map((it) => it?.language ?? 'en'), + builder: (context, language) { + final messageText = message.translate(language).replaceMentions().text?.replaceAll('\n', '\n\n').trim(); + + if (messageText == null || messageText.trim().isEmpty) return const Empty(); + + return core.StreamMessageText( + messageText, + selectable: isDesktopDeviceOrWeb, + onTapLink: onLinkTap, + onTapMention: onMentionTap, + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/deleted_message.dart b/packages/stream_chat_flutter/lib/src/message_widget/deleted_message.dart deleted file mode 100644 index 10004802fa..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/deleted_message.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template streamDeletedMessage} -/// Displays that a message was deleted at this position in the message list. -/// {@endtemplate} -class StreamDeletedMessage extends StatelessWidget { - /// {@macro streamDeletedMessage} - const StreamDeletedMessage({ - super.key, - required this.messageTheme, - this.borderRadiusGeometry, - this.shape, - this.borderSide, - this.reverse = false, - }); - - /// The theme of the message - final StreamMessageThemeData messageTheme; - - /// The border radius of the message text - final BorderRadiusGeometry? borderRadiusGeometry; - - /// The shape of the message text - final ShapeBorder? shape; - - /// The [BorderSide] of the message text - final BorderSide? borderSide; - - /// If true the widget will be mirrored - final bool reverse; - - @override - Widget build(BuildContext context) { - return Material( - color: messageTheme.messageBackgroundColor, - shape: shape ?? - RoundedRectangleBorder( - borderRadius: borderRadiusGeometry ?? BorderRadius.zero, - side: borderSide ?? - BorderSide( - color: messageTheme.messageBorderColor ?? Colors.transparent, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, - ), - child: Text( - context.translations.messageDeletedLabel, - style: messageTheme.messageDeletedStyle, - ), - ), - ); - } -} 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/giphy_ephemeral_message.dart b/packages/stream_chat_flutter/lib/src/message_widget/giphy_ephemeral_message.dart index dea2cc8e2c..8bbd977ad1 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/giphy_ephemeral_message.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/giphy_ephemeral_message.dart @@ -1,18 +1,18 @@ -import 'package:flutter/material.dart'; -import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat_flutter/src/attachment/thumbnail/giphy_attachment_thumbnail.dart'; -import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/misc/timestamp.dart'; -import 'package:stream_chat_flutter/src/misc/visible_footnote.dart'; -import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:flutter/material.dart' hide Action; +import 'package:stream_chat_flutter/src/attachment/giphy_attachment.dart'; +import 'package:stream_chat_flutter/src/message_widget/components/stream_message_metadata.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; /// Signature for the action callback passed to [GiphyEphemeralMessage]. /// /// Used by [GiphyEphemeralMessage.onActionPressed]. typedef GiffyAction = void Function(String name, String value); +const _kDefaultConstraints = BoxConstraints(maxWidth: 256); +const _kDefaultGiphyConstraints = BoxConstraints(minWidth: 128, maxWidth: 256, maxHeight: 256); + /// {@template giphyEphemeralMessage} /// Shows an ephemeral message of type giphy in a [MessageWidget]. /// {@endtemplate} @@ -21,12 +21,16 @@ class GiphyEphemeralMessage extends StatelessWidget { const GiphyEphemeralMessage({ super.key, required this.message, + this.constraints, this.onActionPressed, }); /// The underlying [Message] object which this widget represents. final Message message; + /// The constraints to apply to the overall widget layout. + final BoxConstraints? constraints; + /// Callback called when an action is pressed. final GiffyAction? onActionPressed; @@ -37,84 +41,42 @@ class GiphyEphemeralMessage extends StatelessWidget { final actions = giphy.actions; assert(actions != null && actions.isNotEmpty, 'actions cannot be null'); - final chatTheme = StreamChatTheme.of(context); - final textTheme = chatTheme.textTheme; - final colorTheme = chatTheme.colorTheme; + final spacing = context.streamSpacing; - final divider = Divider(thickness: 1, height: 0, color: colorTheme.borders); + final effectiveConstraints = constraints ?? _kDefaultConstraints; - return Padding( - padding: const EdgeInsets.all(8), - child: Align( - alignment: Alignment.centerRight, - child: SizedBox( - width: 304, - height: 343, - child: Column( - children: [ - Expanded( - child: Card( - elevation: 2, - color: colorTheme.barsBg, - margin: EdgeInsets.zero, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(16), - topLeft: Radius.circular(16), - bottomLeft: Radius.circular(16), - ), - ), + return core.StreamMessageLayout( + data: const core.StreamMessageLayoutData( + alignment: .end, + stackPosition: .single, + contentKind: .singleAttachment, + ), + child: Builder( + builder: (context) => Align( + alignment: core.StreamMessageLayout.alignmentDirectionalOf(context), + child: Padding( + padding: .symmetric(horizontal: spacing.md), + child: ConstrainedBox( + constraints: effectiveConstraints, + child: core.StreamMessageContent( + footer: StreamMessageMetadata(message: message), + child: core.StreamMessageBubble( child: Column( + mainAxisSize: .min, + crossAxisAlignment: .stretch, children: [ - Padding( - padding: const EdgeInsets.all(8), - child: GiphyHeader(title: giphy.title), - ), - divider, - Expanded( - child: Padding( - padding: const EdgeInsets.all(2), - child: ClipRRect( - borderRadius: BorderRadius.circular(2), - child: StreamGiphyAttachmentThumbnail( - giphy: giphy, - width: double.infinity, - height: double.infinity, - ), - ), - ), - ), - divider, - SizedBox( - height: 48, - child: Padding( - padding: const EdgeInsets.all(2), - child: GiphyActions( - giphy: giphy, - onActionPressed: onActionPressed, - ), - ), + GiphyHeader(title: context.translations.onlyVisibleToYouText), + StreamGiphyAttachment( + message: message, + giphy: giphy, + constraints: _kDefaultGiphyConstraints, ), + GiphyActions(actions: actions!, onActionPressed: onActionPressed), ], ), ), ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const StreamVisibleFootnote(), - const SizedBox(width: 8), - StreamTimestamp( - date: message.createdAt.toLocal(), - formatter: (_, date) => Jiffy.parseFromDateTime(date).jm, - style: textTheme.footnote.copyWith( - color: colorTheme.textLowEmphasis, - ), - ), - ], - ), - ], + ), ), ), ), @@ -129,68 +91,51 @@ class GiphyActions extends StatelessWidget { /// {@macro giphyActions} const GiphyActions({ super.key, - required this.giphy, + required this.actions, required this.onActionPressed, }); /// The underlying [Attachment] object which this widget represents. - final Attachment giphy; + final List actions; /// Callback called when an action is pressed. final GiffyAction? onActionPressed; @override Widget build(BuildContext context) { - final theme = StreamChatTheme.of(context); - final textTheme = theme.textTheme; - final colorTheme = theme.colorTheme; - - return Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: TextButton( - onPressed: switch (onActionPressed) { - final onPressed? => () => onPressed('image_action', 'cancel'), - _ => null, - }, - style: TextButton.styleFrom( - textStyle: textTheme.bodyBold, - foregroundColor: colorTheme.textLowEmphasis, - ), - child: Text(context.translations.cancelLabel.capitalize()), - ), - ), - VerticalDivider(thickness: 1, width: 4, color: colorTheme.borders), - Expanded( - child: TextButton( - onPressed: switch (onActionPressed) { - final onPressed? => () => onPressed('image_action', 'shuffle'), - _ => null, - }, - style: TextButton.styleFrom( - textStyle: textTheme.bodyBold, - foregroundColor: colorTheme.textLowEmphasis, - ), - child: Text(context.translations.shuffleLabel.capitalize()), - ), - ), - VerticalDivider(thickness: 1, width: 4, color: colorTheme.borders), - Expanded( - child: TextButton( - onPressed: switch (onActionPressed) { - final onPressed? => () => onPressed('image_action', 'send'), - _ => null, + final spacing = context.streamSpacing; + + return Padding( + padding: .symmetric(horizontal: spacing.xxs), + child: Row( + mainAxisSize: .min, + spacing: spacing.xs, + mainAxisAlignment: .spaceEvenly, + children: [ + ...actions.map( + (action) { + final style = switch (action.style) { + 'primary' => core.StreamButtonStyle.primary, + _ => core.StreamButtonStyle.secondary, + }; + + return core.StreamButton( + label: action.text, + style: style, + type: .ghost, + size: .small, + onTap: switch (onActionPressed) { + final onPressed? => () => onPressed( + action.name.toLowerCase(), + action.text.toLowerCase(), + ), + _ => null, + }, + ); }, - style: TextButton.styleFrom( - textStyle: textTheme.bodyBold, - foregroundColor: colorTheme.accentPrimary, - ), - child: Text(context.translations.sendLabel.capitalize()), ), - ), - ], + ], + ), ); } } @@ -200,36 +145,31 @@ class GiphyActions extends StatelessWidget { /// {@endtemplate} class GiphyHeader extends StatelessWidget { /// {@macro giphyHeader} - const GiphyHeader({super.key, this.title}); + const GiphyHeader({super.key, required this.title}); /// The title of the giphy. - final String? title; + final String title; @override Widget build(BuildContext context) { - final colorTheme = StreamChatTheme.of(context).colorTheme; - return Row( - children: [ - const StreamSvgIcon(icon: StreamSvgIcons.giphy), - const SizedBox(width: 8), - Text( - context.translations.giphyLabel, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(width: 8), - if (title != null) - Expanded( - child: Text( - title!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - // ignore: deprecated_member_use - color: colorTheme.textHighEmphasis.withOpacity(0.5), - ), - ), - ), - ], + final icons = context.streamIcons; + final spacing = context.streamSpacing; + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + + return Padding( + padding: EdgeInsets.symmetric( + vertical: spacing.xs, + horizontal: spacing.sm, + ), + child: Row( + mainAxisSize: .min, + spacing: spacing.xs, + children: [ + Icon(icons.eyeFill16, size: 16, color: colorScheme.brand.shade900), + Text(title, style: textTheme.captionEmphasis.copyWith(color: colorScheme.brand.shade900)), + ], + ), ); } } 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 deleted file mode 100644 index 5858becd78..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_card.dart +++ /dev/null @@ -1,271 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template messageCard} -/// The widget containing a quoted message. -/// -/// Used in [MessageWidgetContent]. Should not be used elsewhere. -/// {@endtemplate} -class MessageCard extends StatefulWidget { - /// {@macro messageCard} - const MessageCard({ - super.key, - required this.message, - required this.isFailedState, - required this.showUserAvatar, - required this.messageTheme, - required this.hasQuotedMessage, - required this.hasUrlAttachments, - required this.hasNonUrlAttachments, - required this.hasPoll, - required this.isOnlyEmoji, - required this.isGiphy, - required this.attachmentBuilders, - required this.attachmentPadding, - required this.attachmentShape, - required this.onAttachmentTap, - required this.onShowMessage, - required this.onReplyTap, - required this.attachmentActionsModalBuilder, - required this.textPadding, - required this.reverse, - this.shape, - this.borderSide, - this.borderRadiusGeometry, - this.textBuilder, - this.quotedMessageBuilder, - this.onLinkTap, - this.onMentionTap, - this.onQuotedMessageTap, - }); - - /// {@macro isFailedState} - final bool isFailedState; - - /// {@macro showUserAvatar} - final DisplayWidget showUserAvatar; - - /// {@macro shape} - final ShapeBorder? shape; - - /// {@macro borderSide} - final BorderSide? borderSide; - - /// {@macro messageTheme} - final StreamMessageThemeData messageTheme; - - /// {@macro borderRadiusGeometry} - final BorderRadiusGeometry? borderRadiusGeometry; - - /// {@macro hasQuotedMessage} - final bool hasQuotedMessage; - - /// {@macro hasUrlAttachments} - final bool hasUrlAttachments; - - /// {@macro hasNonUrlAttachments} - final bool hasNonUrlAttachments; - - /// {@macro hasPoll} - final bool hasPoll; - - /// {@macro isOnlyEmoji} - final bool isOnlyEmoji; - - /// {@macro isGiphy} - final bool isGiphy; - - /// {@macro message} - final Message message; - - /// {@macro attachmentBuilders} - final List? attachmentBuilders; - - /// {@macro attachmentPadding} - final EdgeInsetsGeometry attachmentPadding; - - /// {@macro attachmentShape} - final ShapeBorder? attachmentShape; - - /// {@macro onAttachmentTap} - final StreamAttachmentWidgetTapCallback? onAttachmentTap; - - /// {@macro onShowMessage} - final ShowMessageCallback? onShowMessage; - - /// {@macro onReplyTap} - final void Function(Message)? onReplyTap; - - /// {@macro attachmentActionsBuilder} - final AttachmentActionsBuilder? attachmentActionsModalBuilder; - - /// {@macro textPadding} - final EdgeInsets textPadding; - - /// {@macro textBuilder} - final Widget Function(BuildContext, Message)? textBuilder; - - /// {@macro quotedMessageBuilder} - final Widget Function(BuildContext, Message)? quotedMessageBuilder; - - /// {@macro onLinkTap} - final void Function(String)? onLinkTap; - - /// {@macro onMentionTap} - final void Function(User)? onMentionTap; - - /// {@macro onQuotedMessageTap} - final OnQuotedMessageTap? onQuotedMessageTap; - - /// {@macro reverse} - final bool reverse; - - @override - State createState() => _MessageCardState(); -} - -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?; - final attachmentsWidth = renderBox?.size.width; - - if (attachmentsWidth == null || attachmentsWidth == 0) return; - - if (mounted) { - setState(() => widthLimit = attachmentsWidth); - } - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - // 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(); - }); - } - } - - @override - Widget build(BuildContext context) { - final onQuotedMessageTap = widget.onQuotedMessageTap; - final quotedMessageBuilder = widget.quotedMessageBuilder; - - 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), - ), - clipBehavior: Clip.hardEdge, - decoration: _buildDecoration(widget.messageTheme), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.hasQuotedMessage) - InkWell( - onTap: !widget.message.quotedMessage!.isDeleted && - onQuotedMessageTap != null - ? () => onQuotedMessageTap(widget.message.quotedMessageId) - : null, - child: quotedMessageBuilder?.call( - context, - widget.message.quotedMessage!, - ) ?? - QuotedMessage( - message: widget.message, - textBuilder: widget.textBuilder, - 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, - ), - TextBubble( - messageTheme: widget.messageTheme, - message: widget.message, - textPadding: widget.textPadding, - textBuilder: widget.textBuilder, - isOnlyEmoji: widget.isOnlyEmoji, - hasQuotedMessage: widget.hasQuotedMessage, - hasUrlAttachments: widget.hasUrlAttachments, - onLinkTap: widget.onLinkTap, - onMentionTap: widget.onMentionTap, - ), - ], - ), - ); - } - - ShapeDecoration _buildDecoration(StreamMessageThemeData theme) { - final gradient = _getBackgroundGradient(theme); - final color = gradient == null ? _getBackgroundColor(theme) : null; - - final borderColor = theme.messageBorderColor ?? Colors.transparent; - final borderRadius = widget.borderRadiusGeometry ?? BorderRadius.zero; - - return ShapeDecoration( - color: color, - gradient: gradient, - shape: switch (widget.shape) { - final shape? => shape, - _ => RoundedRectangleBorder( - borderRadius: borderRadius, - side: switch (widget.borderSide) { - final side? => side, - _ => BorderSide(color: borderColor), - }, - ), - }, - ); - } - - Color? _getBackgroundColor(StreamMessageThemeData theme) { - if (widget.hasQuotedMessage) { - return theme.messageBackgroundColor; - } - - final containsOnlyUrlAttachment = - widget.hasUrlAttachments && !widget.hasNonUrlAttachments; - - if (containsOnlyUrlAttachment) { - return theme.urlAttachmentBackgroundColor; - } - - if (widget.isOnlyEmoji) return null; - - return theme.messageBackgroundColor; - } - - Gradient? _getBackgroundGradient(StreamMessageThemeData theme) { - if (widget.isOnlyEmoji) return null; - - return theme.messageBackgroundGradient; - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_text.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_text.dart deleted file mode 100644 index e46e4fc420..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_text.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:collection/collection.dart' show IterableExtension; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template streamMessageText} -/// The text content of a message. -/// {@endtemplate} -class StreamMessageText extends StatelessWidget { - /// {@macro streamMessageText} - const StreamMessageText({ - super.key, - required this.message, - required this.messageTheme, - this.onMentionTap, - this.onLinkTap, - }); - - /// Message whose text is to be displayed - final Message message; - - /// The action to perform when a mention is tapped - final void Function(User)? onMentionTap; - - /// The action to perform when a link is tapped - final void Function(String)? onLinkTap; - - /// [StreamMessageThemeData] whose text theme is to be applied - final StreamMessageThemeData messageTheme; - - @override - Widget build(BuildContext context) { - final streamChat = StreamChat.of(context); - assert(streamChat.currentUser != null, ''); - return BetterStreamBuilder( - stream: streamChat.currentUserStream.map((it) => it!.language ?? 'en'), - initialData: streamChat.currentUser!.language ?? 'en', - builder: (context, language) { - final messageText = message - .translate(language) - .replaceMentions() - .text - ?.replaceAll('\n', '\n\n') - .trim(); - - return StreamMarkdownMessage( - data: messageText ?? '', - messageTheme: messageTheme, - selectable: isDesktopDeviceOrWeb, - onTapLink: ( - String text, - String? href, - String title, - ) { - if (text.startsWith('@')) { - final mentionedUser = message.mentionedUsers.firstWhereOrNull( - (u) => '@${u.name}' == text, - ); - - if (mentionedUser == null) return; - - onMentionTap?.call(mentionedUser); - } else if (href != null) { - if (onLinkTap != null) { - onLinkTap!(href); - } else { - launchURL(context, href); - } - } - }, - ); - }, - ); - } -} 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..e7671c9839 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,1249 +1,1116 @@ -import 'package:flutter/material.dart' hide ButtonStyle; +import 'dart:math' as math; +import 'dart:ui' show lerpDouble; + +import 'package:collection/collection.dart'; +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/platform_widget_builder/src/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/message_widget/components/stream_message_annotations.dart'; +import 'package:stream_chat_flutter/src/message_widget/components/stream_message_content.dart'; +import 'package:stream_chat_flutter/src/message_widget/components/stream_message_metadata.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; -/// The display behaviour of a widget -enum DisplayWidget { - /// Hides the widget replacing its space with a spacer - hide, - - /// Hides the widget not replacing its space - gone, +/// A chat message widget that renders a single message with its attachments, +/// reactions, and interaction callbacks. +/// +/// [StreamMessageWidget] displays a single [Message] within a chat message +/// list. It handles the complete message layout including the author avatar, +/// message content (text, attachments, polls, quoted messages), reactions, +/// thread indicators, and user interaction gestures such as tap, long-press, +/// and context menus. +/// +/// On mobile platforms, a long-press opens the [StreamMessageActionsModal] +/// with available actions (reply, edit, delete, pin, etc.). On desktop and +/// web, those same actions appear in a right-click context menu. +/// +/// This widget delegates rendering to either a custom builder registered via +/// [StreamComponentFactory], or [DefaultStreamMessage] when no custom builder +/// is provided. Register a custom builder through [StreamChatConfigurationData] +/// to fully replace the default message layout while still receiving the same +/// [StreamMessageWidgetProps]. +/// +/// {@tool snippet} +/// +/// Display a message with default settings: +/// +/// ```dart +/// StreamMessageWidget( +/// message: message, +/// ) +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// +/// Customise interaction callbacks: +/// +/// ```dart +/// StreamMessageWidget( +/// message: message, +/// onMessageTap: (msg) => print('Tapped: ${msg.id}'), +/// onThreadTap: (parent, threadMsg) => Navigator.push(...), +/// onUserAvatarTap: (user) => showProfile(user), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamMessageWidgetProps], which holds every configurable property. +/// * [DefaultStreamMessage], the default implementation used when no custom +/// builder is registered. +/// * [StreamMessageActionsModal], the modal shown on long-press (mobile). +/// * [StreamMessageListView], which hosts a scrollable list of these widgets. +class StreamMessageWidget extends StatelessWidget { + /// Creates a chat message widget. + /// + /// The [message] is required. All other parameters are optional and have + /// sensible defaults resolved from the ambient theme and message data. + StreamMessageWidget({ + super.key, + required Message message, + EdgeInsetsGeometry? padding, + double? spacing, + Color? backgroundColor, + double maxWidth = 264, + bool swipeToReply = false, + void Function(Message)? onMessageTap, + void Function(Message)? onMessageLongPress, + void Function(User)? onUserAvatarTap, + void Function(Message message, String url)? onMessageLinkTap, + void Function(User user)? onUserMentionTap, + void Function(Message parentMessage, Message? threadMessage)? onThreadTap, + void Function(Message)? onViewInChannelTap, + void Function(Message)? onReplyTap, + void Function(Message)? onReactionsTap, + void Function(Message quotedMessage)? onQuotedMessageTap, + Comparator? reactionSorting, + MessageActionsBuilder? actionsBuilder, + void Function(BuildContext, Message)? onMessageActions, + void Function(BuildContext, Message)? onBouncedErrorMessageActions, + void Function(Message)? onEditMessageTap, + List? attachmentBuilders, + ShowMessageCallback? onShowMessage, + AttachmentActionsBuilder? attachmentActionsModalBuilder, + }) : props = .new( + message: message, + padding: padding, + spacing: spacing, + backgroundColor: backgroundColor, + maxWidth: maxWidth, + swipeToReply: swipeToReply, + onMessageTap: onMessageTap, + onMessageLongPress: onMessageLongPress, + onUserAvatarTap: onUserAvatarTap, + onMessageLinkTap: onMessageLinkTap, + onUserMentionTap: onUserMentionTap, + onThreadTap: onThreadTap, + onViewInChannelTap: onViewInChannelTap, + onReplyTap: onReplyTap, + onReactionsTap: onReactionsTap, + onQuotedMessageTap: onQuotedMessageTap, + reactionSorting: reactionSorting, + actionsBuilder: actionsBuilder, + onMessageActions: onMessageActions, + onBouncedErrorMessageActions: onBouncedErrorMessageActions, + onEditMessageTap: onEditMessageTap, + attachmentBuilders: attachmentBuilders, + onShowMessage: onShowMessage, + attachmentActionsModalBuilder: attachmentActionsModalBuilder, + ); + + /// Creates a chat message widget from pre-built [props]. + const StreamMessageWidget.fromProps({super.key, required this.props}); + + /// The properties that configure this message widget. + final StreamMessageWidgetProps props; - /// Shows the widget normally - show, + @override + Widget build(BuildContext context) { + final builder = context.chatComponentBuilder(); + if (builder != null) return builder(context, props); + return DefaultStreamMessage(props: props); + } } -/// {@template messageWidget} -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget_paint.png) +/// Properties for configuring a [StreamMessageWidget]. /// -/// Shows a message with reactions, replies and user avatar. +/// This class holds every configuration option for a chat message widget, +/// allowing them to be passed through the [StreamComponentFactory] when a +/// custom builder is registered. /// -/// Usually you don't use this widget as it's the default message widget used by -/// [MessageListView]. +/// Visual properties such as [padding], [spacing], and [backgroundColor] +/// override the corresponding values from [StreamMessageItemThemeData] when +/// non-null. When left null, the theme values are used instead. /// -/// The widget components render the ui based on the first ancestor of type -/// [StreamChatTheme]. -/// Modify it to change the widget appearance. -/// {@endtemplate} -class StreamMessageWidget extends StatefulWidget { - /// {@macro messageWidget} - const StreamMessageWidget({ - super.key, +/// See also: +/// +/// * [StreamMessageWidget], which uses these properties. +/// * [DefaultStreamMessage], the default implementation. +class StreamMessageWidgetProps { + /// Creates properties for a chat message widget. + const StreamMessageWidgetProps({ required this.message, - required this.messageTheme, - this.reverse = false, - this.translateUserAvatar = true, - this.shape, - this.borderSide, - this.borderRadiusGeometry, - this.attachmentShape, - this.onMentionTap, + this.padding, + this.spacing, + this.backgroundColor, + this.maxWidth = 272, + this.swipeToReply = false, this.onMessageTap, this.onMessageLongPress, - this.onReactionsTap, - this.onReactionsHover, - this.showReactionPicker = true, - this.showReactionTail, - this.showUserAvatar = DisplayWidget.show, - this.showSendingIndicator = true, - this.showThreadReplyIndicator = false, - this.showInChannelIndicator = false, - this.onReplyTap, - this.onThreadTap, - this.onConfirmDeleteTap, - this.showUsername = true, - this.showTimestamp = true, - this.showEditedLabel = true, - this.showReactions = true, - this.showDeleteMessage = true, - this.showEditMessage = true, - this.showReplyMessage = true, - this.showThreadReplyMessage = true, - this.showMarkUnreadMessage = true, - this.showResendMessage = true, - this.showCopyMessage = true, - this.showFlagButton = true, - this.showPinButton = true, - this.showPinHighlight = true, this.onUserAvatarTap, - this.onLinkTap, + this.onMessageLinkTap, + this.onUserMentionTap, + this.onThreadTap, + this.onViewInChannelTap, + this.onReplyTap, + this.onReactionsTap, + this.onQuotedMessageTap, + this.reactionSorting, + this.actionsBuilder, this.onMessageActions, this.onBouncedErrorMessageActions, - this.onShowMessage, - this.userAvatarBuilder, - this.quotedMessageBuilder, - this.editMessageInputBuilder, - this.textBuilder, - this.bottomRowBuilderWithDefaultWidget, + this.onEditMessageTap, this.attachmentBuilders, - this.padding, - this.textPadding = const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - this.attachmentPadding = EdgeInsets.zero, - this.widthFactor = 0.78, - this.onQuotedMessageTap, - this.customActions = const [], - this.onAttachmentTap, - this.imageAttachmentThumbnailSize = const Size(400, 400), - this.imageAttachmentThumbnailResizeType = 'clip', - this.imageAttachmentThumbnailCropType = 'center', + this.onShowMessage, this.attachmentActionsModalBuilder, }); - /// {@template onMentionTap} - /// Function called on mention tap - /// {@endtemplate} - final void Function(User)? onMentionTap; - - /// {@template onThreadTap} - /// The function called when tapping on threads - /// {@endtemplate} - final void Function(Message)? onThreadTap; - - /// {@template onReplyTap} - /// The function called when tapping on replies - /// {@endtemplate} - final void Function(Message)? onReplyTap; - - /// {@template onDeleteTap} - /// The function called when delete confirmation button is tapped. - /// {@endtemplate} - final Future Function(Message)? onConfirmDeleteTap; - - /// {@template editMessageInputBuilder} - /// Widget builder for edit message layout - /// {@endtemplate} - final Widget Function(BuildContext, Message)? editMessageInputBuilder; - - /// {@template textBuilder} - /// Widget builder for building text - /// {@endtemplate} - final Widget Function(BuildContext, Message)? textBuilder; - - /// {@template onMessageActions} - /// Function called when a message is long-pressed to show actions. - /// If provided, this callback will be called instead of showing the default - /// message actions modal dialog. - /// {@endtemplate} - final void Function(BuildContext, Message)? onMessageActions; - - /// {@template onBouncedErrorMessageActions} - /// Function called when a message that has bounced with an error is long - /// pressed. If provided, this callback will be called instead of showing the - /// default bounced error message actions dialog. - /// {@endtemplate} - final void Function(BuildContext, Message)? onBouncedErrorMessageActions; - - /// {@template bottomRowBuilderWithDefaultWidget} - /// Widget builder for building a bottom row below the message. - /// Also contains the default bottom row widget. - /// {@endtemplate} - final BottomRowBuilderWithDefaultWidget? bottomRowBuilderWithDefaultWidget; - - /// {@template userAvatarBuilder} - /// Widget builder for building user avatar - /// {@endtemplate} - final Widget Function(BuildContext, User)? userAvatarBuilder; - - /// {@template quotedMessageBuilder} - /// Widget builder for building quoted message - /// {@endtemplate} - final Widget Function(BuildContext, Message)? quotedMessageBuilder; - - /// {@template message} /// The message to display. - /// {@endtemplate} final Message message; - /// {@template messageTheme} - /// The message theme - /// {@endtemplate} - final StreamMessageThemeData messageTheme; - - /// {@template reverse} - /// If true the widget will be mirrored - /// {@endtemplate} - final bool reverse; - - /// {@template shape} - /// The shape of the message text - /// {@endtemplate} - final ShapeBorder? shape; - - /// {@template attachmentShape} - /// The shape of an attachment - /// {@endtemplate} - final ShapeBorder? attachmentShape; - - /// {@template borderSide} - /// The borderSide of the message text - /// {@endtemplate} - final BorderSide? borderSide; - - /// {@template borderRadiusGeometry} - /// The border radius of the message text - /// {@endtemplate} - final BorderRadiusGeometry? borderRadiusGeometry; - - /// {@template padding} - /// The padding of the widget - /// {@endtemplate} + /// Outer padding around the entire message item. + /// + /// When non-null, takes precedence over the theme value from + /// [StreamMessageItemThemeData.padding]. + /// + /// When null (the default), the padding is determined by the theme. final EdgeInsetsGeometry? padding; - /// {@template textPadding} - /// The internal padding of the message text - /// {@endtemplate} - final EdgeInsets textPadding; - - /// {@template attachmentPadding} - /// The internal padding of an attachment - /// {@endtemplate} - final EdgeInsetsGeometry attachmentPadding; - - /// {@template widthFactor} - /// The percentage of the available width the message content should take - /// {@endtemplate} - final double widthFactor; - - /// {@template showUserAvatar} - /// It controls the display behaviour of the user avatar - /// {@endtemplate} - final DisplayWidget showUserAvatar; - - /// {@template showSendingIndicator} - /// It controls the display behaviour of the sending indicator - /// {@endtemplate} - final bool showSendingIndicator; - - /// {@template showReactions} - /// If `true` the message's reactions will be shown. - /// {@endtemplate} - final bool showReactions; - - /// {@template showThreadReplyIndicator} - /// If true the widget will show the thread reply indicator - /// {@endtemplate} - final bool showThreadReplyIndicator; - - /// {@template showInChannelIndicator} - /// If true the widget will show the show in channel indicator - /// {@endtemplate} - final bool showInChannelIndicator; - - /// {@template onUserAvatarTap} - /// The function called when tapping on UserAvatar - /// {@endtemplate} - final void Function(User)? onUserAvatarTap; - - /// {@template onLinkTap} - /// The function called when tapping on a link - /// {@endtemplate} - final void Function(String)? onLinkTap; - - /// {@template showReactionPicker} - /// Whether or not to show the reaction picker. - /// Used in [StreamMessageReactionsModal] and [MessageActionsModal]. - /// {@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} - final ShowMessageCallback? onShowMessage; + /// Horizontal spacing between the leading avatar and the content. + /// + /// When non-null, takes precedence over the theme value from + /// [StreamMessageItemThemeData.spacing]. + /// + /// When null (the default), the spacing is determined by the theme. + final double? spacing; - /// {@template showUsername} - /// If true show the users username next to the timestamp of the message - /// {@endtemplate} - final bool showUsername; - - /// {@template showTimestamp} - /// Show message timestamp - /// {@endtemplate} - final bool showTimestamp; - - /// {@template showTimestamp} - /// Show edited label if message is edited - /// {@endtemplate} - final bool showEditedLabel; - - /// {@template showReplyMessage} - /// Show reply action - /// {@endtemplate} - final bool showReplyMessage; - - /// {@template showThreadReplyMessage} - /// Show thread reply action - /// {@endtemplate} - final bool showThreadReplyMessage; - - /// {@template showMarkUnreadMessage} - /// Show mark unread action - /// {@endtemplate} - final bool showMarkUnreadMessage; - - /// {@template showEditMessage} - /// Show edit action - /// {@endtemplate} - final bool showEditMessage; - - /// {@template showCopyMessage} - /// Show copy action - /// {@endtemplate} - final bool showCopyMessage; - - /// {@template showDeleteMessage} - /// Show delete action - /// {@endtemplate} - final bool showDeleteMessage; - - /// {@template showResendMessage} - /// Show resend action - /// {@endtemplate} - final bool showResendMessage; - - /// {@template showFlagButton} - /// Show flag action - /// {@endtemplate} - final bool showFlagButton; - - /// {@template showPinButton} - /// Show pin action - /// {@endtemplate} - final bool showPinButton; - - /// {@template showPinHighlight} - /// Display Pin Highlight - /// {@endtemplate} - final bool showPinHighlight; - - /// {@template attachmentBuilders} - /// List of attachment builders for rendering attachment widgets pre-defined - /// and custom attachment types. + /// Background color for the entire message item row. /// - /// If null, the widget will create a default list of attachment builders - /// based on the [Attachment.type] of the attachment. - /// {@endtemplate} - final List? attachmentBuilders; + /// When non-null, takes precedence over the theme value from + /// [StreamMessageItemThemeData.backgroundColor]. + /// + /// When null (the default), the background color is determined by the theme. + final Color? backgroundColor; - /// {@template translateUserAvatar} - /// Center user avatar with bottom of the message - /// {@endtemplate} - final bool translateUserAvatar; + /// Maximum width of the message content column, in logical pixels. + /// + /// The content uses at most this width while still respecting the parent + /// [Flex] constraints. Use [double.infinity] to impose no cap from this + /// widget. Defaults to `264` when not specified. + final double maxWidth; - /// {@macro onQuotedMessageTap} - final OnQuotedMessageTap? onQuotedMessageTap; + /// Whether swiping the message triggers a quoted-reply action. + /// + /// When true, the message can be swiped from left to right to initiate a + /// reply. The swipe direction and reply icon position are always + /// start-to-end (left to right in LTR layouts), regardless of whether the + /// message belongs to the current user or another participant. + /// On completion, [onReplyTap] is invoked with the message. + /// + /// Swipe is disabled for deleted messages and messages in a failed state. + /// + /// Defaults to false. + final bool swipeToReply; - /// {@macro onMessageTap} - final OnMessageTap? onMessageTap; + /// Called when the message is tapped. + /// + /// If null, no tap gesture is registered on mobile. On desktop and web, + /// tap behaviour is unaffected because interactions are driven by the + /// context menu instead. + final void Function(Message message)? onMessageTap; - /// {@macro onMessageLongPress} - final OnMessageLongPress? onMessageLongPress; + /// Called when the message is long-pressed. + /// + /// If null, the default long-press behaviour is used, which opens the + /// [StreamMessageActionsModal] on mobile. Provide this callback to + /// override that behaviour entirely. + final void Function(Message message)? onMessageLongPress; - /// {@macro onReactionsTap} + /// Called when the author's avatar is tapped. /// - /// Note: Only used in mobile devices (iOS and Android). Do not confuse this - /// with the tap action on the reactions picker. - final OnReactionsTap? onReactionsTap; + /// If null, tapping the avatar has no effect. A common use is to navigate + /// to the user's profile screen. + final void Function(User user)? onUserAvatarTap; - /// {@macro onReactionsHover} + /// Called when a link is tapped in the message text. /// - /// Note: Only used in desktop devices (web and desktop). - final OnReactionsHover? onReactionsHover; + /// Receives the [Message] containing the link and the tapped URL string. + /// If null, the default link handling behaviour is used. + final void Function(Message message, String url)? onMessageLinkTap; - /// {@template customActions} - /// List of custom actions shown on message long tap - /// {@endtemplate} - final List customActions; + /// Called when a `@mention` is tapped in the message text. + /// + /// Receives the mentioned [User] resolved from the message's + /// [Message.mentionedUsers] list. If null, tapping a mention has no effect. + final void Function(User user)? onUserMentionTap; - /// {@macro onMessageWidgetAttachmentTap} - final StreamAttachmentWidgetTapCallback? onAttachmentTap; + /// Called when the thread reply indicator is tapped. + /// + /// [parentMessage] is the root message of the thread. When the tapped + /// message was shown in-channel via [Message.showInChannel], + /// [threadMessage] contains the original in-channel reply so that the + /// caller can scroll to / highlight it inside the thread view. + /// Otherwise [threadMessage] is null. + /// + /// If null, tapping the thread indicator has no effect. + final void Function(Message parentMessage, Message? threadMessage)? onThreadTap; - /// {@macro attachmentActionsBuilder} - final AttachmentActionsBuilder? attachmentActionsModalBuilder; + /// Called when the "View" button on the "Also sent in channel" annotation + /// is tapped inside a thread view. + /// + /// Typically used to pop the thread screen and scroll to / highlight the + /// message in the parent channel list. + /// + /// When null, the "View" button falls back to [onThreadTap]. + final void Function(Message message)? onViewInChannelTap; - /// Size of the image attachment thumbnail. - final Size imageAttachmentThumbnailSize; + /// Called when the quoted-reply action is selected from the actions list. + /// + /// Receives the [Message] that should be quoted. Typically used to set the + /// quoted message on the message input. + /// + /// If null, the quoted-reply action is still shown but has no effect. + final void Function(Message message)? onReplyTap; - /// Resize type of the image attachment thumbnail. + /// Called when the reactions row beneath the message bubble is tapped. /// - /// Defaults to [crop] - final String /*clip|crop|scale|fill*/ imageAttachmentThumbnailResizeType; + /// If null, the default behaviour opens a [ReactionDetailSheet] showing + /// the full list of reactions. Provide this callback to replace that + /// default with custom handling. + final void Function(Message message)? onReactionsTap; - /// Crop type of the image attachment thumbnail. + /// Called when an inline quoted message is tapped. /// - /// Defaults to [center] - final String /*center|top|bottom|left|right*/ - imageAttachmentThumbnailCropType; - - /// {@template copyWith} - /// Creates a copy of [StreamMessageWidget] with specified attributes - /// overridden. - /// {@endtemplate} - StreamMessageWidget copyWith({ - Key? key, - void Function(User)? onMentionTap, - void Function(Message)? onThreadTap, - void Function(Message)? onReplyTap, - Future Function(Message)? onConfirmDeleteTap, - Widget Function(BuildContext, Message)? editMessageInputBuilder, - Widget Function(BuildContext, Message)? textBuilder, - Widget Function(BuildContext, Message)? quotedMessageBuilder, - BottomRowBuilderWithDefaultWidget? bottomRowBuilderWithDefaultWidget, - void Function(BuildContext, Message)? onMessageActions, - void Function(BuildContext, Message)? onBouncedErrorMessageActions, + /// Receives the [Message] that was quoted. Typically used to scroll to + /// the original message in the list. + /// + /// If null, tapping the quoted message has no effect. + final void Function(Message quotedMessage)? onQuotedMessageTap; + + /// Controls how reaction groups are sorted when displayed. + /// + /// Defaults to [ReactionSorting.byFirstReactionAt]. + final Comparator? reactionSorting; + + /// Allows customizing the default message actions list. + /// + /// Receives the [BuildContext] and the default list of + /// [StreamContextMenuAction] items built by the widget. Return a modified + /// list to add, remove, or reorder actions. + final MessageActionsBuilder? actionsBuilder; + + /// Called when a normal message is long-pressed to show actions. + /// + /// When provided, this callback replaces the default behaviour of showing + /// the [StreamMessageActionsModal]. + final void Function(BuildContext context, Message message)? onMessageActions; + + /// Called when a bounced-error message is long-pressed. + /// + /// When provided, this callback replaces the default behaviour of showing + /// the [ModeratedMessageActionsModal]. + final void Function(BuildContext context, Message message)? onBouncedErrorMessageActions; + + /// Called when the edit-message action is selected. + /// + /// When provided, this callback replaces the default behaviour of showing + /// the edit-message bottom sheet via [showEditMessageSheet]. + final void Function(Message message)? onEditMessageTap; + + /// Custom attachment builders for rendering message attachments. + /// + /// When non-null, these builders are used instead of the default ones + /// provided by [StreamChatConfigurationData.attachmentBuilders]. + /// + /// Custom builders are prepended to the default builder list, so they take + /// priority for attachment types they can handle. + final List? attachmentBuilders; + + /// Called when the "show in chat" action is tapped in the full-screen + /// media gallery. + /// + /// Receives the [Message] and its [Channel] so the caller can scroll to + /// the message in the channel view. + final ShowMessageCallback? onShowMessage; + + /// Widget builder for the attachment actions modal shown in the full-screen + /// media gallery. + /// + /// When non-null, allows customizing the [AttachmentActionsModal] displayed + /// when the user taps the actions button in the gallery header. + final AttachmentActionsBuilder? attachmentActionsModalBuilder; + + /// Returns a copy of this [StreamMessageWidgetProps] with the given fields + /// replaced with new values. + StreamMessageWidgetProps copyWith({ Message? message, - StreamMessageThemeData? messageTheme, - bool? reverse, - ShapeBorder? shape, - ShapeBorder? attachmentShape, - BorderSide? borderSide, - BorderRadiusGeometry? borderRadiusGeometry, EdgeInsetsGeometry? padding, - EdgeInsets? textPadding, - EdgeInsetsGeometry? attachmentPadding, - double? widthFactor, - DisplayWidget? showUserAvatar, - bool? showSendingIndicator, - bool? showReactions, - bool? allRead, - bool? showThreadReplyIndicator, - bool? showInChannelIndicator, + double? spacing, + Color? backgroundColor, + double? maxWidth, + bool? swipeToReply, + void Function(Message)? onMessageTap, + void Function(Message)? onMessageLongPress, void Function(User)? onUserAvatarTap, - void Function(String)? onLinkTap, - bool? showReactionBrowser, - bool? showReactionPicker, - bool? showReactionTail, - List? readList, - ShowMessageCallback? onShowMessage, - bool? showUsername, - bool? showTimestamp, - bool? showEditedLabel, - bool? showReplyMessage, - bool? showThreadReplyMessage, - bool? showEditMessage, - bool? showCopyMessage, - bool? showDeleteMessage, - bool? showResendMessage, - bool? showFlagButton, - bool? showPinButton, - bool? showPinHighlight, - bool? showMarkUnreadMessage, + void Function(Message, String)? onMessageLinkTap, + void Function(User)? onUserMentionTap, + void Function(Message, Message?)? onThreadTap, + void Function(Message)? onViewInChannelTap, + void Function(Message)? onReplyTap, + void Function(Message)? onReactionsTap, + void Function(Message)? onQuotedMessageTap, + Comparator? reactionSorting, + MessageActionsBuilder? actionsBuilder, + void Function(BuildContext, Message)? onMessageActions, + void Function(BuildContext, Message)? onBouncedErrorMessageActions, + void Function(Message)? onEditMessageTap, List? attachmentBuilders, - bool? translateUserAvatar, - OnQuotedMessageTap? onQuotedMessageTap, - OnMessageTap? onMessageTap, - OnMessageLongPress? onMessageLongPress, - OnReactionsTap? onReactionsTap, - OnReactionsHover? onReactionsHover, - List? customActions, - void Function(Message message, Attachment attachment)? onAttachmentTap, - Widget Function(BuildContext, User)? userAvatarBuilder, - Size? imageAttachmentThumbnailSize, - String? imageAttachmentThumbnailResizeType, - String? imageAttachmentThumbnailCropType, + ShowMessageCallback? onShowMessage, AttachmentActionsBuilder? attachmentActionsModalBuilder, }) { - return StreamMessageWidget( - key: key ?? this.key, - onMentionTap: onMentionTap ?? this.onMentionTap, - onThreadTap: onThreadTap ?? this.onThreadTap, - onReplyTap: onReplyTap ?? this.onReplyTap, - onConfirmDeleteTap: onConfirmDeleteTap ?? this.onConfirmDeleteTap, - editMessageInputBuilder: - editMessageInputBuilder ?? this.editMessageInputBuilder, - textBuilder: textBuilder ?? this.textBuilder, - quotedMessageBuilder: quotedMessageBuilder ?? this.quotedMessageBuilder, - bottomRowBuilderWithDefaultWidget: bottomRowBuilderWithDefaultWidget ?? - this.bottomRowBuilderWithDefaultWidget, - onMessageActions: onMessageActions ?? this.onMessageActions, - onBouncedErrorMessageActions: - onBouncedErrorMessageActions ?? this.onBouncedErrorMessageActions, + return StreamMessageWidgetProps( message: message ?? this.message, - messageTheme: messageTheme ?? this.messageTheme, - reverse: reverse ?? this.reverse, - shape: shape ?? this.shape, - attachmentShape: attachmentShape ?? this.attachmentShape, - borderSide: borderSide ?? this.borderSide, - borderRadiusGeometry: borderRadiusGeometry ?? this.borderRadiusGeometry, padding: padding ?? this.padding, - textPadding: textPadding ?? this.textPadding, - attachmentPadding: attachmentPadding ?? this.attachmentPadding, - widthFactor: widthFactor ?? this.widthFactor, - showUserAvatar: showUserAvatar ?? this.showUserAvatar, - showSendingIndicator: showSendingIndicator ?? this.showSendingIndicator, - showEditedLabel: showEditedLabel ?? this.showEditedLabel, - showReactions: showReactions ?? this.showReactions, - showThreadReplyIndicator: - showThreadReplyIndicator ?? this.showThreadReplyIndicator, - showInChannelIndicator: - showInChannelIndicator ?? this.showInChannelIndicator, - 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, - showReplyMessage: showReplyMessage ?? this.showReplyMessage, - showThreadReplyMessage: - showThreadReplyMessage ?? this.showThreadReplyMessage, - showEditMessage: showEditMessage ?? this.showEditMessage, - showCopyMessage: showCopyMessage ?? this.showCopyMessage, - showDeleteMessage: showDeleteMessage ?? this.showDeleteMessage, - showResendMessage: showResendMessage ?? this.showResendMessage, - showFlagButton: showFlagButton ?? this.showFlagButton, - showPinButton: showPinButton ?? this.showPinButton, - showPinHighlight: showPinHighlight ?? this.showPinHighlight, - showMarkUnreadMessage: - showMarkUnreadMessage ?? this.showMarkUnreadMessage, - attachmentBuilders: attachmentBuilders ?? this.attachmentBuilders, - translateUserAvatar: translateUserAvatar ?? this.translateUserAvatar, - onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap, + spacing: spacing ?? this.spacing, + backgroundColor: backgroundColor ?? this.backgroundColor, + maxWidth: maxWidth ?? this.maxWidth, + swipeToReply: swipeToReply ?? this.swipeToReply, onMessageTap: onMessageTap ?? this.onMessageTap, onMessageLongPress: onMessageLongPress ?? this.onMessageLongPress, + onUserAvatarTap: onUserAvatarTap ?? this.onUserAvatarTap, + onMessageLinkTap: onMessageLinkTap ?? this.onMessageLinkTap, + onUserMentionTap: onUserMentionTap ?? this.onUserMentionTap, + onThreadTap: onThreadTap ?? this.onThreadTap, + onViewInChannelTap: onViewInChannelTap ?? this.onViewInChannelTap, + onReplyTap: onReplyTap ?? this.onReplyTap, onReactionsTap: onReactionsTap ?? this.onReactionsTap, - onReactionsHover: onReactionsHover ?? this.onReactionsHover, - customActions: customActions ?? this.customActions, - onAttachmentTap: onAttachmentTap ?? this.onAttachmentTap, - userAvatarBuilder: userAvatarBuilder ?? this.userAvatarBuilder, - imageAttachmentThumbnailSize: - imageAttachmentThumbnailSize ?? this.imageAttachmentThumbnailSize, - imageAttachmentThumbnailResizeType: imageAttachmentThumbnailResizeType ?? - this.imageAttachmentThumbnailResizeType, - imageAttachmentThumbnailCropType: imageAttachmentThumbnailCropType ?? - this.imageAttachmentThumbnailCropType, - attachmentActionsModalBuilder: - attachmentActionsModalBuilder ?? this.attachmentActionsModalBuilder, + onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap, + reactionSorting: reactionSorting ?? this.reactionSorting, + actionsBuilder: actionsBuilder ?? this.actionsBuilder, + onMessageActions: onMessageActions ?? this.onMessageActions, + onBouncedErrorMessageActions: onBouncedErrorMessageActions ?? this.onBouncedErrorMessageActions, + onEditMessageTap: onEditMessageTap ?? this.onEditMessageTap, + attachmentBuilders: attachmentBuilders ?? this.attachmentBuilders, + onShowMessage: onShowMessage ?? this.onShowMessage, + attachmentActionsModalBuilder: attachmentActionsModalBuilder ?? this.attachmentActionsModalBuilder, ); } - - @override - _StreamMessageWidgetState createState() => _StreamMessageWidgetState(); } -class _StreamMessageWidgetState extends State - with AutomaticKeepAliveClientMixin { - bool get showThreadReplyIndicator => widget.showThreadReplyIndicator; - - bool get showSendingIndicator => widget.showSendingIndicator; - - bool get isDeleted => widget.message.isDeleted; - - bool get showUsername => widget.showUsername; - - bool get showTimeStamp => widget.showTimestamp; - - bool get showEditedLabel => widget.showEditedLabel; - - bool get isTextEdited => widget.message.messageTextUpdatedAt != null; - - bool get showInChannel => widget.showInChannelIndicator; - - /// {@template hasQuotedMessage} - /// `true` if [StreamMessageWidget.quotedMessage] is not null. - /// {@endtemplate} - bool get hasQuotedMessage => widget.message.quotedMessage != null; - - bool get isSendFailed => widget.message.state.isSendingFailed; - - bool get isUpdateFailed => widget.message.state.isUpdatingFailed; - - bool get isDeleteFailed => widget.message.state.isDeletingFailed; - - bool get isBouncedWithError => widget.message.isBouncedWithError; - - /// {@template isFailedState} - /// Whether the message has failed to be sent, updated, deleted or is bounced - /// back with the message type as error. - /// {@endtemplate} - bool get isFailedState => - isSendFailed || isUpdateFailed || isDeleteFailed || isBouncedWithError; - - /// {@template isGiphy} - /// `true` if any of the [message]'s attachments are a giphy. - /// {@endtemplate} - bool get isGiphy => widget.message.attachments - .any((element) => element.type == AttachmentType.giphy); - - /// {@template isOnlyEmoji} - /// `true` if [message.text] contains only emoji. - /// {@endtemplate} - bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true; - - /// {@template hasNonUrlAttachments} - /// `true` if any of the [message]'s attachments are a giphy and do not - /// have a [Attachment.titleLink]. - /// {@endtemplate} - 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]. - /// {@endtemplate} - bool get hasUrlAttachments => widget.message.attachments - .any((it) => it.type == AttachmentType.urlPreview); - - /// {@template showBottomRow} - /// Show the [BottomRow] widget if any of the following are `true`: - /// * [StreamMessageWidget.showThreadReplyIndicator] - /// * [StreamMessageWidget.showUsername] - /// * [StreamMessageWidget.showTimestamp] - /// * [StreamMessageWidget.showInChannelIndicator] - /// * [StreamMessageWidget.showSendingIndicator] - /// * [StreamMessageWidget.message.isDeleted] - /// {@endtemplate} - bool get showBottomRow => - showThreadReplyIndicator || - showUsername || - showTimeStamp || - showInChannel || - showSendingIndicator || - isTextEdited; - - /// {@template isPinned} - /// Whether [StreamMessageWidget.message] is pinned or not. - /// {@endtemplate} - bool get isPinned => widget.message.pinned && !widget.message.isDeleted; - - /// {@template shouldShowReactions} - /// Should show message reactions if [StreamMessageWidget.showReactions] is - /// `true`, if there are reactions to show, and if the message is not deleted. - /// {@endtemplate} - bool get shouldShowReactions => - widget.showReactions && - (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; +/// The default implementation of [StreamMessageWidget]. +/// +/// Composes a full message row with an author avatar, content bubble, +/// header annotations, footer metadata, and platform-adaptive interaction +/// handling (tap and long-press on mobile, right-click context menu on +/// desktop and web). +/// +/// Message actions can be customised through +/// [StreamMessageWidgetProps.actionsBuilder]. +/// +/// See also: +/// +/// * [StreamMessageWidget], the public API widget. +/// * [StreamMessageWidgetProps], which configures this widget. +/// * [StreamMessageItemTheme], provides theme data to this widget. +class DefaultStreamMessage extends StatelessWidget { + /// Creates a default chat message widget with the given [props]. + const DefaultStreamMessage({super.key, required this.props}); - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _streamChatTheme = StreamChatTheme.of(context); - _streamChat = StreamChat.of(context); - } + /// The properties that configure this widget. + final StreamMessageWidgetProps props; @override Widget build(BuildContext context) { - super.build(context); - final avatarWidth = - widget.messageTheme.avatarTheme?.constraints.maxWidth ?? 40; - final bottomRowPadding = - widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; - - final showReactions = shouldShowReactions; + final message = props.message; + + final placement = StreamMessageLayout.of(context); + final theme = core.StreamMessageItemTheme.of(context); + final defaults = _StreamMessageWidgetDefaults( + context, + isPinned: message.pinned, + isEdited: message.messageTextUpdatedAt != null, + isBouncedWithError: message.isBouncedWithError, + state: message.state, + ); - return ConditionalParentBuilder( - builder: (context, child) { - final message = widget.message; + final resolve = core.StreamMessageLayoutResolver(placement, [theme, defaults]); + + final effectivePadding = props.padding ?? theme.padding ?? defaults.padding; + final effectiveSpacing = props.spacing ?? theme.spacing ?? defaults.spacing; + final effectiveBackgroundColor = props.backgroundColor ?? theme.backgroundColor ?? defaults.backgroundColor; + final effectiveAvatarVisibility = resolve((theme) => theme?.avatarVisibility); + final effectiveAnnotationVisibility = resolve((theme) => theme?.annotationVisibility); + final effectiveErrorBadgeVisibility = resolve((theme) => theme?.errorBadgeVisibility); + final effectiveMetadataVisibility = resolve((theme) => theme?.metadataVisibility); + final effectiveRepliesVisibility = resolve((theme) => theme?.repliesVisibility); + + Widget? leadingWidget; + if (props.message.user case final user?) { + final effectiveAvatarSize = theme.avatarSize ?? defaults.avatarSize; + + leadingWidget = effectiveAvatarVisibility.apply( + core.StreamAvatarTheme( + data: .new(size: effectiveAvatarSize), + child: StreamUserAvatar(user: user, showOnlineIndicator: false), + ), + ); + } - // 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; + final annotationWidget = effectiveAnnotationVisibility.apply( + StreamMessageAnnotations( + message: message, + onViewChannelTap: switch (props.onViewInChannelTap) { + final onTap? => () => onTap(message), + _ => () => _onViewThread(context, message), + }, + ), + ); - final menuItems = _buildDesktopOrWebActions(context, message); - if (menuItems.isEmpty) return child; + final metadataWidget = effectiveMetadataVisibility.apply( + StreamMessageMetadata(message: message), + ); - return ContextMenuRegion( - contextMenuBuilder: (_, anchor) => ContextMenu( - anchor: anchor, - menuItems: menuItems, + Widget? repliesWidget; + if (message.replyCount case final replyCount? when replyCount > 0) { + repliesWidget = effectiveRepliesVisibility.apply( + core.StreamMessageReplies( + maxAvatars: 3, + onTap: () => _onViewThread(context, message), + showConnector: placement.contentKind != .jumbomoji, + label: Text('$replyCount replies'), + avatars: message.threadParticipants?.map( + (user) => StreamUserAvatar(user: user, showOnlineIndicator: false), ), - child: child, - ); + ), + ); + } + + final errorBadgeWidget = effectiveErrorBadgeVisibility.apply( + core.StreamErrorBadge(size: core.StreamErrorBadgeSize.sm), + ); + + final contentWidget = StreamMessageContent( + message: message, + annotation: annotationWidget, + errorBadge: errorBadgeWidget, + metadata: metadataWidget, + replies: repliesWidget, + attachmentBuilders: props.attachmentBuilders, + reactionSorting: props.reactionSorting, + onQuotedMessageTap: props.onQuotedMessageTap, + onShowMessage: props.onShowMessage, + onReplyTap: props.onReplyTap, + attachmentActionsModalBuilder: props.attachmentActionsModalBuilder, + onLinkTap: (_, href, __) { + if (href == null) return; + if (props.onMessageLinkTap case final onTap?) return onTap(message, href); + return launchURL(context, href).ignore(); + }, + onMentionTap: switch (props.onUserMentionTap) { + final onTap? => (_, id) { + final user = message.mentionedUsers.firstWhereOrNull((u) => u.id == id); + if (user != null) onTap(user); + }, + _ => null, + }, + onReactionsTap: switch (props.onReactionsTap) { + final onReactionsTap? => () => onReactionsTap(message), + _ => () => _showMessageReactionsModal(context, message), }, - 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, - ); - }), + ); + + Widget result = Material( + animateColor: true, + color: effectiveBackgroundColor, + child: PlatformWidgetBuilder( + mobile: (context, child) => InkWell( + onTap: switch (props.onMessageTap) { + final onMessageTap? => () => onMessageTap(message), + _ => null, + }, + onLongPress: switch (props.onMessageLongPress) { + final onMessageLongPress? => () => onMessageLongPress(message), + _ when message.state.isDeleted => null, + _ when message.state.isOutgoing => null, + _ => () => _onMessageLongPressed(context, message), + }, + child: child, + ), + desktopOrWeb: (context, child) { + final messageState = message.state; + + // 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 channel = StreamChannel.of(context).channel; + final menuItems = _buildDesktopOrWebActions(context, message); + if (menuItems.isEmpty) return MouseRegion(child: child); + + return ContextMenuRegion( + onSelected: (result) { + if (result is! MessageAction) return; + return _onActionTap(context, channel, result).ignore(); + }, + menuBuilder: (_, anchor) => ContextMenu( + anchor: anchor, + menuItems: menuItems, + ), + child: MouseRegion(child: child), + ); + }, + child: Align( + alignment: StreamMessageLayout.alignmentDirectionalOf(context), + child: Padding( + padding: effectivePadding, + child: Row( + mainAxisSize: .min, + spacing: effectiveSpacing, + crossAxisAlignment: .end, + children: [ + ?leadingWidget, + Flexible( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: props.maxWidth), + child: contentWidget, + ), ), - ), + ], ), ), ), ), ); + + if (props.swipeToReply && props.onReplyTap != null && !message.isDeleted && !message.state.isFailed) { + result = _SwipeToReplyWrapper( + message: message, + onReplyTap: props.onReplyTap!, + child: result, + ); + } + + return result; } + // Builds the action list for a bounced (moderation-error) message. + List _buildBouncedErrorMessageActions({ + required BuildContext context, + required Message message, + }) { + return StreamMessageActionsBuilder.buildBouncedErrorActions( + context: context, + message: message, + ); + } + + // Builds the standard action list, applying the custom actionsBuilder if set. + List _buildMessageActions({ + required BuildContext context, + required Message message, + required Channel channel, + OwnUser? currentUser, + }) { + final actions = StreamMessageActionsBuilder.buildActions( + context: context, + message: message, + channel: channel, + currentUser: currentUser, + ); + + if (props.actionsBuilder case final builder?) { + return builder(context, actions); + } + + return StreamContextMenuAction.partitioned(items: actions); + } + + // Dispatches to bounced-error or normal actions for desktop/web. List _buildDesktopOrWebActions( BuildContext context, Message message, ) { - if (isBouncedWithError) { + if (message.isBouncedWithError) { return _buildBouncedErrorMessageDesktopOrWebActions(context, message); } return _buildMessageDesktopOrWebActions(context, message); } + // Builds partitioned bounced-error actions for the desktop/web context menu. List _buildBouncedErrorMessageDesktopOrWebActions( BuildContext context, Message message, ) { - final theme = StreamChatTheme.of(context); - final channel = StreamChannel.of(context).channel; + final actions = StreamMessageActionsBuilder.buildBouncedErrorActions( + 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(); - }, - ), - ]; + return StreamContextMenuAction.partitioned(items: actions); } + // Builds normal actions + reaction picker for the desktop/web context menu. List _buildMessageDesktopOrWebActions( BuildContext context, Message message, ) { - final theme = StreamChatTheme.of(context); final channel = StreamChannel.of(context).channel; + final currentUser = channel.client.state.currentUser; + final showPicker = channel.canSendReaction; + + final actions = _buildMessageActions( + context: context, + message: message, + channel: channel, + currentUser: currentUser, + ); + + void onReactionPicked(Reaction reaction) { + final action = SelectReaction(message: message, reaction: reaction); + return Navigator.pop(context, action); // Pop the modal with the selected reaction action + } return [ - if (widget.showReactionPicker) - StreamChatContextMenuItem( - child: StreamChannel( - channel: channel, - child: ContextMenuReactionPicker(message: message), - ), - ), - 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), - ), - ), + if (showPicker) StreamMessageReactionPicker(message: message, onReactionPicked: onReactionPicked), + ...actions, ]; } - void _showMessageReactionsModal( + // Opens the reaction detail sheet and handles the returned action. + Future _showMessageReactionsModal( BuildContext context, Message message, - ) { + ) async { final channel = StreamChannel.of(context).channel; - showDialog( - useRootNavigator: false, + final action = await ReactionDetailSheet.show( context: context, - useSafeArea: false, - barrierColor: _streamChatTheme.colorTheme.overlay, - builder: (context) => StreamChannel( - channel: channel, - 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, - ), - 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, - ), - ), + message: message, ); + + if (action is! MessageAction) return; + return _onActionTap(context, channel, action).ignore(); } - void _onMessageLongPressed( + // Resolves the thread parent (fetching if shown in-channel) and invokes + // the onThreadTap callback with both the parent and the original message. + Future _onViewThread( + BuildContext context, + Message message, + ) async { + try { + if (message.showInChannel case true) { + final streamChannel = StreamChannel.of(context); + final parentMessage = await streamChannel.getMessage(message.parentId!); + return props.onThreadTap?.call(parentMessage, message); + } + return props.onThreadTap?.call(message, null); + } catch (e, stk) { + debugPrint('Error while fetching message: $e, $stk'); + } + } + + // Routes a long-press to bounced-error or normal actions handler. + Future _onMessageLongPressed( BuildContext context, Message message, ) { - if (isBouncedWithError) { + if (message.isBouncedWithError) { return _onBouncedErrorMessageActions(context, message); } return _onMessageActions(context, message); } - void _onBouncedErrorMessageActions( + // Delegates to the custom callback or falls back to the default dialog. + Future _onBouncedErrorMessageActions( BuildContext context, Message message, - ) { - if (widget.onBouncedErrorMessageActions case final onActions?) { + ) async { + if (props.onBouncedErrorMessageActions case final onActions?) { return onActions(context, message); } return _showBouncedErrorMessageActionsDialog(context, message); } - void _showBouncedErrorMessageActionsDialog( + // Shows the ModeratedMessageActionsModal for a bounced-error message. + 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, + ); + + final action = await showStreamDialog( + context: context, + useRootNavigator: false, + builder: (_) => ModeratedMessageActionsModal( + message: message, + messageActions: actions, + ), ); + + if (action is! MessageAction) return; + return _onActionTap(context, channel, action).ignore(); } - void _onMessageActions( + // Delegates to the custom callback or falls back to the default modal. + Future _onMessageActions( BuildContext context, Message message, - ) { - if (widget.onMessageActions case final onActions?) { + ) async { + if (props.onMessageActions case final onActions?) { return onActions(context, message); } return _showMessageActionModalDialog(context, message); } - void _showMessageActionModalDialog( + // Shows the StreamMessageActionsModal with a reaction picker and actions. + Future _showMessageActionModalDialog( BuildContext context, Message message, - ) { + ) async { final channel = StreamChannel.of(context).channel; + final currentUser = channel.client.state.currentUser; + final showPicker = channel.canSendReaction; - showDialog( - useRootNavigator: false, + final actions = _buildMessageActions( context: context, - useSafeArea: false, - barrierColor: _streamChatTheme.colorTheme.overlay, - builder: (context) { - return StreamChannel( - channel: channel, - child: MessageActionsModal( + message: message, + channel: channel, + currentUser: currentUser, + ); + + final layout = StreamMessageLayout.of(context); + final theme = core.StreamMessageItemTheme.of(context); + final defaults = _StreamMessageWidgetDefaults( + context, + isPinned: message.pinned, + isEdited: message.messageTextUpdatedAt != null, + state: message.state, + ); + + final resolve = core.StreamMessageLayoutResolver(layout, [theme, defaults]); + final avatarVisibility = resolve((theme) => theme?.avatarVisibility); + + var leadingInset = 0.0; + if (avatarVisibility != core.StreamVisibility.gone) { + final effectiveAvatarSize = theme.avatarSize ?? defaults.avatarSize; + final effectiveSpacing = props.spacing ?? theme.spacing ?? defaults.spacing; + leadingInset = effectiveAvatarSize.value + effectiveSpacing; + } + + final action = await showStreamDialog( + context: context, + useRootNavigator: false, + builder: (_) => StreamChatConfiguration( + data: StreamChatConfiguration.of(context), + child: StreamMessageLayout( + data: layout, + child: StreamMessageActionsModal( message: message, - messageWidget: widget.copyWith( - key: const Key('MessageWidget'), - message: message.copyWith( - text: (message.text?.length ?? 0) > 200 - ? '${message.text!.substring(0, 200)}...' - : message.text, + messageActions: actions, + showReactionPicker: showPicker, + leadingInset: leadingInset, + messageWidget: StreamChannel( + channel: channel, + child: StreamMessageWidget( + key: const Key('MessageWidget'), + message: message.trimmed, + padding: EdgeInsets.zero, + backgroundColor: core.StreamColors.transparent, ), - showReactions: false, - showReactionTail: calculateReactionTailEnabled( - ReactionTailType.messageActions, + ), + ), + ), + ), + ); + + if (action is! MessageAction) return; + return _onActionTap(context, channel, action).ignore(); + } + + // Dispatches a MessageAction to the appropriate channel or callback handler. + Future _onActionTap( + BuildContext context, + Channel channel, + MessageAction action, + ) async => 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() => props.onEditMessageTap?.call(action.message), + 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() => props.onReplyTap?.call(action.message), + ThreadReply() => props.onThreadTap?.call(action.message, null), + }; + + // Copies the message text (with mentions replaced) to the clipboard. + 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)); + } + + // Shows a confirmation dialog before deleting the message. + 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 != true) return null; + + return channel.deleteMessage(message); + } + + // Shows a confirmation dialog before flagging the message. + 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 != true) return null; + + final messageId = message.id; + return channel.client.flagMessage(messageId); + } + + // Toggles a reaction: removes it if already present, otherwise sends it. + 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, + ); + } +} + +// Truncates long message text for display in the actions modal preview. +extension on Message { + // Returns a copy with text and nested content truncated to 100 characters. + 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, + ); + } +} + +// Truncates long poll names for display in the actions modal preview. +extension on Poll { + // Returns a copy with name truncated to 100 characters. + Poll get trimmed { + final trimmedName = switch (name) { + final name when name.length > 100 => '${name.substring(0, 100)}...', + _ => name, + }; + + return copyWith(name: trimmedName); + } +} + +class _SwipeToReplyWrapper extends StatelessWidget { + const _SwipeToReplyWrapper({ + required this.message, + required this.onReplyTap, + required this.child, + }); + + final Message message; + final void Function(Message) onReplyTap; + final Widget child; + + static const _swipeThreshold = 0.2; + + @override + Widget build(BuildContext context) { + return Swipeable( + key: ValueKey('swipe-${message.id}'), + direction: SwipeDirection.startToEnd, + swipeThreshold: _swipeThreshold, + onSwiped: (_) => onReplyTap(message), + backgroundBuilder: (context, details) { + final progress = math.min(details.progress, _swipeThreshold) / _swipeThreshold; + final offset = Offset.lerp(const Offset(-24, 0), const Offset(12, 0), progress)!; + + return Align( + alignment: AlignmentDirectional.centerStart, + child: Transform.translate( + offset: offset, + child: Opacity( + opacity: progress, + child: SizedBox.square( + dimension: 32, + child: CustomPaint( + painter: AnimatedCircleBorderPainter( + progress: progress, + color: context.streamColorScheme.borderDefault, + ), + child: Center( + child: Icon( + context.streamIcons.reply20, + size: lerpDouble(0, 20, progress), + ), + ), + ), ), - 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, ), - 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, ), ); }, + child: child, ); } +} - /// 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; - } +// Built-in fallback theme values for [DefaultStreamMessage]. +// +// Used when neither the explicit props nor the ambient +// [StreamMessageItemThemeData] provide a value for a given property. +class _StreamMessageWidgetDefaults extends core.StreamMessageItemThemeData { + _StreamMessageWidgetDefaults( + this._context, { + this.isPinned = false, + this.isEdited = false, + this.isBouncedWithError = false, + required MessageState state, + }) : _messageState = state; + + final bool isPinned; + final bool isEdited; + final bool isBouncedWithError; + + final BuildContext _context; + final MessageState _messageState; + + late final core.StreamSpacing _spacing = _context.streamSpacing; + late final core.StreamColorScheme _colorScheme = _context.streamColorScheme; + + @override + double get spacing => _spacing.xs; + + @override + StreamAvatarSize get avatarSize => .md; + + @override + EdgeInsetsGeometry get padding => .symmetric(horizontal: _spacing.md); + + @override + Color? get backgroundColor { + if (isPinned && !_messageState.isDeleted) return _colorScheme.backgroundHighlight; + return core.StreamColors.transparent; } -} -/// 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, + @override + core.StreamMessageLayoutVisibility get avatarVisibility => .resolveWith( + (placement) => switch ((placement.channelKind, placement.alignment, placement.stackPosition)) { + (.direct, _, _) || (_, .end, _) => .gone, + (_, _, .top || .middle) => .hidden, + (_, _, .single || .bottom) => .visible, + }, + ); + + @override + core.StreamMessageLayoutVisibility get annotationVisibility => .all(.visible); + + @override + core.StreamMessageLayoutVisibility get errorBadgeVisibility => .all( + _messageState.isFailed || isBouncedWithError ? .visible : .gone, + ); - /// Message is in the [MessageActionsModal] - messageActions, + @override + core.StreamMessageLayoutVisibility get metadataVisibility { + if (isEdited) return .all(.visible); + return .resolveWith( + (placement) => switch (placement.stackPosition) { + .single || .bottom => .visible, + _ => .gone, + }, + ); + } - /// Message is in the message reactions modal - reactions, + @override + core.StreamMessageLayoutVisibility get repliesVisibility => .resolveWith( + (layout) => switch (layout.listKind) { + .thread => .gone, + .channel => .visible, + }, + ); } 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 deleted file mode 100644 index a6ae651476..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content.dart +++ /dev/null @@ -1,471 +0,0 @@ -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/stream_chat_flutter.dart'; - -/// Signature for the builder function that will be called when the message -/// bottom row is built. Includes the [Message]. -typedef BottomRowBuilder = Widget Function(BuildContext, Message); - -/// Signature for the builder function that will be called when the message -/// bottom row is built. Includes the [Message] and the default [BottomRow]. -typedef BottomRowBuilderWithDefaultWidget = Widget Function( - BuildContext, - Message, - BottomRow, -); - -/// {@template messageWidgetContent} -/// The main content of a [StreamMessageWidget]. -/// -/// Should not be used outside of [MessageWidget. -/// {@endtemplate} -@internal -class MessageWidgetContent extends StatelessWidget { - /// {@macro messageWidgetContent} - const MessageWidgetContent({ - super.key, - required this.reverse, - required this.isPinned, - required this.showPinHighlight, - required this.showBottomRow, - required this.message, - required this.showUserAvatar, - required this.avatarWidth, - required this.showReactions, - required this.onReactionsTap, - required this.onReactionsHover, - required this.messageTheme, - required this.streamChatTheme, - required this.isFailedState, - required this.hasQuotedMessage, - required this.hasUrlAttachments, - required this.hasNonUrlAttachments, - required this.hasPoll, - required this.isOnlyEmoji, - required this.isGiphy, - required this.attachmentBuilders, - required this.attachmentPadding, - required this.attachmentShape, - required this.onAttachmentTap, - required this.onShowMessage, - required this.onReplyTap, - required this.attachmentActionsModalBuilder, - required this.textPadding, - required this.showReactionPickerTail, - required this.translateUserAvatar, - required this.bottomRowPadding, - required this.showInChannel, - required this.streamChat, - required this.showSendingIndicator, - required this.showThreadReplyIndicator, - required this.showTimeStamp, - required this.showUsername, - required this.showEditedLabel, - required this.messageWidget, - required this.onThreadTap, - this.onUserAvatarTap, - this.borderRadiusGeometry, - this.borderSide, - this.shape, - this.onQuotedMessageTap, - this.onMentionTap, - this.onLinkTap, - this.textBuilder, - this.quotedMessageBuilder, - this.bottomRowBuilderWithDefaultWidget, - this.userAvatarBuilder, - }); - - /// {@macro reverse} - final bool reverse; - - /// {@macro isPinned} - final bool isPinned; - - /// {@macro showPinHighlight} - final bool showPinHighlight; - - /// {@macro showBottomRow} - final bool showBottomRow; - - /// {@macro message} - final Message message; - - /// {@macro showUserAvatar} - final DisplayWidget showUserAvatar; - - /// The width of the avatar. - final double avatarWidth; - - /// {@macro showReactions} - final bool showReactions; - - /// {@macro onReactionsTap} - final VoidCallback onReactionsTap; - - /// {@macro onReactionsHover} - final OnReactionsHover? onReactionsHover; - - /// {@macro messageTheme} - final StreamMessageThemeData messageTheme; - - /// {@macro onUserAvatarTap} - final void Function(User)? onUserAvatarTap; - - /// {@macro streamChatThemeData} - final StreamChatThemeData streamChatTheme; - - /// {@macro isFailedState} - final bool isFailedState; - - /// {@macro borderRadiusGeometry} - final BorderRadiusGeometry? borderRadiusGeometry; - - /// {@macro borderSide} - final BorderSide? borderSide; - - /// {@macro shape} - final ShapeBorder? shape; - - /// {@macro hasQuotedMessage} - final bool hasQuotedMessage; - - /// {@macro hasUrlAttachments} - final bool hasUrlAttachments; - - /// {@macro hasNonUrlAttachments} - final bool hasNonUrlAttachments; - - /// {@macro hasPoll} - final bool hasPoll; - - /// {@macro isOnlyEmoji} - final bool isOnlyEmoji; - - /// {@macro isGiphy} - final bool isGiphy; - - /// {@macro attachmentBuilders} - final List? attachmentBuilders; - - /// {@macro attachmentPadding} - final EdgeInsetsGeometry attachmentPadding; - - /// {@macro attachmentShape} - final ShapeBorder? attachmentShape; - - /// {@macro onAttachmentTap} - final StreamAttachmentWidgetTapCallback? onAttachmentTap; - - /// {@macro onShowMessage} - final ShowMessageCallback? onShowMessage; - - /// {@macro onReplyTap} - final void Function(Message)? onReplyTap; - - /// {@macro onThreadTap} - final void Function(Message)? onThreadTap; - - /// {@macro attachmentActionsBuilder} - final AttachmentActionsBuilder? attachmentActionsModalBuilder; - - /// {@macro textPadding} - final EdgeInsets textPadding; - - /// {@macro onQuotedMessageTap} - final OnQuotedMessageTap? onQuotedMessageTap; - - /// {@macro onMentionTap} - final void Function(User)? onMentionTap; - - /// {@macro onLinkTap} - final void Function(String)? onLinkTap; - - /// {@macro textBuilder} - final Widget Function(BuildContext, Message)? textBuilder; - - /// {@macro quotedMessageBuilder} - final Widget Function(BuildContext, Message)? quotedMessageBuilder; - - /// {@macro showReactionPickerTail} - final bool showReactionPickerTail; - - /// {@macro translateUserAvatar} - final bool translateUserAvatar; - - /// The padding to use for this widget. - final double bottomRowPadding; - - /// {@macro bottomRowBuilderWithDefaultWidget} - final BottomRowBuilderWithDefaultWidget? bottomRowBuilderWithDefaultWidget; - - /// {@macro showInChannelIndicator} - final bool showInChannel; - - /// {@macro streamChat} - final StreamChatState streamChat; - - /// {@macro showSendingIndicator} - final bool showSendingIndicator; - - /// {@macro showThreadReplyIndicator} - final bool showThreadReplyIndicator; - - /// {@macro showTimestamp} - final bool showTimeStamp; - - /// {@macro showUsername} - final bool showUsername; - - /// {@macro showEdited} - final bool showEditedLabel; - - /// {@macro messageWidget} - final StreamMessageWidget messageWidget; - - /// {@macro userAvatarBuilder} - final Widget Function(BuildContext, User)? userAvatarBuilder; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: - reverse ? CrossAxisAlignment.end : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - clipBehavior: Clip.none, - alignment: reverse - ? AlignmentDirectional.bottomEnd - : AlignmentDirectional.bottomStart, - children: [ - if (showBottomRow) - Padding( - padding: EdgeInsets.only( - left: !reverse ? bottomRowPadding : 0, - right: reverse ? bottomRowPadding : 0, - bottom: isPinned && showPinHighlight ? 6.0 : 0.0, - ), - child: _buildBottomRow(context), - ), - Padding( - padding: EdgeInsets.only( - bottom: isPinned && showPinHighlight ? 8.0 : 0.0, - ), - child: Column( - crossAxisAlignment: - reverse ? CrossAxisAlignment.end : CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (isPinned && message.pinnedBy != null && showPinHighlight) - PinnedMessage( - pinnedBy: message.pinnedBy!, - currentUser: streamChat.currentUser!, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - 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, - ), - ), - 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, - ), - ), - ), - ], - ), - ), - ), - 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, - child: DesktopReactionsBuilder( - message: message, - messageTheme: messageTheme, - onHover: onReactionsHover, - borderSide: borderSide, - reverse: reverse, - ), - ), - ], - if (showBottomRow) - SizedBox( - height: context.textScaleFactor * 18.0, - ), - ], - ), - ), - if (isFailedState) - Positioned( - right: reverse ? 0 : null, - left: reverse ? null : 0, - bottom: showBottomRow ? 18 : -2, - child: StreamSvgIcon( - icon: StreamSvgIcons.error, - color: streamChatTheme.colorTheme.accentError, - ), - ), - ], - ), - ], - ); - } - - Widget _buildBottomRow(BuildContext context) { - final defaultWidget = BottomRow( - onThreadTap: onThreadTap, - message: message, - reverse: reverse, - messageTheme: messageTheme, - hasUrlAttachments: hasUrlAttachments, - isOnlyEmoji: isOnlyEmoji, - isDeleted: message.isDeleted, - isGiphy: isGiphy, - showInChannel: showInChannel, - showSendingIndicator: showSendingIndicator, - showThreadReplyIndicator: showThreadReplyIndicator, - showTimeStamp: showTimeStamp, - showUsername: showUsername, - showEditedLabel: showEditedLabel, - streamChatTheme: streamChatTheme, - streamChat: streamChat, - hasNonUrlAttachments: hasNonUrlAttachments, - ); - - if (bottomRowBuilderWithDefaultWidget != null) { - return bottomRowBuilderWithDefaultWidget!( - context, - message, - defaultWidget, - ); - } - - return defaultWidget; - } -} 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 deleted file mode 100644 index ccbd8a0e8a..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget_content_components.dart +++ /dev/null @@ -1,9 +0,0 @@ -export 'bottom_row.dart'; -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/parse_attachments.dart b/packages/stream_chat_flutter/lib/src/message_widget/parse_attachments.dart index b32d09426b..52a5d3c5c5 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/parse_attachments.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/parse_attachments.dart @@ -1,22 +1,51 @@ +import 'dart:async'; +import 'dart:math' as math; + import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget_catalog.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart' as core; + +/// {@template onAttachmentWidgetTap} +/// A callback that is called when an attachment widget is tapped. +/// +/// Return `true` if the attachment was handled by your custom logic, +/// `false` to use the default handler which automatically: +/// - Opens URL previews in browser (or calls [onLinkTap] if provided) +/// - Opens images, videos, and giphys in full screen viewer +/// +/// Supports both synchronous and asynchronous operations via [FutureOr]. +/// +/// Example with custom location attachments: +/// ```dart +/// StreamMessageWidget( +/// message: message, +/// onAttachmentTap: (context, message, attachment) async { +/// if (attachment.type == 'location') { +/// await showLocationDialog(context, attachment); +/// return true; // Handled by custom logic +/// } +/// return false; // Use default behavior for other types +/// }, +/// ) +/// ``` +/// {@endtemplate} +typedef OnAttachmentWidgetTap = FutureOr Function(BuildContext context, Message message, Attachment attachment); /// {@template parseAttachments} /// Parses the attachments of a [StreamMessageWidget]. /// /// Used in [MessageCard]. Should not be used elsewhere. /// {@endtemplate} -class ParseAttachments extends StatelessWidget { +class ParseAttachments extends core.NullableStatelessWidget { /// {@macro parseAttachments} const ParseAttachments({ super.key, required this.message, - required this.attachmentBuilders, - required this.attachmentPadding, - this.attachmentShape, + this.attachmentBuilders, this.onAttachmentTap, this.onShowMessage, + this.onLinkTap, this.onReplyTap, this.attachmentActionsModalBuilder, }); @@ -27,18 +56,15 @@ class ParseAttachments extends StatelessWidget { /// {@macro attachmentBuilders} final List? attachmentBuilders; - /// {@macro attachmentPadding} - final EdgeInsetsGeometry attachmentPadding; - - /// {@macro attachmentShape} - final ShapeBorder? attachmentShape; - /// {@macro onAttachmentTap} - final StreamAttachmentWidgetTapCallback? onAttachmentTap; + final OnAttachmentWidgetTap? onAttachmentTap; /// {@macro onShowMessage} final ShowMessageCallback? onShowMessage; + /// {@macro onLinkTap} + final void Function(String)? onLinkTap; + /// {@macro onReplyTap} final void Function(Message)? onReplyTap; @@ -46,74 +72,90 @@ class ParseAttachments extends StatelessWidget { final AttachmentActionsBuilder? attachmentActionsModalBuilder; @override - Widget build(BuildContext context) { - // Create a default onAttachmentTap callback if not provided. - var onAttachmentTap = this.onAttachmentTap; - onAttachmentTap ??= (message, attachment) { - // If the current attachment is a url preview attachment, open the url - // in the browser. - final isUrlPreview = attachment.type == AttachmentType.urlPreview; - if (isUrlPreview) { - final url = attachment.ogScrapeUrl ?? ''; - launchURL(context, url); - return; - } - - final isImage = attachment.type == AttachmentType.image; - final isVideo = attachment.type == AttachmentType.video; - final isGiphy = attachment.type == AttachmentType.giphy; - - // If the current attachment is a media attachment, open the media - // attachment in full screen. - final isMedia = isImage || isVideo || isGiphy; - if (isMedia) { - final channel = StreamChannel.of(context).channel; - - final attachments = message.toAttachmentPackage( - filter: (it) { - final isImage = it.type == AttachmentType.image; - final isVideo = it.type == AttachmentType.video; - final isGiphy = it.type == AttachmentType.giphy; - return isImage || isVideo || isGiphy; - }, - ); - - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) { - return StreamChannel( - channel: channel, - child: StreamFullScreenMediaBuilder( - userName: message.user!.name, - mediaAttachmentPackages: attachments, - startIndex: attachments.indexWhere( - (it) => it.attachment.id == attachment.id, - ), - onReplyMessage: onReplyTap, - onShowMessage: onShowMessage, - attachmentActionsModalBuilder: attachmentActionsModalBuilder, - ), - ); - }, - ), - ); + Widget? nullableBuild(BuildContext context) { + Future effectiveOnAttachmentTap( + Message message, + Attachment attachment, + ) async { + // Try custom handler first. If it returns true, the attachment was + // handled. + final handled = await onAttachmentTap?.call(context, message, attachment); + if (handled ?? false) return; + + // Otherwise, use the default handler for standard attachment types. + return _defaultAttachmentTapHandler(context, message, attachment); + } - return; - } - }; + final config = StreamChatConfiguration.maybeOf(context); + final effectiveAttachmentBuilder = attachmentBuilders ?? config?.attachmentBuilders; // Create a default attachmentBuilders list if not provided. final builders = StreamAttachmentWidgetBuilder.defaultBuilders( message: message, - shape: attachmentShape, - padding: attachmentPadding, - onAttachmentTap: onAttachmentTap, - customAttachmentBuilders: attachmentBuilders, + onAttachmentTap: effectiveOnAttachmentTap, + customAttachmentBuilders: effectiveAttachmentBuilder, ); final catalog = AttachmentWidgetCatalog(builders: builders); return catalog.build(context, message); } + + Future _defaultAttachmentTapHandler( + BuildContext context, + Message message, + Attachment attachment, + ) async { + // If the current attachment is a url preview attachment, open the url + // in the browser. + final isUrlPreview = attachment.type == AttachmentType.urlPreview; + if (isUrlPreview) { + final url = attachment.ogScrapeUrl; + if (url == null) return; + + if (onLinkTap case final onTap?) return onTap(url); + return launchURL(context, url); + } + + final isImage = attachment.type == AttachmentType.image; + final isVideo = attachment.type == AttachmentType.video; + final isGiphy = attachment.type == AttachmentType.giphy; + + // If the current attachment is a media attachment, open the media + // attachment in full screen. + final isMedia = isImage || isVideo || isGiphy; + if (isMedia) { + final attachments = message.toAttachmentPackage( + filter: (it) { + final isImage = it.type == AttachmentType.image; + final isVideo = it.type == AttachmentType.video; + final isGiphy = it.type == AttachmentType.giphy; + return isImage || isVideo || isGiphy; + }, + ); + + final navigator = Navigator.of(context); + final channel = StreamChannel.of(context).channel; + final startIndex = attachments.indexWhere( + (it) => it.attachment.id == attachment.id, + ); + + return navigator.push( + MaterialPageRoute( + builder: (_) => StreamChannel( + channel: channel, + child: StreamFullScreenMediaBuilder( + userName: message.user!.name, + mediaAttachmentPackages: attachments, + startIndex: math.max(0, startIndex), + onReplyMessage: onReplyTap, + onShowMessage: onShowMessage, + attachmentActionsModalBuilder: attachmentActionsModalBuilder, + ), + ), + ), + ); + } + } } extension on Message { @@ -133,7 +175,7 @@ extension on Message { attachment: it, message: this, ); - }) + }), ]; } } diff --git a/packages/stream_chat_flutter/lib/src/message_widget/pinned_message.dart b/packages/stream_chat_flutter/lib/src/message_widget/pinned_message.dart deleted file mode 100644 index 49b9a4f219..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/pinned_message.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template pinnedMessage} -/// A pinned message in a chat. -/// -/// Used in [MessageWidgetContent]. Should not be used elsewhere. -/// {@endtemplate} -class PinnedMessage extends StatelessWidget { - /// {@macro pinnedMessage} - const PinnedMessage({ - super.key, - required this.pinnedBy, - required this.currentUser, - }); - - /// The [User] who pinned this message. - final User pinnedBy; - - /// The current [User]. - final User currentUser; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const StreamSvgIcon( - size: 16, - icon: StreamSvgIcons.pin, - ), - const SizedBox( - width: 4, - ), - Text( - context.translations.pinnedByUserText( - pinnedBy: pinnedBy, - currentUser: currentUser, - ), - style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, - fontSize: 13, - fontWeight: FontWeight.w400, - ), - ), - ], - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart b/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart deleted file mode 100644 index b6b7837b98..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/quoted_message.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template quotedMessage} -/// A quoted message in a chat. -/// -/// Used in [QuotedMessageCard]. Should not be used elsewhere. -/// {@endtemplate} -class QuotedMessage extends StatelessWidget { - /// {@macro quotedMessage} - const QuotedMessage({ - super.key, - required this.message, - required this.hasNonUrlAttachments, - this.textBuilder, - }); - - /// {@macro message} - final Message message; - - /// {@macro hasNonUrlAttachments} - final bool hasNonUrlAttachments; - - /// {@macro textBuilder} - final Widget Function(BuildContext, Message)? textBuilder; - - @override - Widget build(BuildContext context) { - final streamChat = StreamChat.of(context); - final chatThemeData = StreamChatTheme.of(context); - - final isMyMessage = message.user?.id == streamChat.currentUser?.id; - final isMyQuotedMessage = - message.quotedMessage?.user?.id == streamChat.currentUser?.id; - return StreamQuotedMessageWidget( - message: message.quotedMessage!, - messageTheme: isMyMessage - ? chatThemeData.otherMessageTheme - : chatThemeData.ownMessageTheme, - reverse: !isMyQuotedMessage, - textBuilder: textBuilder, - padding: EdgeInsets.only( - right: 8, - left: 8, - top: 8, - bottom: hasNonUrlAttachments ? 8 : 0, - ), - ); - } -} 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_bubble.dart b/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart deleted file mode 100644 index 82e5b34d02..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/reactions/reaction_bubble.dart +++ /dev/null @@ -1,335 +0,0 @@ -import 'dart:math'; - -import 'package:collection/collection.dart' show IterableExtension; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template streamReactionBubble} -/// Creates a reaction bubble that displays over messages. -/// {@endtemplate} -class StreamReactionBubble extends StatelessWidget { - /// {@macro streamReactionBubble} - const StreamReactionBubble({ - super.key, - required this.reactions, - required this.borderColor, - required this.backgroundColor, - required this.maskColor, - this.reverse = false, - this.flipTail = false, - this.highlightOwnReactions = true, - this.tailCirclesSpacing = 0, - }); - - /// Reactions to show - final List reactions; - - /// Border color of bubble - final Color borderColor; - - /// Background color of bubble - final Color backgroundColor; - - /// Mask color - final Color maskColor; - - /// Reverse for other side - final bool reverse; - - /// Reverse tail for other side - final bool flipTail; - - /// Flag for highlighting own reactions - final bool highlightOwnReactions; - - /// Spacing for tail circles - final double tailCirclesSpacing; - - @override - Widget build(BuildContext context) { - final reactionIcons = StreamChatConfiguration.of(context).reactionIcons; - final totalReactions = reactions.length; - final offset = - totalReactions > 1 ? 16.0.mirrorConditionally(flipTail) : 2.0; - return Stack( - alignment: Alignment.center, - children: [ - Transform.translate( - offset: Offset(-offset, 0), - child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: maskColor, - borderRadius: const BorderRadius.all(Radius.circular(16)), - ), - child: Container( - padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: totalReactions > 1 ? 4.0 : 0, - ), - decoration: BoxDecoration( - border: Border.all( - color: borderColor, - ), - color: backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(14)), - ), - 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(), - ], - ), - ), - ), - ), - ), - Positioned( - bottom: 2, - left: reverse ? null : 13, - right: reverse ? 13 : null, - child: _buildReactionsTail(context), - ), - ], - ); - } - - Widget _buildReaction( - List reactionIcons, - Reaction reaction, - BuildContext context, - ) { - final reactionIcon = reactionIcons.firstWhereOrNull( - (r) => r.type == reaction.type, - ); - - 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, - ), - ); - } - - Widget _buildReactionsTail(BuildContext context) { - final tail = CustomPaint( - painter: ReactionBubblePainter( - backgroundColor, - borderColor, - maskColor, - tailCirclesSpace: tailCirclesSpacing, - flipTail: !flipTail, - numberOfReactions: reactions.length, - ), - ); - return tail; - } -} - -/// Painter widget for a reaction bubble -class ReactionBubblePainter extends CustomPainter { - /// Constructor for creating a [ReactionBubblePainter] - ReactionBubblePainter( - this.color, - this.borderColor, - this.maskColor, { - this.tailCirclesSpace = 0, - this.flipTail = false, - this.numberOfReactions = 0, - }); - - /// Color of bubble - final Color color; - - /// Border color of bubble - final Color borderColor; - - /// Mask color - final Color maskColor; - - /// Tail circle space - final double tailCirclesSpace; - - /// Flip tail - final bool flipTail; - - /// Number of reactions on the page - final int numberOfReactions; - - @override - void paint(Canvas canvas, Size size) { - _drawOvalMask(size, canvas); - - _drawMask(size, canvas); - - _drawOval(size, canvas); - - _drawOvalBorder(size, canvas); - - _drawArc(size, canvas); - - _drawBorder(size, canvas); - } - - void _drawOvalMask(Size size, Canvas canvas) { - final paint = Paint() - ..color = maskColor - ..style = PaintingStyle.fill; - - final path = Path() - ..addOval( - Rect.fromCircle( - center: const Offset(4, 3).mirrorConditionally(flipTail) + - Offset(tailCirclesSpace, tailCirclesSpace) - .mirrorConditionally(flipTail), - radius: 4, - ), - ); - canvas.drawPath(path, paint); - } - - void _drawOvalBorder(Size size, Canvas canvas) { - final paint = Paint() - ..color = borderColor - ..strokeWidth = 1 - ..style = PaintingStyle.stroke; - - final path = Path() - ..addOval( - Rect.fromCircle( - center: const Offset(4, 3).mirrorConditionally(flipTail) + - Offset(tailCirclesSpace, tailCirclesSpace) - .mirrorConditionally(flipTail), - radius: 2, - ), - ); - canvas.drawPath(path, paint); - } - - void _drawOval(Size size, Canvas canvas) { - final paint = Paint() - ..color = color - ..strokeWidth = 1; - - final path = Path() - ..addOval(Rect.fromCircle( - center: const Offset(4, 3).mirrorConditionally(flipTail) + - Offset(tailCirclesSpace, tailCirclesSpace) - .mirrorConditionally(flipTail), - radius: 2, - )); - canvas.drawPath(path, paint); - } - - void _drawBorder(Size size, Canvas canvas) { - final paint = Paint() - ..color = borderColor - ..strokeWidth = 1 - ..style = PaintingStyle.stroke; - - const dy = -2.2; - final startAngle = flipTail ? -0.1 : 1.1; - final sweepAngle = flipTail ? -1.2 : (numberOfReactions > 1 ? 1.2 : 0.9); - final path = Path() - ..addArc( - Rect.fromCircle( - center: const Offset(1, dy).mirrorConditionally(flipTail), - radius: 4, - ), - -pi * startAngle, - -pi / sweepAngle, - ); - canvas.drawPath(path, paint); - } - - void _drawArc(Size size, Canvas canvas) { - final paint = Paint() - ..color = color - ..strokeWidth = 1; - - const dy = -2.2; - final startAngle = flipTail ? -0.0 : 1.0; - final sweepAngle = flipTail ? -1.3 : 1.3; - final path = Path() - ..addArc( - Rect.fromCircle( - center: const Offset(1, dy).mirrorConditionally(flipTail), - radius: 4, - ), - -pi * startAngle, - -pi * sweepAngle, - ); - canvas.drawPath(path, paint); - } - - void _drawMask(Size size, Canvas canvas) { - final paint = Paint() - ..color = maskColor - ..strokeWidth = 1 - ..style = PaintingStyle.fill; - - const dy = -2.2; - final startAngle = flipTail ? -0.1 : 1.1; - final sweepAngle = flipTail ? -1.2 : 1.2; - final path = Path() - ..addArc( - Rect.fromCircle( - center: const Offset(1, dy).mirrorConditionally(flipTail), - radius: 6, - ), - -pi * startAngle, - -pi / sweepAngle, - ); - canvas.drawPath(path, paint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) => true; -} - -/// Extension on [Offset] -extension YTransformer on Offset { - /// Flips x coordinate when flip is true - // ignore: avoid_positional_boolean_parameters - Offset mirrorConditionally(bool flip) => Offset(flip ? -dx : dx, dy); -} - -/// Extension on [Offset] -extension IntTransformer on double { - /// Flips x coordinate when flip is true - // ignore: avoid_positional_boolean_parameters - double mirrorConditionally(bool flip) => flip ? -this : this; -} 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/message_widget/sending_indicator_builder.dart b/packages/stream_chat_flutter/lib/src/message_widget/sending_indicator_builder.dart deleted file mode 100644 index cb42429007..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/sending_indicator_builder.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template sendingIndicatorWrapper} -/// Helper widget for building a [StreamSendingIndicator]. -/// -/// Used in [BottomRow]. Should not be used elsewhere. -/// {@endtemplate} -class SendingIndicatorBuilder extends StatelessWidget { - /// {@macro sendingIndicatorWrapper} - const SendingIndicatorBuilder({ - super.key, - required this.messageTheme, - required this.message, - required this.hasNonUrlAttachments, - required this.streamChat, - required this.streamChatTheme, - this.channel, - }); - - /// {@macro messageTheme} - final StreamMessageThemeData messageTheme; - - /// {@macro message} - final Message message; - - /// {@macro hasNonUrlAttachments} - final bool hasNonUrlAttachments; - - /// {@macro streamChat} - final StreamChatState streamChat; - - /// {@macro streamChatThemeData} - final StreamChatThemeData streamChatTheme; - - /// {@macro channel} - final Channel? channel; - - @override - Widget build(BuildContext context) { - final style = messageTheme.createdAtStyle; - final channel = this.channel ?? StreamChannel.of(context).channel; - final memberCount = channel.memberCount ?? 0; - - if (hasNonUrlAttachments && message.state.isOutgoing) { - final totalAttachments = message.attachments.length; - final attachmentsToUpload = message.attachments.where((it) { - return !it.uploadState.isSuccess; - }); - - if (attachmentsToUpload.isNotEmpty) { - return Text( - context.translations.attachmentsUploadProgressText( - remaining: attachmentsToUpload.length, - total: totalAttachments, - ), - style: style, - ); - } - } - - return BetterStreamBuilder>( - stream: channel.state?.readStream, - initialData: channel.state?.read, - builder: (context, data) { - final readList = data.readsOf(message: message); - final isMessageRead = readList.isNotEmpty; - - final deliveriesList = data.deliveriesOf(message: message); - final isMessageDelivered = deliveriesList.isNotEmpty; - - Widget child = StreamSendingIndicator( - message: message, - isMessageRead: isMessageRead, - isMessageDelivered: isMessageDelivered, - size: style?.fontSize, - ); - - if (isMessageRead) { - child = Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (memberCount > 2) - Text( - readList.length.toString(), - style: style?.copyWith( - color: streamChatTheme.colorTheme.accentPrimary, - ), - ), - const SizedBox(width: 2), - child, - ], - ); - } - - return child; - }, - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/text_bubble.dart b/packages/stream_chat_flutter/lib/src/message_widget/text_bubble.dart deleted file mode 100644 index 7874319ab7..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/text_bubble.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template textBubble} -/// The bubble around a [StreamMessageText]. -/// -/// Used in [MessageCard]. Should not be used elsewhere. -/// {@endtemplate} -class TextBubble extends StatelessWidget { - /// {@macro textBubble} - const TextBubble({ - super.key, - required this.message, - required this.isOnlyEmoji, - required this.textPadding, - required this.messageTheme, - required this.hasUrlAttachments, - required this.hasQuotedMessage, - this.textBuilder, - this.onLinkTap, - this.onMentionTap, - }); - - /// {@macro message} - final Message message; - - /// {@macro isOnlyEmoji} - final bool isOnlyEmoji; - - /// {@macro textPadding} - final EdgeInsets textPadding; - - /// {@macro textBuilder} - final Widget Function(BuildContext, Message)? textBuilder; - - /// {@macro onLinkTap} - final void Function(String)? onLinkTap; - - /// {@macro onMentionTap} - final void Function(User)? onMentionTap; - - /// {@macro messageTheme} - final StreamMessageThemeData messageTheme; - - /// {@macro hasUrlAttachments} - final bool hasUrlAttachments; - - /// {@macro hasQuotedMessage} - final bool hasQuotedMessage; - - @override - Widget build(BuildContext context) { - if (message.text?.trim().isEmpty ?? true) return const Empty(); - return Padding( - padding: isOnlyEmoji ? EdgeInsets.zero : textPadding, - child: textBuilder != null - ? textBuilder!(context, message) - : StreamMessageText( - onLinkTap: onLinkTap, - message: message, - onMentionTap: onMentionTap, - messageTheme: isOnlyEmoji - ? messageTheme.copyWith( - messageTextStyle: messageTheme.messageTextStyle!.copyWith( - fontSize: 42, - ), - ) - : messageTheme, - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/thread_painter.dart b/packages/stream_chat_flutter/lib/src/message_widget/thread_painter.dart deleted file mode 100644 index 09bb63c93f..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/thread_painter.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template threadReplyPainter} -/// A custom painter used to render thread replies. -/// -/// Used in [BottomRow]. -/// {@endtemplate} -class ThreadReplyPainter extends CustomPainter { - /// {@macro threadReplyPainter} - const ThreadReplyPainter({ - this.context, - required this.color, - this.reverse = false, - }); - - /// The color to paint the thread reply with. - final Color? color; - - /// The [BuildContext] to use to retrieve the [StreamChatTheme]. - final BuildContext? context; - - /// {@macro reverse} - final bool reverse; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color ?? StreamChatTheme.of(context!).colorTheme.disabled - ..style = PaintingStyle.stroke - ..strokeWidth = 1 - ..strokeCap = StrokeCap.round; - - final path = Path() - ..moveTo(reverse ? size.width : 0, 0) - ..quadraticBezierTo( - reverse ? size.width : 0, - size.height * 0.38, - reverse ? size.width : 0, - size.height * 0.5, - ) - ..quadraticBezierTo( - reverse ? size.width : 0, - size.height, - reverse ? 0 : size.width, - size.height, - ); - canvas.drawPath(path, paint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/thread_participants.dart b/packages/stream_chat_flutter/lib/src/message_widget/thread_participants.dart deleted file mode 100644 index 5068e2931e..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/thread_participants.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template threadParticipants} -/// Shows the users participating in a thread. -/// -/// Used in [BottomRow]. -/// {@endtemplate} -class ThreadParticipants extends StatelessWidget { - /// {@macro threadParticipants} - const ThreadParticipants({ - super.key, - required StreamChatThemeData streamChatTheme, - required this.threadParticipants, - }) : _streamChatTheme = streamChatTheme; - - /// {@macro streamChatThemeData} - final StreamChatThemeData _streamChatTheme; - - /// The users participating in the thread. - final Iterable threadParticipants; - - @override - Widget build(BuildContext context) { - var padding = 0.0; - return Stack( - children: threadParticipants.map((user) { - padding += 8.0; - return Positioned( - right: padding - 8, - bottom: 0, - top: 0, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: _streamChatTheme.colorTheme.barsBg, - ), - padding: const EdgeInsets.all(1), - child: StreamUserAvatar( - user: user, - constraints: BoxConstraints.tight(const Size.fromRadius(7)), - showOnlineStatus: false, - ), - ), - ); - }).toList(), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/user_avatar_transform.dart b/packages/stream_chat_flutter/lib/src/message_widget/user_avatar_transform.dart deleted file mode 100644 index 275d63e05e..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/user_avatar_transform.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template userAvatarTransform} -/// Transforms a [StreamUserAvatar] according to the specified translation. -/// -/// Used in [MessageWidgetContent]. -/// {@endtemplate} -class UserAvatarTransform extends StatelessWidget { - /// {@macro userAvatarTransform} - const UserAvatarTransform({ - super.key, - required this.translateUserAvatar, - required this.messageTheme, - required this.message, - this.userAvatarBuilder, - this.onUserAvatarTap, - }); - - /// {@macro translateUserAvatar} - final bool translateUserAvatar; - - /// {@macro messageTheme} - final StreamMessageThemeData messageTheme; - - /// {@macro userAvatarBuilder} - final Widget Function(BuildContext, User)? userAvatarBuilder; - - /// {@macro message} - final Message message; - - /// {@macro onUserAvatarTap} - final void Function(User)? onUserAvatarTap; - - @override - Widget build(BuildContext context) { - return Transform.translate( - offset: Offset( - 0, - translateUserAvatar - ? (messageTheme.avatarTheme?.constraints.maxHeight ?? 40) / 2 - : 0, - ), - child: userAvatarBuilder?.call(context, message.user!) ?? - StreamUserAvatar( - user: message.user!, - onTap: onUserAvatarTap, - constraints: messageTheme.avatarTheme!.constraints, - borderRadius: messageTheme.avatarTheme!.borderRadius, - showOnlineStatus: false, - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_widget/username.dart b/packages/stream_chat_flutter/lib/src/message_widget/username.dart deleted file mode 100644 index fad0ff6fb1..0000000000 --- a/packages/stream_chat_flutter/lib/src/message_widget/username.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// {@template username} -/// Displays the username of a particular message's sender. -/// {@endtemplate} -class Username extends StatelessWidget { - /// {@macro username} - const Username({ - super.key, - required this.message, - required this.messageTheme, - }); - - /// {@macro message} - final Message message; - - /// {@macro messageTheme} - final StreamMessageThemeData messageTheme; - - @override - Widget build(BuildContext context) { - return Text( - message.user?.name ?? '', - maxLines: 1, - key: key, - style: messageTheme.messageAuthorStyle, - overflow: TextOverflow.ellipsis, - ); - } -} 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..0a674e949d --- /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/audio_waveform.dart b/packages/stream_chat_flutter/lib/src/misc/audio_waveform.dart deleted file mode 100644 index e35f9277a5..0000000000 --- a/packages/stream_chat_flutter/lib/src/misc/audio_waveform.dart +++ /dev/null @@ -1,487 +0,0 @@ -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/theme/audio_waveform_slider_theme.dart'; -import 'package:stream_chat_flutter/src/theme/audio_waveform_theme.dart'; - -const _kAudioWaveformSliderThumbWidth = 4.0; -const _kAudioWaveformSliderThumbHeight = 28.0; - -/// {@template streamAudioWaveformSlider} -/// A widget that displays an audio waveform and allows the user to interact -/// with it using a slider. -/// {@endtemplate} -class StreamAudioWaveformSlider extends StatefulWidget { - /// {@macro streamAudioWaveformSlider} - const StreamAudioWaveformSlider({ - super.key, - required this.waveform, - this.onChangeStart, - required this.onChanged, - this.onChangeEnd, - this.limit = 100, - this.color, - this.progress = 0, - this.progressColor, - this.minBarHeight, - this.spacingRatio, - this.heightScale, - this.inverse = true, - this.thumbColor, - this.thumbBorderColor, - }); - - /// The waveform data to be drawn. - /// - /// Note: The values should be between 0 and 1. - final List waveform; - - /// Called when the thumb starts being dragged. - final ValueChanged? onChangeStart; - - /// Called while the thumb is being dragged. - final ValueChanged? onChanged; - - /// Called when the thumb stops being dragged. - final ValueChanged? onChangeEnd; - - /// The color of the wave bars. - /// - /// Defaults to [StreamAudioWaveformSliderThemeData.color]. - final Color? color; - - /// The number of wave bars that will be draw in the screen. When the length - /// of [waveform] is bigger than [limit] only the X last bars will be shown. - /// - /// Defaults to 100. - final int limit; - - /// The progress of the audio track. Used to show the progress of the audio. - /// - /// Defaults to 0. - final double progress; - - /// The color of the progressed wave bars. - /// - /// Defaults to [StreamAudioWaveformSliderThemeData.progressColor]. - final Color? progressColor; - - /// The minimum height of the bars. - /// - /// Defaults to [StreamAudioWaveformSliderThemeData.minBarHeight]. - final double? minBarHeight; - - /// The ratio of the spacing between the bars. - /// - /// Defaults to [StreamAudioWaveformSliderThemeData.spacingRatio]. - final double? spacingRatio; - - /// The scale of the height of the bars. - /// - /// Defaults to [StreamAudioWaveformSliderThemeData.heightScale]. - final double? heightScale; - - /// If true, the bars grow from right to left otherwise they grow from left - /// to right. - /// - /// Defaults to true. - final bool inverse; - - /// The color of the slider thumb. - /// - /// Defaults to [StreamAudioWaveformSliderThemeData.thumbColor]. - final Color? thumbColor; - - /// The color of the slider thumb border. - /// - /// Defaults to [StreamAudioWaveformSliderThemeData.thumbBorderColor]. - final Color? thumbBorderColor; - - @override - State createState() => - _StreamAudioWaveformSliderState(); -} - -class _StreamAudioWaveformSliderState extends State { - @override - Widget build(BuildContext context) { - final theme = StreamAudioWaveformSliderTheme.of(context); - final waveformTheme = theme.audioWaveformTheme; - - final color = widget.color ?? waveformTheme!.color!; - final progressColor = widget.progressColor ?? waveformTheme!.progressColor!; - final minBarHeight = widget.minBarHeight ?? waveformTheme!.minBarHeight!; - final spacingRatio = widget.spacingRatio ?? waveformTheme!.spacingRatio!; - final heightScale = widget.heightScale ?? waveformTheme!.heightScale!; - final thumbColor = widget.thumbColor ?? theme.thumbColor!; - final thumbBorderColor = widget.thumbBorderColor ?? theme.thumbBorderColor!; - - return HorizontalSlider( - onChangeStart: widget.onChangeStart, - onChanged: widget.onChanged, - onChangeEnd: widget.onChangeEnd, - child: LayoutBuilder( - builder: (context, constraints) => Stack( - fit: StackFit.expand, - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - StreamAudioWaveform( - waveform: widget.waveform, - limit: widget.limit, - color: color, - progress: widget.progress, - progressColor: progressColor, - minBarHeight: minBarHeight, - spacingRatio: spacingRatio, - heightScale: heightScale, - inverse: widget.inverse, - ), - Builder( - // Just using it for the calculation of the thumb position. - builder: (context) { - final progressWidth = constraints.maxWidth * widget.progress; - return AnimatedPositioned( - curve: const ElasticOutCurve(1.05), - duration: const Duration(milliseconds: 300), - left: progressWidth - _kAudioWaveformSliderThumbWidth / 2, - child: StreamAudioWaveformSliderThumb( - color: thumbColor, - borderColor: thumbBorderColor, - height: constraints.maxHeight, - ), - ); - }, - ), - ], - ), - ), - ); - } -} - -/// {@template streamAudioWaveformSliderThumb} -/// A widget that represents the thumb of the [StreamAudioWaveformSlider]. -/// {@endtemplate} -class StreamAudioWaveformSliderThumb extends StatelessWidget { - /// {@macro streamAudioWaveformSliderThumb} - const StreamAudioWaveformSliderThumb({ - super.key, - this.width = _kAudioWaveformSliderThumbWidth, - this.height = _kAudioWaveformSliderThumbHeight, - this.color = Colors.white, - this.borderColor = const Color(0xffecebeb), - }); - - /// The width of the thumb. - final double width; - - /// The height of the thumb. - final double height; - - /// The color of the thumb. - final Color color; - - /// The border color of the thumb. - final Color borderColor; - - @override - Widget build(BuildContext context) { - return Container( - width: width, - height: height, - decoration: BoxDecoration( - color: color, - border: Border.all( - color: borderColor, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: BorderRadius.circular(2), - ), - ); - } -} - -/// {@template streamAudioWaveform} -/// A widget that displays an audio waveform. -/// -/// The waveform is drawn using the [waveform] data. The waveform is drawn -/// horizontally and the bars grow from right to left. -/// {@endtemplate} -class StreamAudioWaveform extends StatelessWidget { - /// {@macro streamAudioWaveform} - const StreamAudioWaveform({ - super.key, - required this.waveform, - this.limit = 100, - this.color, - this.progress = 0, - this.progressColor, - this.minBarHeight, - this.spacingRatio, - this.heightScale, - this.inverse = true, - }); - - /// The waveform data to be drawn. - /// - /// Note: The values should be between 0 and 1. - final List waveform; - - /// The color of the wave bars. - /// - /// Defaults to [StreamAudioWaveformThemeData.color]. - final Color? color; - - /// The number of wave bars that will be draw in the screen. When the length - /// of [waveform] is bigger than [limit] only the X last bars will be shown. - /// - /// Defaults to 100. - final int limit; - - /// The progress of the audio track. Used to show the progress of the audio. - /// - /// Defaults to 0. - final double progress; - - /// The color of the progressed wave bars. - /// - /// Defaults to [StreamAudioWaveformThemeData.progressColor]. - final Color? progressColor; - - /// The minimum height of the bars. - /// - /// Defaults to [StreamAudioWaveformThemeData.minBarHeight]. - final double? minBarHeight; - - /// The ratio of the spacing between the bars. - /// - /// Defaults to [StreamAudioWaveformThemeData.spacingRatio]. - final double? spacingRatio; - - /// The scale of the height of the bars. - /// - /// Defaults to [StreamAudioWaveformThemeData.heightScale]. - final double? heightScale; - - /// If true, the bars grow from right to left otherwise they grow from left - /// to right. - /// - /// Defaults to true. - final bool inverse; - - @override - Widget build(BuildContext context) { - final theme = StreamAudioWaveformTheme.of(context); - - final color = this.color ?? theme.color!; - final progressColor = this.progressColor ?? theme.progressColor!; - final minBarHeight = this.minBarHeight ?? theme.minBarHeight!; - final spacingRatio = this.spacingRatio ?? theme.spacingRatio!; - final heightScale = this.heightScale ?? theme.heightScale!; - - return CustomPaint( - willChange: true, - painter: _WaveformPainter( - waveform: waveform.reversed, - limit: limit, - color: color, - progress: progress, - progressColor: progressColor, - minBarHeight: minBarHeight, - spacingRatio: spacingRatio, - heightScale: heightScale, - inverse: inverse, - ), - ); - } -} - -class _WaveformPainter extends CustomPainter { - _WaveformPainter({ - required Iterable waveform, - this.limit = 100, - this.color = const Color(0xff7E828B), - this.progress = 0, - this.progressColor = const Color(0xff005FFF), - this.minBarHeight = 2, - double spacingRatio = 0.3, - this.heightScale = 1, - this.inverse = true, - }) : waveform = [ - ...waveform.take(limit), - if (waveform.length < limit) - // Fill the remaining bars with 0 value - ...List.filled(limit - waveform.length, 0) - ], - spacingRatio = spacingRatio.clamp(0, 1); - - final List waveform; - final Color color; - final int limit; - final double progress; - final Color progressColor; - final double minBarHeight; - final double spacingRatio; - final bool inverse; - final double heightScale; - - @override - void paint(Canvas canvas, Size size) { - final canvasWidth = size.width; - final canvasHeight = size.height; - - // The total spacing between the bars in the canvas. - final spacingWidth = canvasWidth * spacingRatio; - final barsWidth = canvasWidth - spacingWidth; - final barWidth = barsWidth / limit; - final barSpacing = spacingWidth / (limit - 1); - final progressWidth = progress * canvasWidth; - - void _paintBar(int index, double barValue) { - var dx = index * (barWidth + barSpacing) + barWidth / 2; - if (inverse) dx = canvasWidth - dx; - final dy = canvasHeight / 2; - - final barHeight = math.max(barValue * canvasHeight, minBarHeight); - - final rect = RRect.fromRectAndRadius( - Rect.fromCenter( - center: Offset(dx, dy), - width: barWidth, - height: barHeight, - ), - const Radius.circular(2), - ); - - final waveColor = switch (dx <= progressWidth) { - true => progressColor, - false => color, - }; - - final wavePaint = Paint() - ..color = waveColor - ..strokeCap = StrokeCap.round; - - canvas.drawRRect(rect, wavePaint); - } - - // Paint all the bars - waveform.forEachIndexed(_paintBar); - } - - @override - bool shouldRepaint(covariant _WaveformPainter oldDelegate) => - !const ListEquality().equals(waveform, oldDelegate.waveform) || - color != oldDelegate.color || - limit != oldDelegate.limit || - progress != oldDelegate.progress || - progressColor != oldDelegate.progressColor || - minBarHeight != oldDelegate.minBarHeight || - spacingRatio != oldDelegate.spacingRatio || - heightScale != oldDelegate.heightScale || - inverse != oldDelegate.inverse; -} - -/// {@template horizontalSlider} -/// A widget that allows interactive horizontal sliding gestures. -/// -/// The `HorizontalSlider` widget wraps a child widget and allows users to -/// perform sliding gestures horizontally. It can be configured with callbacks -/// to notify the parent widget about the changes in the horizontal value. -/// {@endtemplate} -class HorizontalSlider extends StatefulWidget { - /// Creates a horizontal slider. - const HorizontalSlider({ - super.key, - required this.child, - required this.onChanged, - this.onChangeStart, - this.onChangeEnd, - }); - - /// The child widget wrapped by the slider. - final Widget child; - - /// Called when the horizontal value starts changing. - final ValueChanged? onChangeStart; - - /// Called when the horizontal value changes. - final ValueChanged? onChanged; - - /// Called when the horizontal value stops changing. - final ValueChanged? onChangeEnd; - - @override - State createState() => _HorizontalSliderState(); -} - -class _HorizontalSliderState extends State { - bool _active = false; - - /// Returns true if the slider is interactive. - bool get isInteractive => widget.onChanged != null; - - /// Converts the visual position to a value based on the text direction. - double _getValueFromVisualPosition(double visualPosition) { - final textDirection = Directionality.of(context); - final value = switch (textDirection) { - TextDirection.rtl => 1.0 - visualPosition, - TextDirection.ltr => visualPosition, - }; - - return clampDouble(value, 0, 1); - } - - /// Converts the local position to a horizontal value. - double _getValueFromLocalPosition(Offset globalPosition) { - final box = context.findRenderObject()! as RenderBox; - final localPosition = box.globalToLocal(globalPosition); - final visualPosition = localPosition.dx / box.size.width; - return _getValueFromVisualPosition(visualPosition); - } - - void _handleDragStart(DragStartDetails details) { - if (!_active && isInteractive) { - _active = true; - final value = _getValueFromLocalPosition(details.globalPosition); - widget.onChangeStart?.call(value); - } - } - - void _handleDragUpdate(DragUpdateDetails details) { - _handleHorizontalDrag(details.globalPosition); - } - - void _handleDragEnd(DragEndDetails details) { - if (!mounted) return; - - if (_active && mounted) { - final value = _getValueFromLocalPosition(details.globalPosition); - widget.onChangeEnd?.call(value); - _active = false; - } - } - - /// Handles the sliding gesture. - void _handleHorizontalDrag(Offset globalPosition) { - if (!mounted) return; - - if (isInteractive) { - final value = _getValueFromLocalPosition(globalPosition); - widget.onChanged?.call(value); - } - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onHorizontalDragStart: _handleDragStart, - onHorizontalDragUpdate: _handleDragUpdate, - onHorizontalDragEnd: _handleDragEnd, - child: widget.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..e927d39b04 100644 --- a/packages/stream_chat_flutter/lib/src/misc/back_button.dart +++ b/packages/stream_chat_flutter/lib/src/misc/back_button.dart @@ -26,9 +26,9 @@ class StreamBackButton extends StatelessWidget { Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); - Widget icon = StreamSvgIcon( - size: 24, - icon: StreamSvgIcons.left, + Widget icon = Icon( + context.streamIcons.arrowLeft20, + size: 20, color: theme.colorTheme.textHighEmphasis, ); @@ -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/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/misc/connection_status_builder.dart index e6703420b4..3a44e1465e 100644 --- a/packages/stream_chat_flutter/lib/src/misc/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/misc/connection_status_builder.dart @@ -29,13 +29,11 @@ class StreamConnectionStatusBuilder extends StatelessWidget { final WidgetBuilder? loadingBuilder; /// The builder that will be used in case of data - final Widget Function(BuildContext context, ConnectionStatus status) - statusBuilder; + final Widget Function(BuildContext context, ConnectionStatus status) statusBuilder; @override Widget build(BuildContext context) { - final stream = connectionStatusStream ?? - StreamChat.of(context).client.wsConnectionStatusStream; + final stream = connectionStatusStream ?? StreamChat.of(context).client.wsConnectionStatusStream; final client = StreamChat.of(context).client; return BetterStreamBuilder( initialData: client.wsConnectionStatus, diff --git a/packages/stream_chat_flutter/lib/src/misc/date_divider.dart b/packages/stream_chat_flutter/lib/src/misc/date_divider.dart index b927fca54f..7726f612c8 100644 --- a/packages/stream_chat_flutter/lib/src/misc/date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/misc/date_divider.dart @@ -26,19 +26,19 @@ class StreamDateDivider extends StatelessWidget { @override Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + return Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1), decoration: BoxDecoration( - color: chatThemeData.colorTheme.overlayDark, + color: colorScheme.backgroundSurfaceSubtle, borderRadius: BorderRadius.circular(8), ), child: StreamTimestamp( date: dateTime.toLocal(), - style: chatThemeData.textTheme.footnote.copyWith( - color: chatThemeData.colorTheme.barsBg, - ), + style: textTheme.metadataEmphasis.copyWith(color: colorScheme.textSecondary), formatter: (context, date) { if (formatter case final formatter?) { final timestamp = formatter.call(context, date); diff --git a/packages/stream_chat_flutter/lib/src/misc/flex_grid.dart b/packages/stream_chat_flutter/lib/src/misc/flex_grid.dart index 5210cf9aa1..ca0e8955a6 100644 --- a/packages/stream_chat_flutter/lib/src/misc/flex_grid.dart +++ b/packages/stream_chat_flutter/lib/src/misc/flex_grid.dart @@ -80,19 +80,19 @@ class FlexGrid extends StatelessWidget { this.reverse = false, this.spacing = 2.0, this.runSpacing = 2.0, - }) : assert( - pattern.count == children.length, - 'The number of children must match the number of cells in the matrix', - ), - assert( - maxChildren == null || maxChildren <= pattern.count, - 'The number of maxChildren must be less than or equal to the number ' - 'of cells in the matrix', - ), - assert( - maxChildren == null || overlayBuilder != null, - 'overlayBuilder must be provided when maxChildren is not null', - ); + }) : assert( + pattern.count == children.length, + 'The number of children must match the number of cells in the matrix', + ), + assert( + maxChildren == null || maxChildren <= pattern.count, + 'The number of maxChildren must be less than or equal to the number ' + 'of cells in the matrix', + ), + assert( + maxChildren == null || overlayBuilder != null, + 'overlayBuilder must be provided when maxChildren is not null', + ); /// The pattern of the grid. /// 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..c95ba8fae1 --- /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/giphy_chip.dart b/packages/stream_chat_flutter/lib/src/misc/giphy_chip.dart index c956517ba5..1cdc3ced6c 100644 --- a/packages/stream_chat_flutter/lib/src/misc/giphy_chip.dart +++ b/packages/stream_chat_flutter/lib/src/misc/giphy_chip.dart @@ -1,7 +1,7 @@ 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'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template giphy_chip} /// Simple widget which displays a Giphy attribution chip. @@ -21,9 +21,9 @@ class GiphyChip extends StatelessWidget { padding: const EdgeInsets.fromLTRB(4, 4, 8, 4), child: Row( children: [ - StreamSvgIcon( + Icon( + context.streamIcons.bolt16, size: 16, - icon: StreamSvgIcons.lightning, color: colorTheme.barsBg, ), Text( diff --git a/packages/stream_chat_flutter/lib/src/misc/gradient_box_border.dart b/packages/stream_chat_flutter/lib/src/misc/gradient_box_border.dart index 7a881edd16..2ecf925e5d 100644 --- a/packages/stream_chat_flutter/lib/src/misc/gradient_box_border.dart +++ b/packages/stream_chat_flutter/lib/src/misc/gradient_box_border.dart @@ -4,15 +4,19 @@ import 'dart:ui' as ui show lerpDouble; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -const _kDefaultGradient = LinearGradient(colors: [ - Color(0xFF000000), - Color(0xFF000000), -]); - -const _kTransparentGradient = LinearGradient(colors: [ - Color(0x00000000), - Color(0x00000000), -]); +const _kDefaultGradient = LinearGradient( + colors: [ + Color(0xFF000000), + Color(0xFF000000), + ], +); + +const _kTransparentGradient = LinearGradient( + colors: [ + Color(0x00000000), + Color(0x00000000), + ], +); /// {@template gradientBoxBorder} /// A border that draws a gradient instead of a solid color. @@ -109,8 +113,7 @@ class GradientBoxBorder extends BoxBorder { /// Two sides can be merged if one or both are zero-width with /// [GradientBoxBorder.none], or if they both have the same gradient and style bool canMerge(GradientBoxBorder b) { - if ((style == BorderStyle.none && width == 0.0) || - (b.style == BorderStyle.none && b.width == 0.0)) { + if ((style == BorderStyle.none && width == 0.0) || (b.style == BorderStyle.none && b.width == 0.0)) { return true; } return style == b.style && gradient == b.gradient; @@ -209,8 +212,7 @@ class GradientBoxBorder extends BoxBorder { /// Linearly interpolate between two gradient borders. /// /// {@macro dart.ui.shadow.lerp} - static GradientBoxBorder? lerp( - GradientBoxBorder? a, GradientBoxBorder? b, double t) { + static GradientBoxBorder? lerp(GradientBoxBorder? a, GradientBoxBorder? b, double t) { if (identical(a, b)) return a; if (a == null) return b!.scale(t); if (b == null) return a.scale(1.0 - t); @@ -270,10 +272,9 @@ class GradientBoxBorder extends BoxBorder { return switch (shape) { BoxShape.circle => _paintUniformBorderWithCircle(canvas, rect), BoxShape.rectangle => switch (borderRadius) { - final radius? when radius != BorderRadius.zero => - _paintUniformBorderWithRadius(canvas, rect, radius), - _ => _paintUniformBorderWithRectangle(canvas, rect), - }, + final radius? when radius != BorderRadius.zero => _paintUniformBorderWithRadius(canvas, rect, radius), + _ => _paintUniformBorderWithRectangle(canvas, rect), + }, }; } @@ -307,14 +308,16 @@ class GradientBoxBorder extends BoxBorder { Paint _getPaint(Rect rect) { return switch (style) { - BorderStyle.solid => Paint() - ..strokeWidth = width - ..style = PaintingStyle.stroke - ..shader = gradient.createShader(rect), - BorderStyle.none => Paint() - ..strokeWidth = 0.0 - ..style = PaintingStyle.stroke - ..shader = _kTransparentGradient.createShader(rect), + BorderStyle.solid => + Paint() + ..strokeWidth = width + ..style = PaintingStyle.stroke + ..shader = gradient.createShader(rect), + BorderStyle.none => + Paint() + ..strokeWidth = 0.0 + ..style = PaintingStyle.stroke + ..shader = _kTransparentGradient.createShader(rect), }; } diff --git a/packages/stream_chat_flutter/lib/src/misc/info_tile.dart b/packages/stream_chat_flutter/lib/src/misc/info_tile.dart index fd6106491a..0cd7ff9d52 100644 --- a/packages/stream_chat_flutter/lib/src/misc/info_tile.dart +++ b/packages/stream_chat_flutter/lib/src/misc/info_tile.dart @@ -50,13 +50,15 @@ class StreamInfoTile extends StatelessWidget { ), portalFollower: Container( height: 25, - color: backgroundColor ?? + color: + backgroundColor ?? // ignore: deprecated_member_use chatThemeData.colorTheme.textLowEmphasis.withOpacity(0.9), child: Center( child: Text( message, - style: textStyle ?? + style: + textStyle ?? chatThemeData.textTheme.body.copyWith( color: Colors.white, ), diff --git a/packages/stream_chat_flutter/lib/src/misc/markdown_message.dart b/packages/stream_chat_flutter/lib/src/misc/markdown_message.dart index 9b0926ed08..5977692118 100644 --- a/packages/stream_chat_flutter/lib/src/misc/markdown_message.dart +++ b/packages/stream_chat_flutter/lib/src/misc/markdown_message.dart @@ -87,22 +87,23 @@ class StreamMarkdownMessage extends StatelessWidget { syntaxHighlighter: syntaxHighlighter, builders: builders, paddingBuilders: paddingBuilders, - styleSheet: MarkdownStyleSheet.fromTheme( - themeData.copyWith( - textTheme: themeData.textTheme.apply( - bodyColor: messageTheme?.messageTextStyle?.color, - decoration: messageTheme?.messageTextStyle?.decoration, - decorationColor: messageTheme?.messageTextStyle?.decorationColor, - decorationStyle: messageTheme?.messageTextStyle?.decorationStyle, - fontFamily: messageTheme?.messageTextStyle?.fontFamily, - ), - ), - ) - .copyWith( - a: messageTheme?.messageLinksStyle, - p: messageTheme?.messageTextStyle, - ) - .merge(styleSheet), + styleSheet: + MarkdownStyleSheet.fromTheme( + themeData.copyWith( + textTheme: themeData.textTheme.apply( + bodyColor: messageTheme?.messageTextStyle?.color, + decoration: messageTheme?.messageTextStyle?.decoration, + decorationColor: messageTheme?.messageTextStyle?.decorationColor, + decorationStyle: messageTheme?.messageTextStyle?.decorationStyle, + fontFamily: messageTheme?.messageTextStyle?.fontFamily, + ), + ), + ) + .copyWith( + a: messageTheme?.messageLinksStyle, + p: messageTheme?.messageTextStyle, + ) + .merge(styleSheet), ); } } diff --git a/packages/stream_chat_flutter/lib/src/misc/option_list_tile.dart b/packages/stream_chat_flutter/lib/src/misc/option_list_tile.dart index 2c093c9db3..3307a31bbb 100644 --- a/packages/stream_chat_flutter/lib/src/misc/option_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/misc/option_list_tile.dart @@ -60,15 +60,13 @@ class StreamOptionListTile extends StatelessWidget { onTap: onTap, child: Row( children: [ - if (leading != null) - Center(child: leading) - else - const SizedBox(width: 16), + if (leading != null) Center(child: leading) else const SizedBox(width: 16), Expanded( flex: 4, child: Text( title, - style: titleTextStyle ?? + style: + titleTextStyle ?? (titleColor == null ? chatThemeData.textTheme.bodyBold : chatThemeData.textTheme.bodyBold.copyWith( diff --git a/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart b/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart deleted file mode 100644 index 60ac8fe9c2..0000000000 --- a/packages/stream_chat_flutter/lib/src/misc/reaction_icon.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.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, -); - -/// {@template streamReactionIcon} -/// Reaction icon data -/// {@endtemplate} -class StreamReactionIcon { - /// {@macro streamReactionIcon} - const StreamReactionIcon({ - required this.type, - required this.builder, - }); - - /// Type of reaction - final String type; - - /// {@macro reactionIconBuilder} - final ReactionIconBuilder builder; -} diff --git a/packages/stream_chat_flutter/lib/src/misc/reaction_icon_resolver.dart b/packages/stream_chat_flutter/lib/src/misc/reaction_icon_resolver.dart new file mode 100644 index 0000000000..777d631a54 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/misc/reaction_icon_resolver.dart @@ -0,0 +1,89 @@ +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// Maps reaction type strings to [StreamEmojiContent] models. +/// +/// [ReactionIconResolver] provides the set of supported and default reactions +/// and resolves each type into a content model that [StreamEmoji] can render. +/// The resolver returns pure data; the SDK owns rendering. +/// +/// See also: +/// +/// * [DefaultReactionIconResolver], the built-in implementation. +/// * [StreamEmojiContent], the sealed content model returned by [resolve]. +/// * [StreamChatConfigurationData.reactionIconResolver], where the resolver +/// is configured. +abstract class ReactionIconResolver { + /// Creates a [ReactionIconResolver]. + const ReactionIconResolver(); + + /// A small set of commonly used reaction types shown for quick access + /// in the reaction picker bar. + Set get defaultReactions; + + /// All supported reaction types, in display order. + /// + /// Iteration order is used as the reaction display order in default + /// components. Implementations should use a [LinkedHashSet] or + /// equivalent to preserve insertion order. + Set get supportedReactions; + + /// Returns the emoji code for the given reaction [type], or `null` + /// if the type is not supported. + String? emojiCode(String type); + + /// Resolves the given reaction [type] into a content model. + /// + /// Override to return [StreamImageEmoji] for custom emoji. + StreamEmojiContent resolve(String type); +} + +/// Default [ReactionIconResolver] backed by [streamSupportedEmojis]. +/// +/// {@tool snippet} +/// +/// Use custom image emoji (e.g. Twemoji) by extending and overriding +/// [resolve]: +/// +/// ```dart +/// class TwemojiReactionResolver extends DefaultReactionIconResolver { +/// @override +/// StreamEmojiContent resolve(String type) { +/// if (emojiCode(type) != null) { +/// return StreamImageEmoji( +/// url: Uri.parse('https://cdn.example.com/twemoji/$type.png'), +/// ); +/// } +/// return super.resolve(type); +/// } +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [ReactionIconResolver], the abstract contract. +/// * [streamSupportedEmojis], the emoji catalog used by this resolver. +class DefaultReactionIconResolver extends ReactionIconResolver { + /// Creates a [DefaultReactionIconResolver]. + const DefaultReactionIconResolver(); + + static const _defaultQuickReactions = {'like', 'haha', 'love', 'wow', 'sad'}; + + @override + Set get defaultReactions => _defaultQuickReactions; + + @override + Set get supportedReactions => streamSupportedEmojis.keys.toSet(); + + @override + String? emojiCode(String type) => streamSupportedEmojis[type]?.emoji; + + @override + StreamEmojiContent resolve(String type) { + if (emojiCode(type) case final emoji?) { + return StreamUnicodeEmoji(emoji); + } + + return const StreamUnicodeEmoji('❓'); + } +} diff --git a/packages/stream_chat_flutter/lib/src/misc/separated_reorderable_list_view.dart b/packages/stream_chat_flutter/lib/src/misc/separated_reorderable_list_view.dart index 24e09455d8..bd0e672eb6 100644 --- a/packages/stream_chat_flutter/lib/src/misc/separated_reorderable_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/misc/separated_reorderable_list_view.dart @@ -33,53 +33,51 @@ class SeparatedReorderableListView extends ReorderableListView { super.restorationId, super.clipBehavior, }) : super.builder( - buildDefaultDragHandles: false, - itemCount: math.max(0, itemCount * 2 - 1), - itemBuilder: (BuildContext context, int index) { - final itemIndex = index ~/ 2; - if (index.isEven) { - final listItem = itemBuilder(context, itemIndex); - return ReorderableDelayedDragStartListener( - key: listItem.key, - index: index, - child: listItem, - ); - } + buildDefaultDragHandles: false, + itemCount: math.max(0, itemCount * 2 - 1), + itemBuilder: (BuildContext context, int index) { + final itemIndex = index ~/ 2; + if (index.isEven) { + final listItem = itemBuilder(context, itemIndex); + return ReorderableDelayedDragStartListener( + key: listItem.key, + index: index, + child: listItem, + ); + } - final separator = separatorBuilder(context, itemIndex); - if (separator.key == null) { - return KeyedSubtree( - key: ValueKey('reorderable_separator_$itemIndex'), - child: IgnorePointer(child: separator), - ); - } + final separator = separatorBuilder(context, itemIndex); + if (separator.key == null) { + return KeyedSubtree( + key: ValueKey('reorderable_separator_$itemIndex'), + child: IgnorePointer(child: separator), + ); + } - return separator; - }, - onReorder: (int oldIndex, int newIndex) { - // Adjust the indexes due to an issue in the ReorderableListView - // which isn't going to be fixed in the near future. - // - // issue: https://github.com/flutter/flutter/issues/24786 - if (newIndex > oldIndex) { - newIndex -= 1; - } + return separator; + }, + onReorder: (int oldIndex, int newIndex) { + // Adjust the indexes due to an issue in the ReorderableListView + // which isn't going to be fixed in the near future. + // + // issue: https://github.com/flutter/flutter/issues/24786 + if (newIndex > oldIndex) { + newIndex -= 1; + } - // Ideally should never happen as separators are wrapped in the - // IgnorePointer widget. This is just a safety check. - if (oldIndex % 2 == 1) return; + // Ideally should never happen as separators are wrapped in the + // IgnorePointer widget. This is just a safety check. + if (oldIndex % 2 == 1) return; - // The item moved behind the top/bottom separator we should not - // reorder it. - if ((oldIndex - newIndex).abs() == 1) return; + // The item moved behind the top/bottom separator we should not + // reorder it. + if ((oldIndex - newIndex).abs() == 1) return; - // Calculate the updated indexes - final updatedOldIndex = oldIndex ~/ 2; - final updatedNewIndex = oldIndex > newIndex && newIndex % 2 == 1 - ? (newIndex + 1) ~/ 2 - : newIndex ~/ 2; + // Calculate the updated indexes + final updatedOldIndex = oldIndex ~/ 2; + final updatedNewIndex = oldIndex > newIndex && newIndex % 2 == 1 ? (newIndex + 1) ~/ 2 : newIndex ~/ 2; - return onReorder(updatedOldIndex, updatedNewIndex); - }, - ); + return onReorder(updatedOldIndex, updatedNewIndex); + }, + ); } 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..b43c1216e5 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/staggered_scale_transition.dart b/packages/stream_chat_flutter/lib/src/misc/staggered_scale_transition.dart new file mode 100644 index 0000000000..8dc1dddd40 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/misc/staggered_scale_transition.dart @@ -0,0 +1,148 @@ +import 'package:ezanimation/ezanimation.dart'; +import 'package:flutter/material.dart'; + +/// {@template staggeredScaleTransition} +/// A widget that scales in its [children] with a staggered animation. +/// +/// Each child pops in sequentially with a configurable [staggerDelay]. +/// +/// By default, children animate from last to first. Set [animateReversed] +/// to `false` to animate from first to last. +/// +/// {@tool snippet} +/// +/// ```dart +/// StaggeredScaleTransition( +/// children: [ +/// Icon(Icons.star), +/// Icon(Icons.favorite), +/// Icon(Icons.thumb_up), +/// ], +/// ) +/// ``` +/// {@end-tool} +/// {@endtemplate} +class StaggeredScaleTransition extends StatefulWidget { + /// {@macro staggeredScaleTransition} + const StaggeredScaleTransition({ + super.key, + required this.children, + this.staggerDelay = const Duration(milliseconds: 30), + this.animateReversed = true, + }); + + /// The widgets to display with staggered scale-in animation. + final List children; + + /// The delay between the start of each child's animation. + /// + /// Defaults to 30 milliseconds. + final Duration staggerDelay; + + /// Whether to animate children in reversed list order. + /// + /// When `true`, children animate from last to first in list order. + /// When `false`, children animate from first to last in list order. + /// + /// Defaults to `true`. + final bool animateReversed; + + @override + State createState() => _StaggeredScaleTransitionState(); +} + +class _StaggeredScaleTransitionState extends State { + List _animations = []; + + void _initAnimations() { + _animations = List.generate( + widget.children.length, + (index) => EzAnimation.tween( + Tween(begin: 0.0, end: 1.0), + const Duration(milliseconds: 120), + curve: Curves.bounceOut, + ), + ); + } + + void _triggerAnimations() async { + final iterable = switch (widget.animateReversed) { + true => _animations.reversed, + false => _animations, + }; + + for (final animation in iterable) { + if (!mounted) return; + animation.start(); + await Future.delayed(widget.staggerDelay); + } + } + + void _dismissAnimations() { + for (final animation in _animations) { + animation.stop(); + } + } + + void _disposeAnimations() { + for (final animation in _animations) { + animation.dispose(); + } + } + + @override + void initState() { + super.initState(); + _initAnimations(); + + // Trigger animations at the end of the frame to avoid jank. + WidgetsBinding.instance.endOfFrame.then((_) => _triggerAnimations()); + } + + @override + void didUpdateWidget(covariant StaggeredScaleTransition oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.children.length != widget.children.length) { + // Dismiss and dispose old animations. + _dismissAnimations(); + _disposeAnimations(); + + // Initialize new animations. + _initAnimations(); + + // 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) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + for (var i = 0; i < widget.children.length; i++) + AnimatedBuilder( + animation: _animations[i], + builder: (context, child) { + final value = _animations[i].value; + // Width grows at 2x the scale rate so the row reaches full + // width before the bounce oscillation starts. + return Align( + widthFactor: (value * 2.0).clamp(0.0, 1.0), + heightFactor: 1, + child: Transform.scale(scale: value, child: child), + ); + }, + child: widget.children[i], + ), + ], + ); + } +} 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..03665115a0 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/misc/stream_modal.dart @@ -0,0 +1,62 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.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 = Portal(child: Builder(builder: builder)); + return StreamChatTheme(data: theme, child: capturedThemes.wrap(pageChild)); + }, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/misc/swipeable.dart b/packages/stream_chat_flutter/lib/src/misc/swipeable.dart index 45b9b2dceb..8fd78e8c9e 100644 --- a/packages/stream_chat_flutter/lib/src/misc/swipeable.dart +++ b/packages/stream_chat_flutter/lib/src/misc/swipeable.dart @@ -13,10 +13,11 @@ typedef SwipeDirectionCallback = void Function(SwipeDirection direction); /// dismissing action. /// /// Used by [Swipeable.backgroundBuilder]. -typedef BackgroundWidgetBuilder = Widget Function( - BuildContext context, - SwipeUpdateDetails details, -); +typedef BackgroundWidgetBuilder = + Widget Function( + BuildContext context, + SwipeUpdateDetails details, + ); /// The direction in which a [Swipeable] can be swiped. enum SwipeDirection { @@ -32,7 +33,7 @@ enum SwipeDirection { startToEnd, /// The [Swipeable] cannot be swiped by dragging. - none + none, } /// A widget that can be swiped in a specified direction. @@ -95,9 +96,9 @@ class Swipeable extends StatefulWidget { this.dragStartBehavior = DragStartBehavior.start, this.behavior = HitTestBehavior.opaque, }) : assert( - swipeThreshold >= 0.0 && swipeThreshold <= 1.0, - 'swipeThreshold must be between 0.0 and 1.0', - ); + swipeThreshold >= 0.0 && swipeThreshold <= 1.0, + 'swipeThreshold must be between 0.0 and 1.0', + ); /// The widget below this widget in the tree. /// @@ -225,8 +226,7 @@ class _SwipeableClipper extends CustomClipper { } } -class _SwipeableState extends State - with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { +class _SwipeableState extends State with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { @override void initState() { super.initState(); @@ -261,13 +261,9 @@ class _SwipeableState extends State } switch (Directionality.of(context)) { case TextDirection.rtl: - return extent < 0 - ? SwipeDirection.startToEnd - : SwipeDirection.endToStart; + return extent < 0 ? SwipeDirection.startToEnd : SwipeDirection.endToStart; case TextDirection.ltr: - return extent > 0 - ? SwipeDirection.startToEnd - : SwipeDirection.endToStart; + return extent > 0 ? SwipeDirection.startToEnd : SwipeDirection.endToStart; } } @@ -280,8 +276,7 @@ class _SwipeableState extends State void _handleDragStart(DragStartDetails details) { _dragUnderway = true; if (_moveController!.isAnimating) { - _dragExtent = - _moveController!.value * _overallDragAxisExtent * _dragExtent.sign; + _dragExtent = _moveController!.value * _overallDragAxisExtent * _dragExtent.sign; _moveController!.stop(); } else { _dragExtent = 0.0; diff --git a/packages/stream_chat_flutter/lib/src/misc/thread_header.dart b/packages/stream_chat_flutter/lib/src/misc/thread_header.dart index 1142ac4507..73badc2f16 100644 --- a/packages/stream_chat_flutter/lib/src/misc/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/misc/thread_header.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template streamThreadHeader} /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header.png) @@ -57,8 +57,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// and the [ChannelTheme.channelHeaderTheme] property. Modify it to change /// the widget's appearance. /// {@endtemplate} -class StreamThreadHeader extends StatelessWidget - implements PreferredSizeWidget { +class StreamThreadHeader extends StatelessWidget implements PreferredSizeWidget { /// {@macro streamThreadHeader} const StreamThreadHeader({ super.key, @@ -67,13 +66,14 @@ class StreamThreadHeader extends StatelessWidget this.onBackPressed, this.title, this.subtitle, - this.centerTitle, + this.centerTitle = true, this.leading, this.actions, this.onTitleTap, this.showTypingIndicator = true, this.backgroundColor, - this.elevation = 1, + this.elevation = 0, + this.scrolledUnderElevation = 0, }) : preferredSize = const Size.fromHeight(kToolbarHeight); /// Whether to show the leading back button. @@ -99,7 +99,7 @@ class StreamThreadHeader extends StatelessWidget final Widget? subtitle; /// Whether the title should be centered - final bool? centerTitle; + final bool centerTitle; /// Leading widget final Widget? leading; @@ -118,6 +118,9 @@ class StreamThreadHeader extends StatelessWidget /// The elevation for this [StreamThreadHeader]. final double elevation; + /// The scrolled under elevation for this [StreamThreadHeader]. + final double scrolledUnderElevation; + @override Widget build(BuildContext context) { final effectiveCenterTitle = getEffectiveCenterTitle( @@ -127,35 +130,34 @@ class StreamThreadHeader extends StatelessWidget ); final channelHeaderTheme = StreamChannelHeaderTheme.of(context); + final textTheme = context.streamTextTheme; + final colorScheme = context.streamColorScheme; + + final replyCount = parent.replyCount; + + final defaultSubtitle = + subtitle ?? + (replyCount != null + ? Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + context.translations.threadReplyCountText(replyCount), + style: + channelHeaderTheme.subtitleStyle ?? + textTheme.captionDefault.copyWith(color: colorScheme.textSecondary), + ), + ], + ) + : const SizedBox.shrink()); - final defaultSubtitle = subtitle ?? - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - '${context.translations.withText} ', - style: channelHeaderTheme.subtitleStyle, - ), - Flexible( - child: StreamChannelName( - channel: StreamChannel.of(context).channel, - textStyle: channelHeaderTheme.subtitleStyle, - ), - ), - ], - ); - - final theme = Theme.of(context); - return AppBar( + return StreamAppBar( automaticallyImplyLeading: false, - toolbarTextStyle: theme.textTheme.bodyMedium, - titleTextStyle: theme.textTheme.titleLarge, - systemOverlayStyle: theme.brightness == Brightness.dark - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark, elevation: elevation, - leading: leading ?? + scrolledUnderElevation: scrolledUnderElevation, + leading: + leading ?? (showBackButton ? StreamBackButton( channelId: StreamChannel.of(context).channel.cid, @@ -173,20 +175,20 @@ class StreamThreadHeader extends StatelessWidget width: 250, child: Column( mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: effectiveCenterTitle - ? CrossAxisAlignment.center - : CrossAxisAlignment.stretch, + crossAxisAlignment: effectiveCenterTitle ? CrossAxisAlignment.center : CrossAxisAlignment.stretch, children: [ title ?? Text( context.translations.threadReplyLabel, - style: channelHeaderTheme.titleStyle, + style: channelHeaderTheme.titleStyle ?? textTheme.headingSm, ), const SizedBox(height: 2), if (showTypingIndicator) StreamTypingIndicator( channel: StreamChannel.of(context).channel, - style: channelHeaderTheme.subtitleStyle, + style: + channelHeaderTheme.subtitleStyle ?? + textTheme.captionDefault.copyWith(color: colorScheme.textSecondary), parentId: parent.id, alternativeWidget: defaultSubtitle, ) diff --git a/packages/stream_chat_flutter/lib/src/misc/visible_footnote.dart b/packages/stream_chat_flutter/lib/src/misc/visible_footnote.dart index d2308f9d7d..19144bac15 100644 --- a/packages/stream_chat_flutter/lib/src/misc/visible_footnote.dart +++ b/packages/stream_chat_flutter/lib/src/misc/visible_footnote.dart @@ -17,9 +17,9 @@ class StreamVisibleFootnote extends StatelessWidget { return Row( mainAxisSize: MainAxisSize.min, children: [ - StreamSvgIcon( + Icon( + context.streamIcons.eyeFill16, size: 16, - icon: StreamSvgIcons.eye, color: chatThemeData.colorTheme.textLowEmphasis, ), const SizedBox(width: 8), diff --git a/packages/stream_chat_flutter/lib/src/poll/creator/poll_option_reorderable_list_view.dart b/packages/stream_chat_flutter/lib/src/poll/creator/poll_option_reorderable_list_view.dart index cff3c96253..77ef5169c3 100644 --- a/packages/stream_chat_flutter/lib/src/poll/creator/poll_option_reorderable_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/poll/creator/poll_option_reorderable_list_view.dart @@ -116,16 +116,16 @@ class PollOptionListItem extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(vertical: 18), onChanged: switch (onChanged) { final onChanged? => (text) { - final updated = option.copyWith(text: text); - return onChanged.call(updated); - }, + final updated = option.copyWith(text: text); + return onChanged.call(updated); + }, _ => null, }, ), ), IconButton( iconSize: 24, - icon: const StreamSvgIcon(icon: StreamSvgIcons.delete), + icon: Icon(context.streamIcons.delete20), style: IconButton.styleFrom( foregroundColor: colorTheme.textLowEmphasis, ), @@ -181,12 +181,10 @@ class PollOptionReorderableListView extends StatefulWidget { final ValueSetter>? onOptionsChanged; @override - State createState() => - _PollOptionReorderableListViewState(); + State createState() => _PollOptionReorderableListViewState(); } -class _PollOptionReorderableListViewState - extends State { +class _PollOptionReorderableListViewState extends State { late Map _focusNodes; late Map _options; diff --git a/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_dialog.dart b/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_dialog.dart index f9d2a76777..2f247f67c2 100644 --- a/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_dialog.dart @@ -92,8 +92,7 @@ class StreamPollCreatorDialog extends StatefulWidget { final EdgeInsets padding; @override - State createState() => - _StreamPollCreatorDialogState(); + State createState() => _StreamPollCreatorDialogState(); } class _StreamPollCreatorDialogState extends State { @@ -202,12 +201,10 @@ class StreamPollCreatorFullScreenDialog extends StatefulWidget { final EdgeInsets padding; @override - State createState() => - _StreamPollCreatorFullScreenDialogState(); + State createState() => _StreamPollCreatorFullScreenDialogState(); } -class _StreamPollCreatorFullScreenDialogState - extends State { +class _StreamPollCreatorFullScreenDialogState extends State { late final _controller = StreamPollController( poll: widget.poll, config: widget.config, @@ -244,7 +241,7 @@ class _StreamPollCreatorFullScreenDialogState return IconButton( color: colorTheme.accentPrimary, disabledColor: colorTheme.disabled, - icon: const StreamSvgIcon(icon: StreamSvgIcons.send), + icon: Icon(context.streamIcons.send20), onPressed: isValid ? () { final errors = _controller.validateGranularly(); diff --git a/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_widget.dart b/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_widget.dart index dcb9edce7c..93e4872ce9 100644 --- a/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_widget.dart +++ b/packages/stream_chat_flutter/lib/src/poll/creator/stream_poll_creator_widget.dart @@ -67,12 +67,10 @@ class StreamPollCreatorWidget extends StatelessWidget { allowDuplicate: config.allowDuplicateOptions, optionsRange: config.optionsRange, initialOptions: [ - for (final option in poll.options) - PollOptionItem(id: option.id, text: option.text), + for (final option in poll.options) PollOptionItem(id: option.id, text: option.text), ], onOptionsChanged: (options) => controller.options = [ - for (final option in options) - PollOption(id: option.id, text: option.text), + for (final option in options) PollOption(id: option.id, text: option.text), ], ), const SizedBox(height: 32), @@ -118,7 +116,8 @@ class StreamPollCreatorWidget extends StatelessWidget { PollSwitchListTile( title: translations.anonymousPollLabel, value: poll.votingVisibility == VotingVisibility.anonymous, - onChanged: (anon) => controller.votingVisibility = anon // + onChanged: (anon) => controller.votingVisibility = + anon // ? VotingVisibility.anonymous : VotingVisibility.public, ), diff --git a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_add_comment_dialog.dart b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_add_comment_dialog.dart index 47c67a7b76..4f5fe67596 100644 --- a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_add_comment_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_add_comment_dialog.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/poll/stream_poll_text_field.dart'; -import 'package:stream_chat_flutter/src/theme/poll_interactor_theme.dart'; import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; @@ -8,23 +7,32 @@ import 'package:stream_chat_flutter/src/utils/extensions.dart'; /// Shows a dialog that allows the user to add a poll comment. /// /// Optionally, you can provide an [initialValue] to pre-fill the text field. +/// +/// See also: +/// +/// * [PollAddCommentDialog], the dialog widget shown by this function. +/// * [StreamPollInteractor], which invokes this via [StreamPollInteractor.onAddComment]. /// {@endtemplate} Future showPollAddCommentDialog({ required BuildContext context, String initialValue = '', -}) => - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => PollAddCommentDialog( - initialValue: initialValue, - ), - ); +}) => showDialog( + context: context, + barrierDismissible: false, + builder: (_) => PollAddCommentDialog( + initialValue: initialValue, + ), +); /// {@template pollAddCommentDialog} /// A dialog that allows the user to add or update a poll comment. /// /// Optionally, you can provide an [initialValue] to pre-fill the text field. +/// +/// See also: +/// +/// * [showPollAddCommentDialog], the convenience function to show this dialog. +/// * [StreamPollInteractor], the parent widget that triggers this dialog. /// {@endtemplate} class PollAddCommentDialog extends StatefulWidget { /// {@macro pollAddCommentDialog} @@ -48,7 +56,6 @@ class _PollAddCommentDialogState extends State { @override Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); - final pollInteractorTheme = StreamPollInteractorTheme.of(context); final actions = [ TextButton( @@ -80,7 +87,6 @@ class _PollAddCommentDialogState extends State { true => context.translations.addACommentLabel, false => context.translations.updateYourCommentLabel, }, - style: pollInteractorTheme.pollActionDialogTitleStyle, ), actions: actions, titlePadding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), @@ -96,9 +102,10 @@ class _PollAddCommentDialogState extends State { vertical: 12, horizontal: 16, ), - style: pollInteractorTheme.pollActionDialogTextFieldStyle, - fillColor: pollInteractorTheme.pollActionDialogTextFieldFillColor, - borderRadius: pollInteractorTheme.pollActionDialogTextFieldBorderRadius, + // TODO: Fix when working on poll create screen + // style: pollInteractorTheme.pollActionDialogTextFieldStyle, + // fillColor: pollInteractorTheme.pollActionDialogTextFieldFillColor, + // borderRadius: pollInteractorTheme.pollActionDialogTextFieldBorderRadius, onChanged: (value) => setState(() => _comment = value), ), ); diff --git a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_end_vote_dialog.dart b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_end_vote_dialog.dart index fb7ec9f409..e8f8b327e6 100644 --- a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_end_vote_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_end_vote_dialog.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/theme/poll_interactor_theme.dart'; import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; -/// {@template showPollSuggestOptionDialog} +/// {@template showPollEndVoteDialog} /// Shows a dialog that allows the user to end vote for a poll. +/// +/// See also: +/// +/// * [PollEndVoteDialog], the dialog widget shown by this function. +/// * [StreamPollInteractor], which invokes this via [StreamPollInteractor.onEndVote]. /// {@endtemplate} Future showPollEndVoteDialog({ required BuildContext context, @@ -17,6 +21,11 @@ Future showPollEndVoteDialog({ /// {@template pollEndVoteDialog} /// A dialog that allows the user to end vote for a poll. +/// +/// See also: +/// +/// * [showPollEndVoteDialog], the convenience function to show this dialog. +/// * [StreamPollInteractor], the parent widget that triggers this dialog. /// {@endtemplate} class PollEndVoteDialog extends StatelessWidget { /// {@macro pollEndVoteDialog} @@ -25,7 +34,6 @@ class PollEndVoteDialog extends StatelessWidget { @override Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); - final pollInteractorTheme = StreamPollInteractorTheme.of(context); final actions = [ TextButton( @@ -49,10 +57,7 @@ class PollEndVoteDialog extends StatelessWidget { ]; return AlertDialog( - title: Text( - context.translations.endVoteConfirmationText, - style: pollInteractorTheme.pollActionDialogTitleStyle, - ), + title: Text(context.translations.endVoteConfirmationText), actions: actions, titlePadding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), diff --git a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_footer.dart b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_footer.dart index 807e5071e0..d8544e184a 100644 --- a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_footer.dart +++ b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_footer.dart @@ -1,13 +1,20 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/theme/poll_interactor_theme.dart'; -import 'package:stream_chat_flutter/src/utils/utils.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template pollFooter} /// A widget used as the footer of a poll. /// /// Used in [StreamPollInteractor] to display various actions the user can take /// on the poll. +/// +/// See also: +/// +/// * [StreamPollInteractorThemeData.primaryActionStyle], for customizing +/// primary action buttons (view results, end vote). +/// * [StreamPollInteractorThemeData.secondaryActionStyle], for customizing +/// secondary action buttons (suggest option, add comment, view comments). +/// * [StreamPollInteractor], the parent widget that uses this footer. /// {@endtemplate} class PollFooter extends StatelessWidget { /// {@macro pollFooter} @@ -21,7 +28,6 @@ class PollFooter extends StatelessWidget { this.onViewComments, this.onViewResults, this.onSuggestOption, - this.onSeeMoreOptions, }); /// The poll the footer is for. @@ -59,12 +65,6 @@ class PollFooter extends StatelessWidget { /// suggested options. final VoidCallback? onSuggestOption; - /// Callback invoked when the user wants to see more options. - /// - /// This is only available if the poll has more options than the - /// [visibleOptionCount]. - final VoidCallback? onSeeMoreOptions; - bool get _shouldShowEndPollButton { if (poll.isClosed) return false; @@ -72,6 +72,11 @@ class PollFooter extends StatelessWidget { return poll.createdBy?.id == currentUser.id; } + bool get _shouldShowViewResultsButton { + // If the poll has no votes, don't show the button. + return poll.voteCount > 0; + } + bool get _shouldShowAddCommentButton { if (poll.isClosed || !poll.allowAnswers) return false; @@ -93,86 +98,145 @@ class PollFooter extends StatelessWidget { return poll.allowUserSuggestedOptions; } - bool get _shouldEnableViewResultsButton { - // Disable the button if the poll haven't got any votes yet. - if (poll.voteCount < 1) return false; - - return true; - } - @override Widget build(BuildContext context) { final translations = context.translations; - return Column( - spacing: 2, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (visibleOptionCount case final count? - when count < poll.options.length) - PollFooterButton( - title: translations.seeAllOptionsLabel(count: poll.options.length), - onPressed: onSeeMoreOptions, - ), - if (_shouldShowSuggestionsButton) - PollFooterButton( - title: translations.suggestAnOptionLabel, - onPressed: onSuggestOption, + final spacing = context.streamSpacing; + + final showEndPoll = _shouldShowEndPollButton; + final showViewResults = _shouldShowViewResultsButton; + final showSuggestions = _shouldShowSuggestionsButton; + final showAddComment = _shouldShowAddCommentButton; + final showViewComments = _shouldShowViewCommentsButton; + + if (!showEndPoll && !showViewResults && !showSuggestions && !showAddComment && !showViewComments) { + return SizedBox(height: spacing.lg); + } + + return Padding( + padding: .all(spacing.md), + child: Column( + mainAxisSize: .min, + spacing: spacing.xxxs, + crossAxisAlignment: .stretch, + children: [ + Column( + mainAxisSize: .min, + spacing: spacing.xs, + crossAxisAlignment: .stretch, + children: [ + if (showViewResults) + _PollFooterButton.primary( + label: translations.viewResultsLabel, + onPressed: onViewResults, + ), + if (showEndPoll) + _PollFooterButton.primary( + label: translations.endVoteLabel, + onPressed: onEndVote, + ), + ], ), - if (_shouldShowAddCommentButton) - PollFooterButton( - title: translations.addACommentLabel, - onPressed: onAddComment, - ), - if (_shouldShowViewCommentsButton) - PollFooterButton( - title: translations.viewCommentsLabel, - onPressed: onViewComments, - ), - PollFooterButton( - title: translations.viewResultsLabel, - onPressed: _shouldEnableViewResultsButton ? onViewResults : null, - ), - if (_shouldShowEndPollButton) - PollFooterButton( - title: translations.endVoteLabel, - onPressed: onEndVote, + Column( + mainAxisSize: .min, + spacing: spacing.xs, + crossAxisAlignment: .stretch, + children: [ + if (showSuggestions) + _PollFooterButton.secondary( + label: translations.suggestAnOptionLabel, + onPressed: onSuggestOption, + ), + if (showAddComment) + _PollFooterButton.secondary( + label: translations.addACommentLabel, + onPressed: onAddComment, + ), + if (showViewComments) + _PollFooterButton.secondary( + label: translations.viewCommentsLabel, + onPressed: onViewComments, + ), + ], ), - ], + ], + ), ); } } -/// {@template pollFooterButton} -/// A button used in [PollFooter]. -/// -/// Displays the title and invokes the [onPressed] callback when pressed. -/// {@endtemplate} -class PollFooterButton extends StatelessWidget { - /// {@macro pollFooterButton} - const PollFooterButton({ - super.key, - required this.title, +// A button used in [PollFooter]. +// +// Renders a [StreamButton] with the appropriate poll interactor theme style +// applied via [StreamButtonTheme]. +class _PollFooterButton extends StatelessWidget { + const _PollFooterButton({ + required this.label, + required this.type, this.onPressed, }); - /// The title of the button. - final String title; + // Creates a primary poll footer button (outline style). + const _PollFooterButton.primary({ + required String label, + VoidCallback? onPressed, + }) : this(label: label, type: .outline, onPressed: onPressed); + + // Creates a secondary poll footer button (ghost style). + const _PollFooterButton.secondary({ + required String label, + VoidCallback? onPressed, + }) : this(label: label, type: .ghost, onPressed: onPressed); - /// Callback invoked when the button is pressed. + final String label; + final StreamButtonType type; final VoidCallback? onPressed; @override Widget build(BuildContext context) { final theme = StreamPollInteractorTheme.of(context); + final defaults = _StreamPollFooterDefaults(context); - return TextButton( - onPressed: onPressed, - // Consume long press to avoid the parent long press. - onLongPress: onPressed != null ? () {} : null, - style: theme.pollActionButtonStyle, - child: Text(title), + final effectivePrimaryActionStyle = theme.primaryActionStyle ?? defaults.primaryActionStyle; + final effectiveSecondaryActionStyle = theme.secondaryActionStyle ?? defaults.secondaryActionStyle; + + return StreamButtonTheme( + data: StreamButtonThemeData( + secondary: StreamButtonTypeStyle( + outline: effectivePrimaryActionStyle, + ghost: effectiveSecondaryActionStyle, + ), + ), + child: StreamButton( + size: .small, + style: .secondary, + type: type, + onTap: onPressed, + label: label, + ), ); } } + +// Default values for [StreamPollInteractorThemeData] backed by stream design tokens. +class _StreamPollFooterDefaults extends StreamPollInteractorThemeData { + _StreamPollFooterDefaults(this._context); + + final BuildContext _context; + + late final _alignment = StreamMessageLayout.messageAlignmentOf(_context); + late final StreamColorScheme _colorScheme = _context.streamColorScheme; + + @override + StreamButtonThemeStyle get primaryActionStyle => .from( + tapTargetSize: .shrinkWrap, + borderColor: switch (_alignment) { + .start => _colorScheme.borderStrong, + .end => _colorScheme.brand.shade300, + }, + ); + + @override + StreamButtonThemeStyle get secondaryActionStyle => .from(tapTargetSize: .shrinkWrap); +} diff --git a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_header.dart b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_header.dart index 0e32a1e59f..655511b767 100644 --- a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_header.dart +++ b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_header.dart @@ -2,11 +2,20 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/theme/poll_interactor_theme.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template pollHeader} /// A widget used as the header of a poll. /// /// Used in [StreamPollInteractor] to display the poll question and voting mode. +/// +/// See also: +/// +/// * [StreamPollInteractorThemeData.titleTextStyle], for customizing the +/// question text. +/// * [StreamPollInteractorThemeData.subtitleTextStyle], for customizing the +/// voting mode text. +/// * [StreamPollInteractor], the parent widget that uses this header. /// {@endtemplate} class PollHeader extends StatelessWidget { /// {@macro pollHeader} @@ -21,20 +30,46 @@ class PollHeader extends StatelessWidget { @override Widget build(BuildContext context) { final theme = StreamPollInteractorTheme.of(context); + final defaults = _StreamPollHeaderDefaults(context); + + final spacing = context.streamSpacing; + + final effectiveTitleTextStyle = theme.titleTextStyle ?? defaults.titleTextStyle; + final effectiveSubtitleTextStyle = theme.subtitleTextStyle ?? defaults.subtitleTextStyle; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - poll.name, - style: theme.pollTitleStyle, - ), - Text( - context.translations.pollVotingModeLabel(poll.votingMode), - style: theme.pollSubtitleStyle, - ), - ], + return Padding( + padding: .all(spacing.md), + child: Column( + spacing: spacing.xxs, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(poll.name, style: effectiveTitleTextStyle), + Text(context.translations.pollVotingModeLabel(poll.votingMode), style: effectiveSubtitleTextStyle), + ], + ), ); } } + +// Default values for [StreamPollInteractorThemeData] backed by stream design tokens. +class _StreamPollHeaderDefaults extends StreamPollInteractorThemeData { + _StreamPollHeaderDefaults(this._context); + + final BuildContext _context; + + late final _alignment = StreamMessageLayout.messageAlignmentOf(_context); + late final StreamColorScheme _colorScheme = _context.streamColorScheme; + late final StreamTextTheme _textTheme = _context.streamTextTheme; + + Color get _textColor => switch (_alignment) { + .start => _colorScheme.textPrimary, + .end => _colorScheme.brand.shade900, + }; + + @override + TextStyle get titleTextStyle => _textTheme.bodyEmphasis.copyWith(color: _textColor); + + @override + TextStyle get subtitleTextStyle => _textTheme.captionDefault.copyWith(color: _textColor); +} diff --git a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_options_list_view.dart b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_options_list_view.dart index 2aa627a176..5f9651a966 100644 --- a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_options_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_options_list_view.dart @@ -1,14 +1,21 @@ 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/components/avatar/stream_user_avatar_stack.dart'; import 'package:stream_chat_flutter/src/theme/poll_interactor_theme.dart'; -import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/utils/utils.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template pollOptionsListView} /// A widget that displays the list of poll options. /// /// Used in [StreamPollInteractor] to display the poll options to interact with. +/// +/// See also: +/// +/// * [PollOptionItem], the widget used to render each individual option. +/// * [StreamPollInteractorThemeData.optionStyle], for customizing option +/// appearance. +/// * [StreamPollInteractor], the parent widget that uses this list. /// {@endtemplate} class PollOptionsListView extends StatelessWidget { /// {@macro pollOptionsListView} @@ -16,6 +23,7 @@ class PollOptionsListView extends StatelessWidget { super.key, required this.poll, this.visibleOptionCount, + this.onSeeMoreOptions, this.showProgressBar = false, this.onCastVote, this.onRemoveVote, @@ -29,6 +37,12 @@ class PollOptionsListView extends StatelessWidget { /// If null, all options will be visible. final int? visibleOptionCount; + /// Callback invoked when the user wants to see more options. + /// + /// This is only available if the poll has more options than the + /// [visibleOptionCount]. + final VoidCallback? onSeeMoreOptions; + /// Whether to show the voting progress bar. /// /// Note: This is only used when the poll is public. @@ -66,36 +80,57 @@ class PollOptionsListView extends StatelessWidget { _ => poll.options, }; - return ListView.separated( - shrinkWrap: true, - itemCount: options.length, - physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (_, __) => const SizedBox(height: 8), - itemBuilder: (context, index) { - final option = options.elementAt(index); - return PollOptionItem( - key: ValueKey(option.id), - poll: poll, - option: option, - showProgressBar: showProgressBar, - onChanged: (checked) { - if (checked == null) return; - - // Handle voting based on the voting mode. - poll.votingMode.when( - disabled: () {}, // Do nothing - all: () => _handleVoteAction(option, checked: checked), - // Note: We don't need to remove the other votes in the unique - // voting mode as the backend handles it. - unique: () => _handleVoteAction(option, checked: checked), - limited: (count) => _handleVoteAction( - option, - checked: checked && poll.ownVotes.length < count, - ), - ); - }, - ); - }, + final spacing = context.streamSpacing; + final translations = context.translations; + + return Padding( + padding: .symmetric(horizontal: spacing.xs), + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .stretch, + children: [ + ListView.separated( + shrinkWrap: true, + itemCount: options.length, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (_, __) => SizedBox(height: spacing.xxxs), + itemBuilder: (context, index) { + final option = options.elementAt(index); + return PollOptionItem( + key: ValueKey(option.id), + poll: poll, + option: option, + showProgressBar: showProgressBar, + onChanged: (checked) { + if (checked == null) return; + + // Handle voting based on the voting mode. + poll.votingMode.when( + disabled: () {}, // Do nothing + all: () => _handleVoteAction(option, checked: checked), + // Note: We don't need to remove the other votes in the unique + // voting mode as the backend handles it. + unique: () => _handleVoteAction(option, checked: checked), + limited: (count) => _handleVoteAction( + option, + checked: checked && poll.ownVotes.length < count, + ), + ); + }, + ); + }, + ), + if (visibleOptionCount case final count? when count < poll.options.length) + StreamButton( + size: .small, + style: .secondary, + type: .ghost, + onTap: onSeeMoreOptions, + themeStyle: .from(tapTargetSize: .shrinkWrap), + label: translations.seeAllOptionsLabel(count: poll.options.length), + ), + ], + ), ); } } @@ -107,6 +142,11 @@ class PollOptionsListView extends StatelessWidget { /// /// This widget is used to display the poll option and the number of votes it /// has received. Also shows the voters if the poll is public. +/// +/// See also: +/// +/// * [StreamPollOptionStyle], for customizing option appearance. +/// * [PollOptionsListView], which uses this widget for each option. /// {@endtemplate} class PollOptionItem extends StatelessWidget { /// {@macro pollOptionItem} @@ -136,80 +176,91 @@ class PollOptionItem extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = StreamPollInteractorTheme.of(context); + final theme = StreamPollInteractorTheme.of(context).optionStyle; + final defaults = _StreamPollOptionDefaults(context); + + final radius = context.streamRadius; + final spacing = context.streamSpacing; + + final effectiveTextStyle = theme?.textStyle ?? defaults.textStyle; + final effectiveVotesTextStyle = theme?.votesTextStyle ?? defaults.votesTextStyle; + final effectiveCheckboxStyle = theme?.checkboxStyle ?? defaults.checkboxStyle; + final effectiveVotesAvatarSize = theme?.votesAvatarSize ?? defaults.votesAvatarSize; + final effectiveProgressBarStyle = theme?.progressBarStyle ?? defaults.progressBarStyle; final pollClosed = poll.isClosed; final isOptionSelected = poll.hasCurrentUserVotedFor(option); final control = ExcludeFocus( - child: Checkbox( - value: isOptionSelected, - onChanged: pollClosed ? null : onChanged, - checkColor: theme.pollOptionCheckboxCheckColor, - shape: theme.pollOptionCheckboxShape, - side: theme.pollOptionCheckboxBorderSide, - activeColor: theme.pollOptionCheckboxActiveColor, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: const VisualDensity( - vertical: VisualDensity.minimumDensity, - horizontal: VisualDensity.minimumDensity, + child: StreamCheckboxTheme( + data: .new(style: effectiveCheckboxStyle), + child: StreamCheckbox.circular( + size: .md, + value: isOptionSelected, + onChanged: pollClosed ? null : onChanged, ), ), ); return InkWell( + borderRadius: .all(radius.md), onTap: pollClosed ? null : () => onChanged?.call(!isOptionSelected), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: .all(spacing.xs), child: Row( - spacing: 4, + spacing: spacing.sm, children: [ if (pollClosed case false) control, Expanded( child: Column( - spacing: 4, + spacing: spacing.xxs, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - spacing: 4, + spacing: spacing.xs, children: [ Expanded( child: Text( option.text, maxLines: 2, overflow: TextOverflow.ellipsis, - style: theme.pollOptionTextStyle, + style: effectiveTextStyle, ), ), - // Show voters only if the poll is public. - if (poll.votingVisibility == VotingVisibility.public) - OptionVoters( - // We only show the latest 3 voters. - voters: [ - ...?poll.latestVotesByOption[option.id], - ].map((it) => it.user).whereType().take(3), - ), - Text( - poll.voteCountFor(option).toString(), - style: theme.pollOptionVoteCountTextStyle, + Row( + mainAxisSize: .min, + spacing: spacing.xxs, + children: [ + // Show voters only if the poll is public. + if (poll.votingVisibility == VotingVisibility.public) + StreamUserAvatarStack( + size: effectiveVotesAvatarSize, + // We only show the latest 3 voters. + users: [ + ...?poll.latestVotesByOption[option.id], + ].map((it) => it.user).whereType().take(3), + ), + Text( + poll.voteCountFor(option).toString(), + style: effectiveVotesTextStyle, + ), + ], ), ], ), if (showProgressBar) - OptionVotesProgressBar( - value: poll.voteRatioFor(option), - borderRadius: - theme.pollOptionVotesProgressBarBorderRadius ?? - BorderRadius.circular(4), - trackColor: theme.pollOptionVotesProgressBarTrackColor, - valueColor: switch (poll.isOptionWinner(option)) { - true => theme.pollOptionVotesProgressBarWinnerColor, - false => theme.pollOptionVotesProgressBarValueColor, - }, + StreamProgressBarTheme( + data: .new(style: effectiveProgressBarStyle), + child: TweenAnimationBuilder( + curve: Curves.easeOutCubic, + duration: Durations.medium2, + tween: Tween(begin: 0, end: poll.voteRatioFor(option)), + builder: (_, value, _) => StreamProgressBar(value: value), + ), ), ], ), - ) + ), ], ), ), @@ -217,147 +268,47 @@ class PollOptionItem extends StatelessWidget { } } -/// {@template optionVoters} -/// A widget that displays the voters of an option. -/// -/// Used in [PollOptionItem] to display the voters of a poll option. -/// {@endtemplate} -class OptionVoters extends StatelessWidget { - /// {@macro optionVoters} - const OptionVoters({ - super.key, - this.radius = 10, - this.overlap = 0.5, - required this.voters, - }) : assert( - overlap >= 0 && overlap <= 1, - 'Overlap must be between 0 and 1', - ); - - /// The radius of the avatars. - final double radius; - - /// The overlap between the avatars. - /// - /// The default value is 1/2 i.e. 50%. - final double overlap; - - /// The list of voters to display. - final Iterable voters; - - @override - Widget build(BuildContext context) { - if (voters.isEmpty) return const Empty(); - - final theme = StreamChatTheme.of(context); +// Default values for [StreamPollOptionStyle] backed by stream design tokens. +class _StreamPollOptionDefaults extends StreamPollOptionStyle { + _StreamPollOptionDefaults(this._context); - final diameter = radius * 2; - final width = diameter + (voters.length * diameter * overlap); + final BuildContext _context; - var overlapPadding = 0.0; + late final _alignment = StreamMessageLayout.messageAlignmentOf(_context); + late final StreamColorScheme _colorScheme = _context.streamColorScheme; + late final StreamTextTheme _textTheme = _context.streamTextTheme; - return SizedBox.fromSize( - size: Size(width, diameter), - child: Stack( - children: [ - ...voters.map( - (user) { - overlapPadding += diameter * overlap; - return Positioned( - right: overlapPadding - (diameter * overlap), - bottom: 0, - top: 0, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: theme.colorTheme.barsBg, - ), - padding: const EdgeInsets.all(1), - child: StreamUserAvatar( - user: user, - constraints: BoxConstraints.tight(Size.fromRadius(radius)), - showOnlineStatus: false, - ), - ), - ); - }, - ), - ], - ), - ); - } -} - -/// {@template optionVotesProgressBar} -/// A widget that displays the progress of the votes for an option. -/// -/// Used in [PollOptionItem] to display the progress of the votes for a -/// particular option. -/// {@endtemplate} -class OptionVotesProgressBar extends StatelessWidget { - /// {@macro optionVotesProgressBar} - const OptionVotesProgressBar({ - super.key, - required this.value, - this.minHeight = 4, - this.trackColor, - this.valueColor, - this.borderRadius = BorderRadius.zero, - }); - - /// The value of the progress bar. - final double value; + Color get _textColor => switch (_alignment) { + .start => _colorScheme.textPrimary, + .end => _colorScheme.brand.shade900, + }; - /// The minimum height of the progress bar. - final double minHeight; + @override + StreamAvatarStackSize get votesAvatarSize => StreamAvatarStackSize.xs; - /// The color of the track. - final Color? trackColor; + @override + TextStyle get textStyle => _textTheme.captionDefault.copyWith(color: _textColor); - /// The color of the value. - final Color? valueColor; + @override + TextStyle get votesTextStyle => _textTheme.metadataDefault.copyWith(color: _textColor); - /// The border radius of the progress bar. - /// - /// Defaults to [BorderRadius.zero]. - final BorderRadiusGeometry borderRadius; + @override + StreamCheckboxStyle get checkboxStyle => StreamCheckboxStyle.from( + side: switch (_alignment) { + .start => BorderSide(color: _colorScheme.borderStrong), + .end => BorderSide(color: _colorScheme.brand.shade300), + }, + ); @override - Widget build(BuildContext context) { - final shape = RoundedRectangleBorder(borderRadius: borderRadius); - return Container( - constraints: BoxConstraints( - minWidth: double.infinity, - minHeight: minHeight, - ), - decoration: ShapeDecoration( - shape: shape, - color: trackColor, - ), - child: LayoutBuilder( - builder: (context, constraints) { - final size = constraints.constrain(Size.zero); - - final textDirection = Directionality.of(context); - final alignment = switch (textDirection) { - TextDirection.ltr => Alignment.centerLeft, - TextDirection.rtl => Alignment.centerRight, - }; - - return Align( - alignment: alignment, - child: AnimatedContainer( - height: size.height, - width: size.width * value, - duration: Durations.medium2, - decoration: ShapeDecoration( - shape: shape, - color: valueColor, - ), - ), - ); - }, - ), - ); - } + StreamProgressBarStyle get progressBarStyle => StreamProgressBarStyle( + trackColor: switch (_alignment) { + .start => _colorScheme.backgroundSurfaceStrong, + .end => _colorScheme.brand.shade200, + }, + fillColor: switch (_alignment) { + .start => _colorScheme.accentNeutral, + .end => _colorScheme.accentPrimary, + }, + ); } diff --git a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_suggest_option_dialog.dart b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_suggest_option_dialog.dart index fd4c02f934..44ab300753 100644 --- a/packages/stream_chat_flutter/lib/src/poll/interactor/poll_suggest_option_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/poll/interactor/poll_suggest_option_dialog.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/poll/stream_poll_text_field.dart'; -import 'package:stream_chat_flutter/src/theme/poll_interactor_theme.dart'; import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; @@ -8,23 +7,34 @@ import 'package:stream_chat_flutter/src/utils/extensions.dart'; /// Shows a dialog that allows the user to suggest an option for a poll. /// /// Optionally, you can provide an [initialOption] to pre-fill the text field. +/// +/// See also: +/// +/// * [PollSuggestOptionDialog], the dialog widget shown by this function. +/// * [StreamPollInteractor], which invokes this via +/// [StreamPollInteractor.onSuggestOption]. /// {@endtemplate} Future showPollSuggestOptionDialog({ required BuildContext context, String initialOption = '', -}) => - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => PollSuggestOptionDialog( - initialOption: initialOption, - ), - ); +}) => showDialog( + context: context, + barrierDismissible: false, + builder: (_) => PollSuggestOptionDialog( + initialOption: initialOption, + ), +); /// {@template pollSuggestOptionDialog} /// A dialog that allows the user to suggest an option for a poll. /// /// Optionally, you can provide an [initialOption] to pre-fill the text field. +/// +/// See also: +/// +/// * [showPollSuggestOptionDialog], the convenience function to show this +/// dialog. +/// * [StreamPollInteractor], the parent widget that triggers this dialog. /// {@endtemplate} class PollSuggestOptionDialog extends StatefulWidget { /// {@macro pollSuggestOptionDialog} @@ -39,8 +49,7 @@ class PollSuggestOptionDialog extends StatefulWidget { final String initialOption; @override - State createState() => - _PollSuggestOptionDialogState(); + State createState() => _PollSuggestOptionDialogState(); } class _PollSuggestOptionDialogState extends State { @@ -49,7 +58,6 @@ class _PollSuggestOptionDialogState extends State { @override Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); - final pollInteractorTheme = StreamPollInteractorTheme.of(context); final actions = [ TextButton( @@ -78,7 +86,7 @@ class _PollSuggestOptionDialogState extends State { return AlertDialog( title: Text( context.translations.suggestAnOptionLabel, - style: pollInteractorTheme.pollActionDialogTitleStyle, + // style: pollInteractorTheme.pollActionDialogTitleStyle, ), actions: actions, titlePadding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16), @@ -94,9 +102,10 @@ class _PollSuggestOptionDialogState extends State { vertical: 12, horizontal: 16, ), - style: pollInteractorTheme.pollActionDialogTextFieldStyle, - fillColor: pollInteractorTheme.pollActionDialogTextFieldFillColor, - borderRadius: pollInteractorTheme.pollActionDialogTextFieldBorderRadius, + // TODO: Fix when working on poll create screen + // style: pollInteractorTheme.pollActionDialogTextFieldStyle, + // fillColor: pollInteractorTheme.pollActionDialogTextFieldFillColor, + // borderRadius: pollInteractorTheme.pollActionDialogTextFieldBorderRadius, onChanged: (value) => setState(() => _option = value), ), ); diff --git a/packages/stream_chat_flutter/lib/src/poll/interactor/stream_poll_interactor.dart b/packages/stream_chat_flutter/lib/src/poll/interactor/stream_poll_interactor.dart index 035ea76e1e..3abdb2bd6d 100644 --- a/packages/stream_chat_flutter/lib/src/poll/interactor/stream_poll_interactor.dart +++ b/packages/stream_chat_flutter/lib/src/poll/interactor/stream_poll_interactor.dart @@ -19,6 +19,13 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// /// The widget also provides a [visibleOptionCount] parameter to control the /// number of visible options in the poll. If null, all options will be visible. +/// +/// See also: +/// +/// * [StreamPollInteractorTheme], for customizing poll interactor appearance. +/// * [PollHeader], the header sub-component showing the poll question. +/// * [PollOptionsListView], the list of votable options. +/// * [PollFooter], the footer sub-component with action buttons. /// {@endtemplate} class StreamPollInteractor extends StatelessWidget { /// {@macro streamPollInteractor} @@ -26,10 +33,6 @@ class StreamPollInteractor extends StatelessWidget { super.key, required this.poll, required this.currentUser, - this.padding = const EdgeInsets.symmetric( - vertical: 12, - horizontal: 10, - ), this.visibleOptionCount, this.onCastVote, this.onRemoveVote, @@ -47,9 +50,6 @@ class StreamPollInteractor extends StatelessWidget { /// The current user interacting with the poll. final User currentUser; - /// The padding to apply to the interactor. - final EdgeInsetsGeometry padding; - /// The number of visible options in the poll. /// /// If null, all options will be visible. @@ -96,41 +96,37 @@ class StreamPollInteractor extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: padding, - child: Column( - spacing: 8, - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PollHeader(poll: poll), - MediaQuery.removePadding( - context: context, - // Workaround for the bottom padding issue. - // Link: https://github.com/flutter/flutter/issues/156149 - removeTop: true, - removeBottom: true, - child: PollOptionsListView( - poll: poll, - showProgressBar: true, - visibleOptionCount: visibleOptionCount, - onCastVote: onCastVote, - onRemoveVote: onRemoveVote, - ), - ), - PollFooter( + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PollHeader(poll: poll), + MediaQuery.removePadding( + context: context, + // Workaround for the bottom padding issue. + // Link: https://github.com/flutter/flutter/issues/156149 + removeTop: true, + removeBottom: true, + child: PollOptionsListView( poll: poll, - currentUser: currentUser, + showProgressBar: true, visibleOptionCount: visibleOptionCount, - onEndVote: onEndVote, - onAddComment: onAddComment, - onViewComments: onViewComments, - onViewResults: onViewResults, - onSuggestOption: onSuggestOption, onSeeMoreOptions: onSeeMoreOptions, + onCastVote: onCastVote, + onRemoveVote: onRemoveVote, ), - ], - ), + ), + PollFooter( + poll: poll, + currentUser: currentUser, + visibleOptionCount: visibleOptionCount, + onEndVote: onEndVote, + onAddComment: onAddComment, + onViewComments: onViewComments, + onViewResults: onViewResults, + onSuggestOption: onSuggestOption, + ), + ], ); } } diff --git a/packages/stream_chat_flutter/lib/src/poll/stream_poll_comments_dialog.dart b/packages/stream_chat_flutter/lib/src/poll/stream_poll_comments_dialog.dart index eb6ba6db92..deecc7c6f0 100644 --- a/packages/stream_chat_flutter/lib/src/poll/stream_poll_comments_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/poll/stream_poll_comments_dialog.dart @@ -77,8 +77,7 @@ class StreamPollCommentsDialog extends StatefulWidget { final VoidCallback? onUpdateComment; @override - State createState() => - _StreamPollCommentsDialogState(); + State createState() => _StreamPollCommentsDialogState(); } class _StreamPollCommentsDialogState extends State { diff --git a/packages/stream_chat_flutter/lib/src/poll/stream_poll_option_votes_dialog.dart b/packages/stream_chat_flutter/lib/src/poll/stream_poll_option_votes_dialog.dart index a22e994c57..d6391e69ed 100644 --- a/packages/stream_chat_flutter/lib/src/poll/stream_poll_option_votes_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/poll/stream_poll_option_votes_dialog.dart @@ -1,11 +1,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_view.dart'; import 'package:stream_chat_flutter/src/theme/poll_option_votes_dialog_theme.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template showStreamPollOptionVotesDialog} /// Displays an interactive dialog to show all the votes for a poll option. @@ -66,12 +66,10 @@ class StreamPollOptionVotesDialog extends StatefulWidget { final int? pollVotesCount; @override - State createState() => - _StreamPollOptionVotesDialogState(); + State createState() => _StreamPollOptionVotesDialogState(); } -class _StreamPollOptionVotesDialogState - extends State { +class _StreamPollOptionVotesDialogState extends State { late StreamPollVoteListController _controller; @override @@ -83,8 +81,7 @@ class _StreamPollOptionVotesDialogState @override void didUpdateWidget(covariant StreamPollOptionVotesDialog oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.poll.id != widget.poll.id || - oldWidget.option.id != widget.option.id) { + if (oldWidget.poll.id != widget.poll.id || oldWidget.option.id != widget.option.id) { _controller.dispose(); // Dispose the old controller. _initializeController(); // Initialize a new controller. } @@ -131,8 +128,8 @@ class _StreamPollOptionVotesDialogState mainAxisAlignment: MainAxisAlignment.end, children: [ if (isOptionWinner) ...[ - StreamSvgIcon( - icon: StreamSvgIcons.award, + Icon( + context.streamIcons.trophy20, color: theme.pollOptionWinnerVoteCountTextStyle?.color, ), const SizedBox(width: 8), @@ -141,9 +138,7 @@ class _StreamPollOptionVotesDialogState context.translations.voteCountLabel( count: widget.pollVotesCount, ), - style: isOptionWinner - ? theme.pollOptionWinnerVoteCountTextStyle - : theme.pollOptionVoteCountTextStyle, + style: isOptionWinner ? theme.pollOptionWinnerVoteCountTextStyle : theme.pollOptionVoteCountTextStyle, ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/poll/stream_poll_results_dialog.dart b/packages/stream_chat_flutter/lib/src/poll/stream_poll_results_dialog.dart index e1b5ee1064..2180e6e8c7 100644 --- a/packages/stream_chat_flutter/lib/src/poll/stream_poll_results_dialog.dart +++ b/packages/stream_chat_flutter/lib/src/poll/stream_poll_results_dialog.dart @@ -1,13 +1,13 @@ import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; import 'package:stream_chat_flutter/src/poll/stream_poll_option_votes_dialog.dart'; import 'package:stream_chat_flutter/src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_tile.dart'; import 'package:stream_chat_flutter/src/theme/poll_results_dialog_theme.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template showStreamPollResultsDialog} /// Displays an interactive dialog to show the results of a poll. @@ -270,9 +270,7 @@ class PollVotesByOptionItem extends StatelessWidget { vertical: 12, horizontal: 16, ), - decoration: isOptionWinner - ? theme.pollOptionsWinnerDecoration - : theme.pollOptionsDecoration, + decoration: isOptionWinner ? theme.pollOptionsWinnerDecoration : theme.pollOptionsDecoration, child: Column( spacing: 16, mainAxisSize: MainAxisSize.min, @@ -283,24 +281,20 @@ class PollVotesByOptionItem extends StatelessWidget { Expanded( child: Text( option.text, - style: isOptionWinner - ? theme.pollOptionsWinnerTextStyle - : theme.pollOptionsTextStyle, + style: isOptionWinner ? theme.pollOptionsWinnerTextStyle : theme.pollOptionsTextStyle, ), ), const SizedBox(width: 8), if (isOptionWinner) ...[ - StreamSvgIcon( - icon: StreamSvgIcons.award, + Icon( + context.streamIcons.trophy20, color: theme.pollOptionsWinnerVoteCountTextStyle?.color, ), const SizedBox(width: 8), ], Text( context.translations.voteCountLabel(count: pollVotesCount), - style: isOptionWinner - ? theme.pollOptionsWinnerVoteCountTextStyle - : theme.pollOptionsVoteCountTextStyle, + style: isOptionWinner ? theme.pollOptionsWinnerVoteCountTextStyle : theme.pollOptionsVoteCountTextStyle, ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/poll/stream_poll_text_field.dart b/packages/stream_chat_flutter/lib/src/poll/stream_poll_text_field.dart index 3a4af9b9c4..9a28423978 100644 --- a/packages/stream_chat_flutter/lib/src/poll/stream_poll_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/poll/stream_poll_text_field.dart @@ -90,9 +90,9 @@ class _StreamPollTextFieldState extends State { if (currValue != newValue) { _controller.value = switch (newValue) { final value? => TextEditingValue( - text: value, - selection: TextSelection.collapsed(offset: value.length), - ), + text: value, + selection: TextSelection.collapsed(offset: value.length), + ), _ => TextEditingValue.empty, }; } @@ -129,7 +129,8 @@ class _StreamPollTextFieldState extends State { right: horizontalPadding / 2, ), errorText: widget.errorText, - errorStyle: widget.errorStyle ?? + errorStyle: + widget.errorStyle ?? theme.textTheme.footnote.copyWith( color: theme.colorTheme.accentError, ), 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 75% 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..7a00738cb0 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 @@ -1,10 +1,8 @@ // ignore_for_file: cascade_invocations -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} @@ -42,8 +40,7 @@ class DesktopReactionsBuilder extends StatefulWidget { final bool reverse; @override - State createState() => - _DesktopReactionsBuilderState(); + State createState() => _DesktopReactionsBuilderState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -71,19 +68,18 @@ class _DesktopReactionsBuilderState extends State { Widget build(BuildContext context) { final streamChat = StreamChat.of(context); final currentUser = streamChat.currentUser!; - final reactionIcons = StreamChatConfiguration.of(context).reactionIcons; + final config = StreamChatConfiguration.of(context); + final resolver = config.reactionIconResolver; final streamChatTheme = StreamChatTheme.of(context); final reactionsMap = {}; widget.message.latestReactions?.forEach((element) { - if (!reactionsMap.containsKey(element.type) || - element.user!.id == currentUser.id) { + if (!reactionsMap.containsKey(element.type) || element.user!.id == currentUser.id) { reactionsMap[element.type] = element; } }); - final reactionsList = reactionsMap.values.toList() - ..sort((a, b) => a.user!.id == currentUser.id ? 1 : -1); + final reactionsList = reactionsMap.values.toList()..sort((a, b) => a.user!.id == currentUser.id ? 1 : -1); return PortalTarget( visible: _showReactionsPopup, @@ -101,11 +97,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( @@ -122,17 +114,13 @@ class _DesktopReactionsBuilderState extends State { runSpacing: 4, children: [ ...reactionsList.map((reaction) { - final reactionIcon = reactionIcons.firstWhereOrNull( - (r) => r.type == reaction.type, - ); - return _BottomReaction( currentUser: currentUser, reaction: reaction, message: widget.message, borderSide: widget.borderSide, messageTheme: widget.messageTheme, - reactionIcon: reactionIcon, + resolver: resolver, streamChatTheme: streamChatTheme, ); }).toList(), @@ -159,7 +147,7 @@ class _BottomReaction extends StatelessWidget { required this.message, required this.borderSide, required this.messageTheme, - required this.reactionIcon, + required this.resolver, required this.streamChatTheme, }); @@ -168,7 +156,7 @@ class _BottomReaction extends StatelessWidget { final Message message; final BorderSide? borderSide; final StreamMessageThemeData? messageTheme; - final StreamReactionIcon? reactionIcon; + final ReactionIconResolver resolver; final StreamChatThemeData streamChatTheme; @override @@ -182,17 +170,18 @@ class _BottomReaction extends StatelessWidget { onTap: () { if (reaction.userId == userId) { StreamChannel.of(context).channel.deleteReaction( - message, - reaction, - ); - } else if (reactionIcon != null) { + message, + reaction, + ); + } else { StreamChannel.of(context).channel.sendReaction( - message, - reactionIcon!.type, - score: reaction.score + 1, - enforceUnique: - StreamChatConfiguration.of(context).enforceUniqueReactions, - ); + message, + Reaction( + type: reaction.type, + emojiCode: resolver.emojiCode(reaction.type), + ), + enforceUnique: StreamChatConfiguration.of(context).enforceUniqueReactions, + ); } }, child: Card( @@ -201,7 +190,8 @@ class _BottomReaction extends StatelessWidget { // This is done to avoid shadow when background color is transparent. elevation: backgroundColor == Colors.transparent ? 0 : null, shape: RoundedRectangleBorder( - side: borderSide ?? + side: + borderSide ?? BorderSide( color: messageTheme?.reactionsBorderColor ?? Colors.transparent, ), @@ -213,22 +203,9 @@ class _BottomReaction extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - ConstrainedBox( - constraints: BoxConstraints.tight( - const Size.square(14), - ), - child: reactionIcon?.builder( - context, - reaction.user?.id == userId, - 14, - ) ?? - Icon( - Icons.help_outline_rounded, - size: 14, - color: reaction.user?.id == userId - ? streamChatTheme.colorTheme.accentPrimary - : streamChatTheme.colorTheme.textLowEmphasis, - ), + StreamEmoji( + size: StreamEmojiSize.sm, + emoji: resolver.resolve(reaction.type), ), const SizedBox(width: 4), Text( diff --git a/packages/stream_chat_flutter/lib/src/reactions/detail/reaction_detail_sheet.dart b/packages/stream_chat_flutter/lib/src/reactions/detail/reaction_detail_sheet.dart new file mode 100644 index 0000000000..93c29161c4 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/reactions/detail/reaction_detail_sheet.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/components/avatar/stream_user_avatar.dart'; +import 'package:stream_chat_flutter/src/message_action/message_action.dart'; +import 'package:stream_chat_flutter/src/scroll_view/reaction_scroll_view/stream_reaction_list_view.dart'; +import 'package:stream_chat_flutter/src/stream_chat.dart'; +import 'package:stream_chat_flutter/src/stream_chat_configuration.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// A bottom sheet that displays detailed reaction information for a message. +/// +/// Shows the total reaction count, emoji filter chips for each reaction type, +/// and a paginated scrollable list of users who reacted. +/// +/// Reactions are fetched from the server using [StreamReactionListController], +/// supporting cursor-based pagination for large reaction lists. +/// +/// Use [ReactionDetailSheet.show] to display the sheet. +class ReactionDetailSheet extends StatefulWidget { + /// Creates a reaction detail sheet. + /// + /// This constructor is private. Use [ReactionDetailSheet.show] to display + /// the sheet as a modal bottom sheet. + const ReactionDetailSheet._({ + required this.scrollController, + required this.message, + this.initialReactionType, + }); + + /// The message whose reactions are displayed. + final Message message; + + /// Scroll controller provided by [DraggableScrollableSheet]. + final ScrollController scrollController; + + /// The reaction type to pre-select when the sheet opens. + /// + /// When non-null, the sheet opens with this reaction type already filtered + /// and the corresponding chip scrolled into view. + final String? initialReactionType; + + /// Shows the reaction detail sheet as a modal bottom sheet. + /// + /// Returns a [SelectReaction] if the user selects a reaction, or `null` + /// if the sheet is dismissed without any selection. + static Future show({ + required BuildContext context, + required Message message, + String? initialReactionType, + }) { + final radius = context.streamRadius; + final colorScheme = context.streamColorScheme; + + return showModalBottomSheet( + context: context, + useSafeArea: true, + isScrollControlled: true, + showDragHandle: true, + backgroundColor: colorScheme.backgroundElevation1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadiusDirectional.only( + topStart: radius.xl, + topEnd: radius.xl, + ), + ), + builder: (context) => DraggableScrollableSheet( + snap: true, + expand: false, + minChildSize: 0.5, + snapSizes: const [0.5, 1], + builder: (_, scrollController) => ReactionDetailSheet._( + scrollController: scrollController, + message: message, + initialReactionType: initialReactionType, + ), + ), + ); + } + + @override + State createState() => _ReactionDetailSheetState(); +} + +class _ReactionDetailSheetState extends State { + late StreamReactionListController _controller; + late String? _currentReactionType = widget.initialReactionType; + + @override + void initState() { + super.initState(); + _initializeController(); + } + + @override + void didUpdateWidget(covariant ReactionDetailSheet oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.message.id != widget.message.id) { + _controller.dispose(); // Dispose the old controller. + _initializeController(); // Initialize a new controller. + } + } + + void _initializeController() { + _controller = .new( + client: StreamChat.of(context).client, + messageId: widget.message.id, + sort: const [.desc(ReactionSortKey.createdAt)], + filter: switch (_currentReactionType) { + final type? => .equal('type', type), + _ => null, + }, + ); + } + + void _onReactionTypeSelected(String? type) { + if (type == _currentReactionType) return; + setState(() => _currentReactionType = type); + + final updatedFilter = switch (type) { + final type? => Filter.equal('type', type), + _ => null, + }; + + _controller.filter = updatedFilter; + _controller.doInitialLoad(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + final textTheme = context.streamTextTheme; + + final config = StreamChatConfiguration.of(context); + final resolver = config.reactionIconResolver; + + final ownReactions = [...?widget.message.ownReactions]; + final ownReactionsMap = {for (final it in ownReactions) it.type: it}; + final reactionGroups = widget.message.reactionGroups ?? {}; + + final currentUserId = StreamChatCore.of(context).currentUser?.id; + + final visibleCount = switch (_currentReactionType) { + final type? => reactionGroups[type]?.count ?? 0, + _ => reactionGroups.values.fold(0, (sum, g) => sum + g.count), + }; + + return Column( + mainAxisSize: .min, + crossAxisAlignment: .stretch, + children: [ + Padding( + padding: .symmetric(horizontal: spacing.sm), + child: Text( + switch (visibleCount) { + 1 => '1 Reaction', + _ => '$visibleCount Reactions', + }, + textAlign: .center, + style: textTheme.headingSm, + ), + ), + SizedBox(height: spacing.sm), + StreamEmojiChipBar( + selected: _currentReactionType, + onSelected: _onReactionTypeSelected, + items: [ + for (final MapEntry(:key, :value) in reactionGroups.entries) + StreamEmojiChipItem( + value: key, + emoji: resolver.resolve(key), + count: value.count, + ), + ], + leading: StreamEmojiChip.addEmoji( + onPressed: () async { + final selectedReactions = ownReactionsMap.keys.toSet(); + final emoji = await StreamEmojiPickerSheet.show( + context: context, + selectedReactions: selectedReactions, + ); + + if (!context.mounted) return; + if (emoji == null) return Navigator.of(context).pop(); + + final reaction = Reaction(type: emoji.shortName, emojiCode: emoji.emoji); + return Navigator.of(context).pop(SelectReaction(message: widget.message, reaction: reaction)); + }, + ), + ), + SizedBox(height: spacing.md), + Expanded( + child: StreamReactionListView( + controller: _controller, + scrollController: widget.scrollController, + padding: .symmetric(horizontal: spacing.xxs), + itemBuilder: (context, reactions, index) { + final reaction = reactions[index]; + final user = reaction.user; + if (user == null) return const SizedBox.shrink(); + + final isOwnReaction = currentUserId != null && reaction.userId == currentUserId; + + return StreamListTile( + leading: StreamUserAvatar(size: .md, user: user, showOnlineIndicator: false), + title: Text(user.name), + subtitle: isOwnReaction ? const Text('Tap to remove') : null, + trailing: StreamEmoji(size: .md, emoji: resolver.resolve(reaction.type)), + onTap: switch (isOwnReaction) { + true => () { + final action = SelectReaction(message: widget.message, reaction: reaction); + return Navigator.of(context).pop(action); + }, + _ => null, + }, + ); + }, + ), + ), + ], + ); + } +} 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..c9406db718 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/reactions/picker/reaction_picker.dart @@ -0,0 +1,89 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_configuration.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// {@template onReactionPicked} +/// Callback called when a reaction is picked. +/// {@endtemplate} +typedef OnReactionPicked = ValueSetter; + +/// {@template streamMessageReactionPicker} +/// A chat-specific reaction picker that bridges [StreamReactionPicker] with +/// chat domain models. +/// +/// Resolves reaction icons via [ReactionIconResolver], tracks the current +/// user's own reactions on the [Message], and wires the "add reaction" button +/// to [StreamEmojiPickerSheet]. +/// +/// Visual customisation is controlled through [StreamReactionPickerTheme] in +/// the widget tree. +/// +/// See also: +/// +/// * [StreamReactionPicker], the domain-agnostic core picker. +/// * [ReactionIconResolver], which maps reaction types to emoji content models. +/// * [StreamReactionPickerTheme], for customising the picker appearance. +/// {@endtemplate} +class StreamMessageReactionPicker extends StatelessWidget { + /// {@macro streamMessageReactionPicker} + const StreamMessageReactionPicker({ + super.key, + required this.message, + this.onReactionPicked, + }); + + /// The message to attach the reaction to. + final Message message; + + /// {@macro onReactionPicked} + final OnReactionPicked? onReactionPicked; + + @override + Widget build(BuildContext context) { + final config = StreamChatConfiguration.of(context); + final resolver = config.reactionIconResolver; + final reactionTypes = resolver.defaultReactions; + + final ownReactions = [...?message.ownReactions]; + final ownReactionsMap = {for (final it in ownReactions) it.type: it}; + + final items = [ + ...reactionTypes.map( + (type) => StreamReactionPickerItem( + key: type, + emoji: resolver.resolve(type), + // If the reaction is present in ownReactions, it is selected. + isSelected: ownReactionsMap[type] != null, + ), + ), + ]; + + void onItemPicked(StreamReactionPickerItem item) { + final reactionEmojiCode = resolver.emojiCode(item.key); + final pickedReaction = switch (ownReactionsMap[item.key]) { + final reaction? => reaction, + _ => Reaction(type: item.key, emojiCode: reactionEmojiCode), + }; + + return onReactionPicked?.call(pickedReaction); + } + + return StreamReactionPicker( + items: items, + onReactionPicked: onItemPicked, + onAddReactionTap: () async { + final selectedReactions = ownReactionsMap.keys.toSet(); + final emoji = await StreamEmojiPickerSheet.show( + context: context, + selectedReactions: selectedReactions, + ); + + if (!context.mounted || emoji == null) return; + + final reaction = Reaction(type: emoji.shortName, emojiCode: emoji.emoji); + return onReactionPicked?.call(reaction); + }, + ); + } +} 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..f5165d57e6 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/reactions/user_reactions.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/components/avatar/stream_user_avatar.dart'; +import 'package:stream_chat_flutter/src/misc/empty_widget.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'; +import 'package:stream_core_flutter/stream_core_flutter.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 resolver = StreamChatConfiguration.of(context).reactionIconResolver; + + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + GestureDetector( + onTap: switch (onTap) { + final onTap? => () => onTap(reactionUser), + _ => null, + }, + child: StreamUserAvatar( + size: .xl, + user: reactionUser, + showOnlineIndicator: false, + ), + ), + PositionedDirectional( + bottom: 8, + end: isCurrentUserReaction ? null : 0, + start: isCurrentUserReaction ? 0 : null, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: messageTheme.reactionsMaskColor, + borderRadius: const BorderRadius.all(Radius.circular(26)), + ), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: messageTheme.reactionsBackgroundColor, + border: Border.all( + color: messageTheme.reactionsBorderColor ?? Colors.transparent, + ), + borderRadius: const BorderRadius.all(Radius.circular(24)), + ), + child: StreamEmoji( + size: StreamEmojiSize.sm, + emoji: resolver.resolve(reaction.type), + ), + ), + ), + ), + ], + ), + 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/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart index dc685d09ae..fae5ad9bca 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_grid_tile.dart @@ -46,31 +46,23 @@ class StreamChannelGridTile extends StatelessWidget { Widget? footer, GestureTapCallback? onTap, GestureLongPressCallback? onLongPress, - }) => - StreamChannelGridTile( - key: key ?? this.key, - channel: channel ?? this.channel, - footer: footer ?? this.footer, - onTap: onTap ?? this.onTap, - onLongPress: onLongPress ?? this.onLongPress, - child: child ?? this.child, - ); + }) => StreamChannelGridTile( + key: key ?? this.key, + channel: channel ?? this.channel, + footer: footer ?? this.footer, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + child: child ?? this.child, + ); @override Widget build(BuildContext context) { final channelPreviewTheme = StreamChannelPreviewTheme.of(context); - final child = this.child ?? - StreamChannelAvatar( - channel: channel, - borderRadius: BorderRadius.circular(32), - constraints: const BoxConstraints.tightFor( - height: 64, - width: 64, - ), - ); + final child = this.child ?? StreamChannelAvatar(size: .xl, channel: channel); - final footer = this.footer ?? + final footer = + this.footer ?? StreamChannelName( channel: channel, textStyle: channelPreviewTheme.titleStyle, diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_grid_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_grid_view.dart index e391451647..9f52e09410 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_grid_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_grid_view.dart @@ -2,18 +2,17 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default grid delegate for [StreamChannelGridView]. -const defaultChannelGridViewDelegate = - SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4); +const defaultChannelGridViewDelegate = SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4); /// Signature for the item builder that creates the children of the /// [StreamChannelGridView]. -typedef StreamChannelGridViewIndexedWidgetBuilder - = StreamScrollViewIndexedWidgetBuilder; +typedef StreamChannelGridViewIndexedWidgetBuilder = + StreamScrollViewIndexedWidgetBuilder; /// A [GridView] that shows a grid of [User]s, /// it uses [StreamChannelGridTile] as a default item. @@ -351,9 +350,9 @@ class StreamChannelGridView extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(8), child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( + emptyIcon: Icon( + context.streamIcons.messageBubble32, size: 148, - icon: StreamSvgIcons.message, color: chatThemeData.colorTheme.disabled, ), emptyTitle: Text( @@ -364,18 +363,17 @@ class StreamChannelGridView extends StatelessWidget { ), ); }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.grid( + loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.grid( onTap: controller.retry, error: Text( context.translations.loadingChannelsError, textAlign: TextAlign.center, ), ), - loadMoreIndicatorBuilder: (context) => const Center( + loadMoreIndicatorBuilder: (context) => Center( child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), ), ), loadingBuilder: (context) => diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_empty_state.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_empty_state.dart new file mode 100644 index 0000000000..d3b83f3a6d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_empty_state.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A widget that is used to display the empty state of the channel list. +class StreamChannelListEmptyState extends StatelessWidget { + /// Creates a new instance of the [StreamChannelListEmptyState]. + const StreamChannelListEmptyState({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + context.streamIcons.messageBubbles32, + size: 32, + ), + SizedBox(height: context.streamSpacing.sm), + Text(context.translations.noConversationsYetText), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_item.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_item.dart new file mode 100644 index 0000000000..a5c833a49b --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_item.dart @@ -0,0 +1,786 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; +import 'package:stream_chat_flutter/src/misc/timestamp.dart'; +import 'package:stream_chat_flutter/src/utils/date_formatter.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// A widget that displays a channel preview. +/// +/// This widget is intended to be used as a Tile in [StreamChannelListView]. +/// +/// It shows the last message of the channel, the last message time, the unread +/// message count, the typing indicator, the sending indicator and the channel +/// avatar. +/// +/// Internally uses [StreamListTileContainer] from the core design system for +/// consistent visual presentation. +/// +/// See also: +/// * [StreamChannelAvatar] +/// * [StreamChannelName] +class StreamChannelListItem extends StatelessWidget { + /// Creates a new instance of [StreamChannelListItem] widget. + StreamChannelListItem({ + super.key, + required Channel channel, + GestureTapCallback? onTap, + GestureLongPressCallback? onLongPress, + bool selected = false, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + props = .new( + channel: channel, + onTap: onTap, + onLongPress: onLongPress, + selected: selected, + ); + + /// The properties for the channel list item. + final StreamChannelListItemProps props; + + /// Creates a copy of this tile but with the given fields replaced with + /// the new values. + StreamChannelListItem copyWith({ + Key? key, + Channel? channel, + VoidCallback? onTap, + VoidCallback? onLongPress, + bool? selected, + }) { + return StreamChannelListItem( + key: key ?? this.key, + channel: channel ?? props.channel, + onTap: onTap ?? props.onTap, + onLongPress: onLongPress ?? props.onLongPress, + selected: selected ?? props.selected, + ); + } + + @override + Widget build(BuildContext context) { + final builder = context.chatComponentBuilder(); + return builder?.call(context, props) ?? _DefaultStreamChannelListItem(props: props); + } +} + +/// Properties for configuring a [StreamChannelListItem]. +/// +/// This class holds all the configuration options for a channel list item, +/// allowing them to be passed through the [StreamComponentFactory]. +/// +/// See also: +/// +/// * [StreamChannelListItem], which uses these properties. +/// * [DefaultStreamChannelListItem], the default implementation. +class StreamChannelListItemProps { + /// Creates properties for a channel list item. + const StreamChannelListItemProps({ + required this.channel, + this.leading, + this.title, + this.subtitle, + this.trailing, + this.onTap, + this.onLongPress, + this.sendingIndicatorBuilder, + this.selected = false, + }); + + /// The channel to display. + final Channel channel; + + /// A widget to display as the avatar. + /// + /// Defaults to [StreamChannelAvatar]. + final Widget? leading; + + /// The primary content of the list tile. + /// + /// Defaults to [StreamChannelName]. + final Widget? title; + + /// Additional content displayed below the title. + /// + /// Defaults to [ChannelListTileSubtitle] which shows typing indicators, + /// draft messages, or the last message preview. + final Widget? subtitle; + + /// A widget to display as the timestamp. + /// + /// Defaults to [ChannelLastMessageDate]. + final Widget? trailing; + + /// Called when the user taps this list tile. + final GestureTapCallback? onTap; + + /// Called when the user long-presses on this list tile. + final GestureLongPressCallback? onLongPress; + + /// The widget builder for the sending indicator. + /// + /// `Message` is the last message in the channel. Use it to determine the + /// status using [Message.state]. + final Widget Function(BuildContext, Message)? sendingIndicatorBuilder; + + /// True if the tile is in a selected state. + final bool selected; +} + +class _DefaultStreamChannelListItem extends StatelessWidget { + const _DefaultStreamChannelListItem({ + required this.props, + }); + + final StreamChannelListItemProps props; + + @override + Widget build(BuildContext context) { + final channelState = props.channel.state!; + final textTheme = context.streamTextTheme; + + final avatar = props.leading ?? StreamChannelAvatar(channel: props.channel, size: StreamAvatarGroupSize.xl); + final titleWidget = + props.title ?? + StreamChannelName( + channel: props.channel, + textStyle: textTheme.headingSm.copyWith(height: 1), + ); + final subtitleWidget = + props.subtitle ?? + ChannelListTileSubtitle( + channel: props.channel, + sendingIndicatorBuilder: props.sendingIndicatorBuilder, + ); + final timestampWidget = props.trailing ?? ChannelLastMessageDate(channel: props.channel); + + return BetterStreamBuilder( + stream: props.channel.isMutedStream, + initialData: props.channel.isMuted, + builder: (context, isMuted) => BetterStreamBuilder( + stream: channelState.unreadCountStream, + initialData: channelState.unreadCount, + builder: (context, unreadCount) { + return StreamChannelListTile( + avatar: avatar, + title: titleWidget, + subtitle: subtitleWidget, + timestamp: timestampWidget, + unreadCount: unreadCount, + isMuted: isMuted, + onTap: props.onTap, + onLongPress: props.onLongPress, + selected: props.selected, + ); + }, + ), + ); + } +} + +/// A widget that displays a channel list tile. +/// It's the basic component for [StreamChannelListItem] without any of the logic. +/// It can be used to fully customize the list tile data being shown. +class StreamChannelListTile extends StatelessWidget { + /// Creates a new instance of [StreamChannelListTile] widget. + const StreamChannelListTile({ + super.key, + required this.avatar, + required this.title, + this.subtitle, + this.timestamp, + this.unreadCount = 0, + this.isMuted = false, + this.onTap, + this.onLongPress, + this.selected = false, + }); + + /// The avatar widget displayed at the leading edge. + /// + /// Typically a [StreamAvatar], [StreamAvatarGroup], or an avatar wrapped + /// in a [StreamOnlineIndicator]. + final Widget avatar; + + /// The channel title widget. + /// + /// Typically a [Text] widget with the channel name. The default text style + /// is provided by the theme's title style via [DefaultTextStyle]. + final Widget title; + + /// The message preview widget displayed below the title. + /// + /// Typically a [Text] widget with the last message, but can be any widget + /// for richer content (e.g., icons, read receipts, sender prefix). + final Widget? subtitle; + + /// The timestamp widget displayed in the trailing section of the title row. + /// + /// Typically a [Text] widget with a formatted date string. The default text + /// style is provided by the theme's timestamp style via [DefaultTextStyle]. + final Widget? timestamp; + + /// The number of unread messages. + /// + /// When greater than zero, a [StreamBadgeNotification] is displayed. + final int unreadCount; + + /// Whether the channel is muted. + /// + /// When true, a mute icon is displayed in the title or subtitle. + final bool isMuted; + + /// Called when the list item is tapped. + final VoidCallback? onTap; + + /// Called when the list item is long-pressed. + final VoidCallback? onLongPress; + + /// Whether the list item is in a selected state. + final bool selected; + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + final channelListItemTheme = StreamChannelListItemTheme.of(context); + final defaults = _StreamChannelListItemThemeDefaults(context); + + final effectiveTitleStyle = channelListItemTheme.titleStyle ?? defaults.titleStyle; + final effectiveSubtitleStyle = channelListItemTheme.subtitleStyle ?? defaults.subtitleStyle; + final effectiveTimestampStyle = channelListItemTheme.timestampStyle ?? defaults.timestampStyle; + final effectiveMuteIconPosition = channelListItemTheme.muteIconPosition ?? defaults.muteIconPosition; + + final muteIcon = isMuted + ? Icon( + context.streamIcons.mute20, + size: 20, + color: context.streamColorScheme.textTertiary, + ) + : null; + + final hasMuteIconInSubtitle = effectiveMuteIconPosition == MuteIconPosition.subtitle && isMuted; + + return StreamListTileTheme( + data: context.streamListTileTheme.copyWith( + contentPadding: EdgeInsets.all(spacing.md - 4), + backgroundColor: channelListItemTheme.backgroundColor, + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: Material( + type: MaterialType.transparency, + child: StreamListTileContainer( + enabled: true, + selected: selected, + onTap: onTap, + onLongPress: onLongPress, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: spacing.md, + children: [ + avatar, + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(vertical: spacing.xxxs), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: spacing.xxs, + children: [ + _TitleRow( + title: title, + titleTrailing: effectiveMuteIconPosition == MuteIconPosition.title ? muteIcon : null, + timestamp: timestamp, + unreadCount: unreadCount, + titleStyle: effectiveTitleStyle, + timestampStyle: effectiveTimestampStyle, + spacing: spacing, + ), + if (subtitle != null || hasMuteIconInSubtitle) + _SubtitleRow( + subtitle: subtitle, + subtitleTrailing: effectiveMuteIconPosition == MuteIconPosition.subtitle ? muteIcon : null, + subtitleStyle: effectiveSubtitleStyle, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _TitleRow extends StatelessWidget { + const _TitleRow({ + required this.title, + this.titleTrailing, + this.timestamp, + required this.unreadCount, + required this.titleStyle, + required this.timestampStyle, + required this.spacing, + }); + + final Widget title; + final Widget? titleTrailing; + final Widget? timestamp; + final int unreadCount; + final TextStyle titleStyle; + final TextStyle timestampStyle; + final StreamSpacing spacing; + + @override + Widget build(BuildContext context) { + return Row( + spacing: spacing.md, + children: [ + Expanded( + child: Row( + spacing: spacing.xxs, + children: [ + Flexible( + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: StreamBadgeNotificationSize.sm.value), + child: Align( + alignment: AlignmentDirectional.centerStart, + widthFactor: 1, + child: DefaultTextStyle.merge( + style: titleStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: title, + ), + ), + ), + ), + ?titleTrailing, + ], + ), + ), + if (timestamp != null || unreadCount > 0) + Row( + mainAxisSize: MainAxisSize.min, + spacing: spacing.xs, + children: [ + if (timestamp case final timestamp?) + DefaultTextStyle.merge( + style: timestampStyle, + child: timestamp, + ), + if (unreadCount > 0) StreamBadgeNotification(label: '$unreadCount'), + ], + ), + ], + ); + } +} + +class _SubtitleRow extends StatelessWidget { + const _SubtitleRow({ + required this.subtitle, + this.subtitleTrailing, + required this.subtitleStyle, + }); + + final Widget? subtitle; + final Widget? subtitleTrailing; + final TextStyle subtitleStyle; + + @override + Widget build(BuildContext context) { + return DefaultTextStyle( + style: subtitleStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: Row( + children: [ + Expanded(child: subtitle ?? const SizedBox.shrink()), + ?subtitleTrailing, + ], + ), + ); + } +} + +class _StreamChannelListItemThemeDefaults extends StreamChannelListItemThemeData { + _StreamChannelListItemThemeDefaults(this._context); + + final BuildContext _context; + + late final _colorScheme = _context.streamColorScheme; + late final _textTheme = _context.streamTextTheme; + + @override + TextStyle get titleStyle => _textTheme.headingSm.copyWith(color: _colorScheme.textPrimary); + + @override + TextStyle get subtitleStyle => _textTheme.captionDefault.copyWith(color: _colorScheme.textSecondary); + + @override + TextStyle get timestampStyle => _textTheme.captionDefault.copyWith(color: _colorScheme.textTertiary); + + @override + Color get borderColor => _colorScheme.borderSubtle; + + @override + MuteIconPosition get muteIconPosition => MuteIconPosition.title; +} + +/// Shows the delivery status icon + "You:" prefix for outgoing messages in +/// the channel list. +/// +/// Unlike [StreamSendingIndicator], this widget does not show a read count +/// number. It only shows: +/// - Clock icon + "You:" (sending) +/// - Single check + "You:" (sent) +/// - Double check grey + "You:" (delivered) +/// - Double check blue + "You:" (read) +class _ChannelListDeliveryStatus extends StatelessWidget { + const _ChannelListDeliveryStatus({ + required this.channel, + required this.message, + }); + + final Channel channel; + final Message message; + + @override + Widget build(BuildContext context) { + final colorTheme = context.streamMessageTheme.mergeWithDefaults(context); + final colorScheme = context.streamColorScheme; + + return BetterStreamBuilder>( + stream: channel.state?.readStream, + initialData: channel.state?.read, + builder: (context, data) { + final isRead = data.readsOf(message: message).isNotEmpty; + final isDelivered = data.deliveriesOf(message: message).isNotEmpty; + + final Widget icon; + if (isRead) { + icon = Icon( + context.streamIcons.checks16, + size: 16, + color: colorTheme.outgoing?.textReadColor ?? colorScheme.accentPrimary, + ); + } else if (isDelivered) { + icon = Icon( + context.streamIcons.checks16, + size: 16, + color: colorTheme.outgoing?.textTimestampColor ?? colorScheme.textTertiary, + ); + } else if (message.state.isCompleted) { + icon = Icon( + context.streamIcons.checkmark16, + size: 16, + color: colorTheme.outgoing?.textTimestampColor ?? colorScheme.textTertiary, + ); + } else if (message.state.isOutgoing) { + icon = Icon( + context.streamIcons.clock16, + size: 16, + color: colorTheme.outgoing?.textTimestampColor ?? colorScheme.textTertiary, + ); + } else { + return const Empty(); + } + + return Padding( + padding: const EdgeInsetsDirectional.only(end: 4), + child: icon, + ); + }, + ); + } +} + +/// A widget that displays the channel last message date. +class ChannelLastMessageDate extends StatelessWidget { + /// Creates a new instance of the [ChannelLastMessageDate] widget. + ChannelLastMessageDate({ + super.key, + required this.channel, + this.textStyle, + this.formatter, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ); + + /// The channel to display the last message date for. + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + /// The formatter to format the date. + final DateFormatter? formatter; + + @override + Widget build(BuildContext context) { + return BetterStreamBuilder( + stream: channel.lastMessageAtStream, + initialData: channel.lastMessageAt, + builder: (context, lastMessageAt) => StreamTimestamp( + date: lastMessageAt.toLocal(), + style: textStyle, + formatter: formatter, + ), + ); + } +} + +/// A widget that displays the subtitle for [StreamChannelListItem]. +/// +/// Shows typing indicators, draft messages, or the last message preview. +/// The delivery status prefix (icon + "You:") is only shown when the subtitle +/// displays an actual sent message from the current user (not for drafts or +/// typing indicators). +class ChannelListTileSubtitle extends StatelessWidget { + /// Creates a new instance of [StreamChannelListTileSubtitle] widget. + ChannelListTileSubtitle({ + super.key, + required this.channel, + this.textStyle, + this.sendingIndicatorBuilder, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ); + + /// The channel to create the subtitle from. + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + /// The widget builder for the sending indicator. + /// + /// `Message` is the last message in the channel. Use it to determine the + /// status using [Message.state]. + final Widget Function(BuildContext, Message)? sendingIndicatorBuilder; + + @override + Widget build(BuildContext context) { + return StreamTypingIndicator( + channel: channel, + style: textStyle, + alternativeWidget: _ChannelLastMessageWithStatus( + channel: channel, + textStyle: textStyle, + sendingIndicatorBuilder: sendingIndicatorBuilder, + ), + ); + } +} + +/// Combines the delivery status prefix with the last message text. +/// +/// Shows the delivery status only when the displayed content is an actual +/// sent message from the current user (not a draft). +class _ChannelLastMessageWithStatus extends StatefulWidget { + const _ChannelLastMessageWithStatus({ + required this.channel, + this.textStyle, + this.sendingIndicatorBuilder, + }); + + final Channel channel; + final TextStyle? textStyle; + final Widget Function(BuildContext, Message)? sendingIndicatorBuilder; + + @override + State<_ChannelLastMessageWithStatus> createState() => _ChannelLastMessageWithStatusState(); +} + +class _ChannelLastMessageWithStatusState extends State<_ChannelLastMessageWithStatus> { + Message? _currentLastMessage; + + static bool _defaultLastMessagePredicate(Message message) { + if (message.isShadowed) return false; + if (message.isError) return false; + if (message.isEphemeral) return false; + + return true; + } + + @override + Widget build(BuildContext context) { + final channelState = widget.channel.state; + if (channelState == null) return const Empty(); + + final currentUser = widget.channel.client.state.currentUser; + + return BetterStreamBuilder<(Draft?, List)>( + stream: CombineLatestStream.combine2( + channelState.draftStream, + channelState.messagesStream, + (draft, messages) => (draft, messages), + ), + initialData: (channelState.draft, channelState.messages), + builder: (context, data) { + final (draft, messages) = data; + + // If there's a draft, show only the draft preview (no delivery status). + if (draft?.message case final draftMessage?) { + return StreamDraftMessagePreviewText( + draftMessage: draftMessage, + textStyle: widget.textStyle, + ); + } + + // Find the last valid message. + final message = messages.lastWhereOrNull( + _defaultLastMessagePredicate, + ); + final latestLastMessage = [message, _currentLastMessage].latest; + + if (latestLastMessage == null) { + return Text( + context.translations.emptyMessagesText, + style: widget.textStyle?.copyWith(color: context.streamColorScheme.textTertiary), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + + final isOwnMessage = currentUser != null && latestLastMessage.user?.id == currentUser.id; + + // Show delivery status prefix only for own messages. + final Widget deliveryPrefix; + if (isOwnMessage) { + deliveryPrefix = + widget.sendingIndicatorBuilder?.call(context, latestLastMessage) ?? + _ChannelListDeliveryStatus( + channel: widget.channel, + message: latestLastMessage, + ); + } else { + deliveryPrefix = const Empty(); + } + + return Row( + children: [ + if (!latestLastMessage.isDeleted) deliveryPrefix, + Flexible( + child: StreamMessagePreviewText( + message: latestLastMessage, + textStyle: widget.textStyle, + channel: channelState.channelState.channel, + ), + ), + ], + ); + }, + ); + } +} + +/// A widget that displays the last message of a channel. +class ChannelLastMessageText extends StatefulWidget { + /// Creates a new instance of [ChannelLastMessageText] widget. + ChannelLastMessageText({ + super.key, + required this.channel, + this.textStyle, + this.lastMessagePredicate = _defaultLastMessagePredicate, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ); + + /// The channel to display the last message of. + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + /// The predicate to determine if the message should be considered for the + /// last message. + /// + /// This predicate is used to filter out messages that should not be + /// considered for the last message. + final bool Function(Message) lastMessagePredicate; + + // The default predicate to determine if the message should be + // considered for the last message. + static bool _defaultLastMessagePredicate(Message message) { + if (message.isShadowed) return false; + if (message.isError) return false; + if (message.isEphemeral) return false; + + return true; + } + + @override + State createState() => _ChannelLastMessageTextState(); +} + +class _ChannelLastMessageTextState extends State { + Message? _currentLastMessage; + + @override + Widget build(BuildContext context) { + final channelState = widget.channel.state; + if (channelState == null) return const Empty(); + + return BetterStreamBuilder<(Draft?, List)>( + stream: CombineLatestStream.combine2( + channelState.draftStream, + channelState.messagesStream, + (draft, messages) => (draft, messages), + ), + initialData: (channelState.draft, channelState.messages), + builder: (context, data) { + final (draft, messages) = data; + + // Prioritize the draft message if it exists. + if (draft?.message case final draftMessage?) { + return StreamDraftMessagePreviewText( + draftMessage: draftMessage, + textStyle: widget.textStyle, + ); + } + + // Otherwise, show the channel last message if it exists. + final message = messages.lastWhereOrNull(widget.lastMessagePredicate); + final latestLastMessage = [message, _currentLastMessage].latest; + + if (latestLastMessage == null) { + return Text( + maxLines: 1, + context.translations.emptyMessagesText, + style: widget.textStyle, + overflow: TextOverflow.ellipsis, + ); + } + + return StreamMessagePreviewText( + message: latestLastMessage, + textStyle: widget.textStyle, + channel: channelState.channelState.channel, + ); + }, + ); + } +} + +extension on Iterable { + Message? get latest { + return reduce((a, b) { + if (a == null) return b; + if (b == null) return a; + + if (a.createdAt.isAfter(b.createdAt)) return a; + return b; + }); + } +} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_skeleton_loading.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_skeleton_loading.dart new file mode 100644 index 0000000000..005f4d3fe3 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_skeleton_loading.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// A shimmer loading placeholder for the channel list view. +/// +/// Displays a skeleton UI with shimmer animation using +/// [StreamSkeletonLoading] and [StreamSkeletonBox] from the core package. +class StreamChannelListSkeletonLoading extends StatelessWidget { + /// Creates a new instance of [StreamChannelListSkeletonLoading]. + const StreamChannelListSkeletonLoading({ + super.key, + this.itemCount = 7, + }); + + /// The number of skeleton items to display. + final int itemCount; + + @override + Widget build(BuildContext context) { + return StreamSkeletonLoading( + child: ListView.separated( + physics: const NeverScrollableScrollPhysics(), + itemCount: itemCount, + separatorBuilder: (context, index) => const SizedBox(height: 1), + itemBuilder: (context, index) => const _StreamChannelListItemSkeleton(), + ), + ); + } +} + +class _StreamChannelListItemSkeleton extends StatelessWidget { + const _StreamChannelListItemSkeleton(); + + @override + Widget build(BuildContext context) { + final spacing = context.streamSpacing; + + return Padding( + padding: EdgeInsets.all(spacing.md), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + spacing: spacing.md, + children: [ + const StreamSkeletonBox.circular(radius: 24), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: spacing.xs, + children: [ + Row( + children: [ + Expanded( + child: StreamSkeletonBox( + width: double.infinity, + height: 16, + borderRadius: BorderRadius.all(context.streamRadius.max), + ), + ), + SizedBox(width: spacing.md), + StreamSkeletonBox( + width: 48, + height: 16, + borderRadius: BorderRadius.all(context.streamRadius.max), + ), + ], + ), + Row( + children: [ + Expanded( + flex: 3, + child: StreamSkeletonBox( + width: double.infinity, + height: 16, + borderRadius: BorderRadius.all(context.streamRadius.max), + ), + ), + const Spacer( + flex: 2, + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_tile.dart deleted file mode 100644 index 1d950ce44f..0000000000 --- a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_tile.dart +++ /dev/null @@ -1,445 +0,0 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:stream_chat_flutter/src/message_widget/sending_indicator_builder.dart'; -import 'package:stream_chat_flutter/src/misc/empty_widget.dart'; -import 'package:stream_chat_flutter/src/misc/timestamp.dart'; -import 'package:stream_chat_flutter/src/utils/date_formatter.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// A widget that displays a channel preview. -/// -/// This widget is intended to be used as a Tile in [StreamChannelListView] -/// -/// It shows the last message of the channel, the last message time, the unread -/// message count, the typing indicator, the sending indicator and the channel -/// avatar. -/// -/// See also: -/// * [StreamChannelAvatar] -/// * [StreamChannelName] -class StreamChannelListTile extends StatelessWidget { - /// Creates a new instance of [StreamChannelListTile] widget. - StreamChannelListTile({ - super.key, - required this.channel, - this.leading, - this.title, - this.subtitle, - this.trailing, - this.onTap, - this.onLongPress, - this.tileColor, - this.visualDensity = VisualDensity.compact, - this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), - this.unreadIndicatorBuilder, - this.sendingIndicatorBuilder, - this.selected = false, - this.selectedTileColor, - }) : assert( - channel.state != null, - 'Channel ${channel.id} is not initialized', - ); - - /// The channel to display. - final Channel channel; - - /// A widget to display before the title. - final Widget? leading; - - /// The primary content of the list tile. - final Widget? title; - - /// Additional content displayed below the title. - final Widget? subtitle; - - /// A widget to display at the end of tile. - final Widget? trailing; - - /// Called when the user taps this list tile. - final GestureTapCallback? onTap; - - /// Called when the user long-presses on this list tile. - final GestureLongPressCallback? onLongPress; - - /// {@template flutter.material.ListTile.tileColor} - /// Defines the background color of `ListTile`. - /// - /// When the value is null, - /// the `tileColor` is set to [ListTileTheme.tileColor] - /// if it's not null and to [Colors.transparent] if it's null. - /// {@endtemplate} - final Color? tileColor; - - /// Defines how compact the list tile's layout will be. - /// - /// {@macro flutter.material.themedata.visualDensity} - /// - /// See also: - /// - /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all - /// widgets within a [Theme]. - final VisualDensity visualDensity; - - /// The tile's internal padding. - /// - /// Insets a [ListTile]'s contents: its [leading], [title], [subtitle], - /// and [trailing] widgets. - /// - /// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used. - final EdgeInsetsGeometry contentPadding; - - /// The widget builder for the unread indicator. - final WidgetBuilder? unreadIndicatorBuilder; - - /// The widget builder for the sending indicator. - /// - /// `Message` is the last message in the channel, Use it to determine the - /// status using [Message.state]. - final Widget Function(BuildContext, Message)? sendingIndicatorBuilder; - - /// True if the tile is in a selected state. - final bool selected; - - /// The color of the tile in selected state. - final Color? selectedTileColor; - - /// Creates a copy of this tile but with the given fields replaced with - /// the new values. - StreamChannelListTile copyWith({ - Key? key, - Channel? channel, - Widget? leading, - Widget? title, - Widget? subtitle, - VoidCallback? onTap, - VoidCallback? onLongPress, - VisualDensity? visualDensity, - EdgeInsetsGeometry? contentPadding, - bool? selected, - Widget Function(BuildContext, Message)? sendingIndicatorBuilder, - Color? tileColor, - Color? selectedTileColor, - WidgetBuilder? unreadIndicatorBuilder, - Widget? trailing, - }) { - return StreamChannelListTile( - key: key ?? this.key, - channel: channel ?? this.channel, - leading: leading ?? this.leading, - title: title ?? this.title, - subtitle: subtitle ?? this.subtitle, - onTap: onTap ?? this.onTap, - onLongPress: onLongPress ?? this.onLongPress, - visualDensity: visualDensity ?? this.visualDensity, - contentPadding: contentPadding ?? this.contentPadding, - sendingIndicatorBuilder: - sendingIndicatorBuilder ?? this.sendingIndicatorBuilder, - tileColor: tileColor ?? this.tileColor, - trailing: trailing ?? this.trailing, - unreadIndicatorBuilder: - unreadIndicatorBuilder ?? this.unreadIndicatorBuilder, - selected: selected ?? this.selected, - selectedTileColor: selectedTileColor ?? this.selectedTileColor, - ); - } - - @override - Widget build(BuildContext context) { - final channelState = channel.state!; - final currentUser = channel.client.state.currentUser!; - - final channelPreviewTheme = StreamChannelPreviewTheme.of(context); - final streamChatTheme = StreamChatTheme.of(context); - final streamChat = StreamChat.of(context); - - final leading = this.leading ?? - StreamChannelAvatar( - channel: channel, - ); - - final title = this.title ?? - StreamChannelName( - channel: channel, - textStyle: channelPreviewTheme.titleStyle, - ); - - final subtitle = this.subtitle ?? - ChannelListTileSubtitle( - channel: channel, - textStyle: channelPreviewTheme.subtitleStyle, - ); - - final trailing = this.trailing ?? - ChannelLastMessageDate( - channel: channel, - textStyle: channelPreviewTheme.lastMessageAtStyle, - formatter: channelPreviewTheme.lastMessageAtFormatter, - ); - - return BetterStreamBuilder( - stream: channel.isMutedStream, - initialData: channel.isMuted, - builder: (context, isMuted) => AnimatedOpacity( - opacity: isMuted ? 0.5 : 1, - duration: const Duration(milliseconds: 300), - child: ListTile( - onTap: onTap, - onLongPress: onLongPress, - visualDensity: visualDensity, - contentPadding: contentPadding, - leading: leading, - tileColor: tileColor, - selected: selected, - selectedTileColor: selectedTileColor ?? - StreamChatTheme.of(context).colorTheme.borders, - title: Row( - children: [ - Expanded(child: title), - BetterStreamBuilder>( - stream: channelState.membersStream, - initialData: channelState.members, - comparator: const ListEquality().equals, - builder: (context, members) { - if (members.isEmpty) { - return const Empty(); - } - return unreadIndicatorBuilder?.call(context) ?? - StreamUnreadIndicator.channels(cid: channel.cid); - }, - ), - ], - ), - subtitle: Row( - children: [ - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: subtitle, - ), - ), - BetterStreamBuilder>( - stream: channelState.messagesStream, - initialData: channelState.messages, - comparator: const ListEquality().equals, - builder: (context, messages) { - final lastMessage = messages.lastWhereOrNull( - (m) => !m.shadowed && !m.isDeleted, - ); - - if (lastMessage == null || - (lastMessage.user?.id != currentUser.id)) { - return const Empty(); - } - - final hasNonUrlAttachments = lastMessage.attachments - .any((it) => it.type != AttachmentType.urlPreview); - - return Padding( - padding: const EdgeInsets.only(right: 4), - child: - sendingIndicatorBuilder?.call(context, lastMessage) ?? - SendingIndicatorBuilder( - messageTheme: streamChatTheme.ownMessageTheme, - message: lastMessage, - hasNonUrlAttachments: hasNonUrlAttachments, - streamChat: streamChat, - streamChatTheme: streamChatTheme, - channel: channel, - ), - ); - }, - ), - trailing, - ], - ), - ), - ), - ); - } -} - -/// A widget that displays the channel last message date. -class ChannelLastMessageDate extends StatelessWidget { - /// Creates a new instance of the [ChannelLastMessageDate] widget. - ChannelLastMessageDate({ - super.key, - required this.channel, - this.textStyle, - this.formatter, - }) : assert( - channel.state != null, - 'Channel ${channel.id} is not initialized', - ); - - /// The channel to display the last message date for. - final Channel channel; - - /// The style of the text displayed - final TextStyle? textStyle; - - /// The formatter to format the date. - final DateFormatter? formatter; - - @override - Widget build(BuildContext context) { - return BetterStreamBuilder( - stream: channel.lastMessageAtStream, - initialData: channel.lastMessageAt, - builder: (context, lastMessageAt) => StreamTimestamp( - date: lastMessageAt.toLocal(), - style: textStyle, - formatter: formatter, - ), - ); - } -} - -/// A widget that displays the subtitle for [StreamChannelListTile]. -class ChannelListTileSubtitle extends StatelessWidget { - /// Creates a new instance of [StreamChannelListTileSubtitle] widget. - ChannelListTileSubtitle({ - super.key, - required this.channel, - this.textStyle, - }) : assert( - channel.state != null, - 'Channel ${channel.id} is not initialized', - ); - - /// The channel to create the subtitle from. - final Channel channel; - - /// The style of the text displayed - final TextStyle? textStyle; - - @override - Widget build(BuildContext context) { - if (channel.isMuted) { - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - const StreamSvgIcon(size: 16, icon: StreamSvgIcons.mute), - Expanded( - child: Text( - ' ${context.translations.channelIsMutedText}', - style: textStyle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ); - } - return StreamTypingIndicator( - channel: channel, - style: textStyle, - alternativeWidget: ChannelLastMessageText( - channel: channel, - textStyle: textStyle, - ), - ); - } -} - -/// A widget that displays the last message of a channel. -class ChannelLastMessageText extends StatefulWidget { - /// Creates a new instance of [ChannelLastMessageText] widget. - ChannelLastMessageText({ - super.key, - required this.channel, - this.textStyle, - this.lastMessagePredicate = _defaultLastMessagePredicate, - }) : assert( - channel.state != null, - 'Channel ${channel.id} is not initialized', - ); - - /// The channel to display the last message of. - final Channel channel; - - /// The style of the text displayed - final TextStyle? textStyle; - - /// The predicate to determine if the message should be considered for the - /// last message. - /// - /// This predicate is used to filter out messages that should not be - /// considered for the last message. - final bool Function(Message) lastMessagePredicate; - - // The default predicate to determine if the message should be - // considered for the last message. - static bool _defaultLastMessagePredicate(Message message) { - if (message.isShadowed) return false; - if (message.isDeleted) return false; - if (message.isError) return false; - if (message.isEphemeral) return false; - - return true; - } - - @override - State createState() => _ChannelLastMessageTextState(); -} - -class _ChannelLastMessageTextState extends State { - Message? _currentLastMessage; - - @override - Widget build(BuildContext context) { - final channelState = widget.channel.state; - if (channelState == null) return const Empty(); - - return BetterStreamBuilder<(Draft?, List)>( - stream: CombineLatestStream.combine2( - channelState.draftStream, - channelState.messagesStream, - (draft, messages) => (draft, messages), - ), - initialData: (channelState.draft, channelState.messages), - builder: (context, data) { - final (draft, messages) = data; - - // Prioritize the draft message if it exists. - if (draft?.message case final draftMessage?) { - return StreamDraftMessagePreviewText( - draftMessage: draftMessage, - textStyle: widget.textStyle, - ); - } - - // Otherwise, show the channel last message if it exists. - final message = messages.lastWhereOrNull(widget.lastMessagePredicate); - final latestLastMessage = [message, _currentLastMessage].latest; - - if (latestLastMessage == null) { - return Text( - maxLines: 1, - context.translations.emptyMessagesText, - style: widget.textStyle, - overflow: TextOverflow.ellipsis, - ); - } - - return StreamMessagePreviewText( - message: latestLastMessage, - textStyle: widget.textStyle, - channel: channelState.channelState.channel, - ); - }, - ); - } -} - -extension on Iterable { - Message? get latest { - return reduce((a, b) { - if (a == null) return b; - if (b == null) return a; - - if (a.createdAt.isAfter(b.createdAt)) return a; - return b; - }); - } -} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_view.dart index 3541963b3f..94648cb486 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/channel_scroll_view/stream_channel_list_view.dart @@ -1,26 +1,26 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/scroll_view/channel_scroll_view/stream_channel_list_empty_state.dart'; +import 'package:stream_chat_flutter/src/scroll_view/channel_scroll_view/stream_channel_list_skeleton_loading.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default separator builder for [StreamChannelListView]. Widget defaultChannelListViewSeparatorBuilder( BuildContext context, List items, int index, -) => - const StreamChannelListSeparator(); +) => const StreamChannelListSeparator(); /// Signature for the item builder that creates the children of the /// [StreamChannelListView]. -typedef StreamChannelListViewIndexedWidgetBuilder - = StreamScrollViewIndexedWidgetBuilder; +typedef StreamChannelListViewIndexedWidgetBuilder = + StreamScrollViewIndexedWidgetBuilder; /// A [ListView] that shows a list of [Channel]s, -/// it uses [StreamChannelListTile] as a default item. +/// it uses [StreamChannelListItem] as a default item. /// /// This is the new version of [StreamChannelListView] that uses /// [StreamChannelListController]. @@ -40,7 +40,7 @@ typedef StreamChannelListViewIndexedWidgetBuilder /// ``` /// /// See also: -/// * [StreamChannelListTile] +/// * [StreamChannelListItem] /// * [StreamChannelListController] class StreamChannelListView extends StatelessWidget { /// Creates a new instance of [StreamChannelListView]. @@ -304,7 +304,7 @@ class StreamChannelListView extends StatelessWidget { final onTap = onChannelTap; final onLongPress = onChannelLongPress; - final streamChannelListTile = StreamChannelListTile( + final streamChannelListTile = StreamChannelListItem( channel: channel, onTap: onTap == null ? null : () => onTap(channel), onLongPress: onLongPress == null ? null : () => onLongPress(channel), @@ -318,42 +318,18 @@ class StreamChannelListView extends StatelessWidget { ) ?? streamChannelListTile; }, - emptyBuilder: (context) { - final chatThemeData = StreamChatTheme.of(context); - return emptyBuilder?.call(context) ?? - Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( - size: 148, - icon: StreamSvgIcons.message, - color: chatThemeData.colorTheme.disabled, - ), - emptyTitle: Text( - context.translations.letsStartChattingLabel, - style: chatThemeData.textTheme.headline, - ), - ), - ), - ); - }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.list( + emptyBuilder: (context) => emptyBuilder?.call(context) ?? const StreamChannelListEmptyState(), + loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.list( onTap: controller.retry, error: Text(context.translations.loadingChannelsError), ), - loadMoreIndicatorBuilder: (context) => const Center( + loadMoreIndicatorBuilder: (context) => Center( child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), ), ), - loadingBuilder: (context) => - loadingBuilder?.call(context) ?? - const Center( - child: StreamScrollViewLoadingWidget(), - ), + loadingBuilder: (context) => loadingBuilder?.call(context) ?? const StreamChannelListSkeletonLoading(), errorBuilder: (context, error) => errorBuilder?.call(context, error) ?? Center( @@ -367,7 +343,7 @@ class StreamChannelListView extends StatelessWidget { } /// A widget that is used to display a separator between -/// [StreamChannelListTile] items. +/// [StreamChannelListItem] items. class StreamChannelListSeparator extends StatelessWidget { /// Creates a new instance of [StreamChannelListSeparator]. const StreamChannelListSeparator({super.key}); diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/draft_scroll_view/stream_draft_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/draft_scroll_view/stream_draft_list_view.dart index 1c59029017..9869084406 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/draft_scroll_view/stream_draft_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/draft_scroll_view/stream_draft_list_view.dart @@ -2,22 +2,20 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default separator builder for [StreamDraftListView]. Widget defaultDraftListViewSeparatorBuilder( BuildContext context, List drafts, int index, -) => - const StreamDraftListSeparator(); +) => const StreamDraftListSeparator(); /// Signature for the item builder that creates the children of the /// [StreamDraftListView]. -typedef StreamDraftListViewIndexedWidgetBuilder - = StreamScrollViewIndexedWidgetBuilder; +typedef StreamDraftListViewIndexedWidgetBuilder = StreamScrollViewIndexedWidgetBuilder; /// {@template streamDraftListView} /// A [ListView] that shows a list of [Draft]'s. It uses a @@ -318,9 +316,9 @@ class StreamDraftListView extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(8), child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( + emptyIcon: Icon( + context.streamIcons.edit32, size: 148, - icon: StreamSvgIcons.edit, color: chatThemeData.colorTheme.disabled, ), emptyTitle: Text( @@ -331,15 +329,14 @@ class StreamDraftListView extends StatelessWidget { ), ); }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.list( + loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.list( onTap: controller.retry, error: Text(context.translations.loadingMessagesError), ), - loadMoreIndicatorBuilder: (context) => const Center( + loadMoreIndicatorBuilder: (context) => Center( child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), ), ), loadingBuilder: (context) => diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_grid_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_grid_view.dart index adf97a7860..0068b50890 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_grid_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_grid_view.dart @@ -2,18 +2,16 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default grid delegate for [StreamMemberGridView]. -const defaultMemberGridViewDelegate = - SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4); +const defaultMemberGridViewDelegate = SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4); /// Signature for the item builder that creates the children of the /// [StreamMemberGridView]. -typedef StreamMemberGridViewIndexedWidgetBuilder - = StreamScrollViewIndexedWidgetBuilder; +typedef StreamMemberGridViewIndexedWidgetBuilder = StreamScrollViewIndexedWidgetBuilder; /// Signature for the member grid tile, currently equal to [StreamUserGridTile]. typedef StreamMemberGridTile = StreamUserGridTile; @@ -352,9 +350,9 @@ class StreamMemberGridView extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(8), child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( + emptyIcon: Icon( + context.streamIcons.user32, size: 148, - icon: StreamSvgIcons.user, color: chatThemeData.colorTheme.disabled, ), emptyTitle: Text( @@ -365,18 +363,17 @@ class StreamMemberGridView extends StatelessWidget { ), ); }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.grid( + loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.grid( onTap: controller.retry, error: Text( context.translations.loadingUsersError, textAlign: TextAlign.center, ), ), - loadMoreIndicatorBuilder: (context) => const Center( + loadMoreIndicatorBuilder: (context) => Center( child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), ), ), loadingBuilder: (context) => diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_list_view.dart index fcdad5379e..f6a65ec55b 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/member_scroll_view/stream_member_list_view.dart @@ -2,22 +2,20 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default separator builder for [StreamMemberListView]. Widget defaultMemberListViewSeparatorBuilder( BuildContext context, List members, int index, -) => - const StreamUserListSeparator(); +) => const StreamUserListSeparator(); /// Signature for the item builder that creates the children of the /// [StreamMemberListView]. -typedef StreamMemberListViewIndexedWidgetBuilder - = StreamScrollViewIndexedWidgetBuilder; +typedef StreamMemberListViewIndexedWidgetBuilder = StreamScrollViewIndexedWidgetBuilder; /// Signature for the member grid tile, currently equal to [StreamUserListTile]. typedef StreamMemberListTile = StreamUserListTile; @@ -278,86 +276,85 @@ class StreamMemberListView extends StatelessWidget { @override Widget build(BuildContext context) => PagedValueListView( - scrollDirection: scrollDirection, - padding: padding, - physics: physics, - reverse: reverse, - controller: controller, - scrollController: scrollController, - primary: primary, - shrinkWrap: shrinkWrap, - addAutomaticKeepAlives: addAutomaticKeepAlives, - addRepaintBoundaries: addRepaintBoundaries, - addSemanticIndexes: addSemanticIndexes, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - dragStartBehavior: dragStartBehavior, - cacheExtent: cacheExtent, - clipBehavior: clipBehavior, - loadMoreTriggerIndex: loadMoreTriggerIndex, - separatorBuilder: separatorBuilder, - itemBuilder: (context, members, index) { - final member = members[index]; - final onTap = onMemberTap; - final onLongPress = onMemberLongPress; + scrollDirection: scrollDirection, + padding: padding, + physics: physics, + reverse: reverse, + controller: controller, + scrollController: scrollController, + primary: primary, + shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + dragStartBehavior: dragStartBehavior, + cacheExtent: cacheExtent, + clipBehavior: clipBehavior, + loadMoreTriggerIndex: loadMoreTriggerIndex, + separatorBuilder: separatorBuilder, + itemBuilder: (context, members, index) { + final member = members[index]; + final onTap = onMemberTap; + final onLongPress = onMemberLongPress; - final streamUserListTile = StreamMemberListTile( - user: member.user!, - onTap: onTap == null ? null : () => onTap(member), - onLongPress: onLongPress == null ? null : () => onLongPress(member), - ); + final streamUserListTile = StreamMemberListTile( + user: member.user!, + onTap: onTap == null ? null : () => onTap(member), + onLongPress: onLongPress == null ? null : () => onLongPress(member), + ); - return itemBuilder?.call( - context, - members, - index, - streamUserListTile, - ) ?? - streamUserListTile; - }, - emptyBuilder: (context) { - final chatThemeData = StreamChatTheme.of(context); - return emptyBuilder?.call(context) ?? - Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( - size: 148, - icon: StreamSvgIcons.user, - color: chatThemeData.colorTheme.disabled, - ), - emptyTitle: Text( - context.translations.noUsersLabel, - style: chatThemeData.textTheme.headline, - ), - ), + return itemBuilder?.call( + context, + members, + index, + streamUserListTile, + ) ?? + streamUserListTile; + }, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: Icon( + context.streamIcons.user32, + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.noUsersLabel, + style: chatThemeData.textTheme.headline, ), - ); - }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.list( - onTap: controller.retry, - error: Text(context.translations.loadingUsersError), + ), + ), + ); + }, + loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.list( + onTap: controller.retry, + error: Text(context.translations.loadingUsersError), + ), + loadMoreIndicatorBuilder: (context) => Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), + ), + ), + loadingBuilder: (context) => + loadingBuilder?.call(context) ?? + const Center( + child: StreamScrollViewLoadingWidget(), ), - loadMoreIndicatorBuilder: (context) => const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), + errorBuilder: (context, error) => + errorBuilder?.call(context, error) ?? + Center( + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingUsersError), + onRetryPressed: controller.refresh, ), ), - loadingBuilder: (context) => - loadingBuilder?.call(context) ?? - const Center( - child: StreamScrollViewLoadingWidget(), - ), - errorBuilder: (context, error) => - errorBuilder?.call(context, error) ?? - Center( - child: StreamScrollViewErrorWidget( - errorTitle: Text(context.translations.loadingUsersError), - onRetryPressed: controller.refresh, - ), - ), - ); + ); } diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart index e7a939f942..9e0f76fab9 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_grid_view.dart @@ -2,18 +2,16 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default grid delegate for [StreamMessageSearchGridView]. -const defaultMessageSearchGridViewDelegate = - SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4); +const defaultMessageSearchGridViewDelegate = SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4); /// Signature for the item builder that creates the children of the /// [StreamMessageSearchGridView]. -typedef StreamMessageSearchGridViewIndexedWidgetBuilder - = PagedValueScrollViewIndexedWidgetBuilder; +typedef StreamMessageSearchGridViewIndexedWidgetBuilder = PagedValueScrollViewIndexedWidgetBuilder; /// A [GridView] that shows a grid of [GetMessageResponse]s, /// it uses [StreamMessageSearchGridTile] as a default item. @@ -323,9 +321,9 @@ class StreamMessageSearchGridView extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(8), child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( + emptyIcon: Icon( + context.streamIcons.messageBubble32, size: 148, - icon: StreamSvgIcons.message, color: chatThemeData.colorTheme.disabled, ), emptyTitle: Text( @@ -336,15 +334,14 @@ class StreamMessageSearchGridView extends StatelessWidget { ), ); }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.grid( + loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.grid( onTap: controller.retry, error: Text(context.translations.loadingMessagesError), ), - loadMoreIndicatorBuilder: (context) => const Center( + loadMoreIndicatorBuilder: (context) => Center( child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), ), ), loadingBuilder: (context) => diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart index 23575f05bc..3a9be001a3 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_tile.dart @@ -91,20 +91,19 @@ class StreamMessageSearchListTile extends StatelessWidget { Color? tileColor, VisualDensity? visualDensity, EdgeInsetsGeometry? contentPadding, - }) => - StreamMessageSearchListTile( - key: key ?? this.key, - messageResponse: messageResponse ?? this.messageResponse, - leading: leading ?? this.leading, - title: title ?? this.title, - subtitle: subtitle ?? this.subtitle, - trailing: trailing ?? this.trailing, - onTap: onTap ?? this.onTap, - onLongPress: onLongPress ?? this.onLongPress, - tileColor: tileColor ?? this.tileColor, - visualDensity: visualDensity ?? this.visualDensity, - contentPadding: contentPadding ?? this.contentPadding, - ); + }) => StreamMessageSearchListTile( + key: key ?? this.key, + messageResponse: messageResponse ?? this.messageResponse, + leading: leading ?? this.leading, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + trailing: trailing ?? this.trailing, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + tileColor: tileColor ?? this.tileColor, + visualDensity: visualDensity ?? this.visualDensity, + contentPadding: contentPadding ?? this.contentPadding, + ); @override Widget build(BuildContext context) { @@ -112,23 +111,17 @@ class StreamMessageSearchListTile extends StatelessWidget { final user = message.user!; final channelPreviewTheme = StreamChannelPreviewTheme.of(context); - final leading = this.leading ?? - StreamUserAvatar( - user: user, - constraints: const BoxConstraints.tightFor( - height: 40, - width: 40, - ), - ); + final leading = this.leading ?? StreamUserAvatar(size: .lg, user: user); - final title = this.title ?? + final title = + this.title ?? MessageSearchListTileTitle( messageResponse: messageResponse, - textStyle: channelPreviewTheme.titleStyle - ?.copyWith(overflow: TextOverflow.ellipsis), + textStyle: channelPreviewTheme.titleStyle?.copyWith(overflow: TextOverflow.ellipsis), ); - final subtitle = this.subtitle ?? + final subtitle = + this.subtitle ?? Row( children: [ Expanded( @@ -185,9 +178,7 @@ class MessageSearchListTileTitle extends StatelessWidget { TextSpan( children: [ TextSpan( - text: user.id == StreamChat.of(context).currentUser?.id - ? context.translations.youText - : user.name, + text: user.id == StreamChat.of(context).currentUser?.id ? context.translations.youText : user.name, ), if (channelName != null) ...[ TextSpan( diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart index 2f4c75468f..170967a148 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/message_search_scroll_view/stream_message_search_list_view.dart @@ -2,23 +2,21 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default separator builder for [StreamMessageSearchListView]. Widget defaultMessageSearchListViewSeparatorBuilder( BuildContext context, List responses, int index, -) => - const StreamMessageSearchListSeparator(); +) => const StreamMessageSearchListSeparator(); /// Signature for the item builder that creates the children of the /// [StreamMessageSearchListView]. -typedef StreamMessageSearchListViewIndexedWidgetBuilder - = StreamScrollViewIndexedWidgetBuilder; +typedef StreamMessageSearchListViewIndexedWidgetBuilder = + StreamScrollViewIndexedWidgetBuilder; /// A [ListView] that shows a list of [GetMessageResponse]s, /// it uses [StreamMessageSearchListTile] as a default item. @@ -81,8 +79,7 @@ class StreamMessageSearchListView extends StatelessWidget { final StreamMessageSearchListViewIndexedWidgetBuilder? itemBuilder; /// A builder that is called to build the list separator. - final PagedValueScrollViewIndexedWidgetBuilder - separatorBuilder; + final PagedValueScrollViewIndexedWidgetBuilder separatorBuilder; /// A builder that is called to build the empty state of the list. final WidgetBuilder? emptyBuilder; @@ -308,8 +305,7 @@ class StreamMessageSearchListView extends StatelessWidget { final streamMessageSearchListTile = StreamMessageSearchListTile( messageResponse: messageResponse, onTap: onTap == null ? null : () => onTap(messageResponse), - onLongPress: - onLongPress == null ? null : () => onLongPress(messageResponse), + onLongPress: onLongPress == null ? null : () => onLongPress(messageResponse), ); return itemBuilder?.call( @@ -327,9 +323,9 @@ class StreamMessageSearchListView extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(8), child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( + emptyIcon: Icon( + context.streamIcons.messageBubble32, size: 148, - icon: StreamSvgIcons.message, color: chatThemeData.colorTheme.disabled, ), emptyTitle: Text( @@ -340,15 +336,14 @@ class StreamMessageSearchListView extends StatelessWidget { ), ); }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.list( + loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.list( onTap: controller.retry, error: Text(context.translations.loadingMessagesError), ), - loadMoreIndicatorBuilder: (context) => const Center( + loadMoreIndicatorBuilder: (context) => Center( child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), ), ), loadingBuilder: (context) => diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart index dcee2b76a4..82ec1a4b8e 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery.dart @@ -1,17 +1,15 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:photo_manager/photo_manager.dart' - show AssetEntity, ThumbnailFormat, ThumbnailSize; +import 'package:photo_manager/photo_manager.dart' show AssetEntity, ThumbnailFormat, ThumbnailSize; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default grid delegate for [StreamPhotoGallery]. -const defaultStreamPhotoGalleryDelegate = - SliverGridDelegateWithFixedCrossAxisCount( +const defaultStreamPhotoGalleryDelegate = SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 2, crossAxisSpacing: 2, @@ -19,8 +17,8 @@ const defaultStreamPhotoGalleryDelegate = /// Signature for the item builder that creates the children of the /// [StreamPhotoGallery]. -typedef StreamPhotoGalleryIndexedWidgetBuilder - = StreamScrollViewIndexedWidgetBuilder; +typedef StreamPhotoGalleryIndexedWidgetBuilder = + StreamScrollViewIndexedWidgetBuilder; /// Widget used to display a gallery of photos in the form of grid. class StreamPhotoGallery extends StatelessWidget { @@ -58,6 +56,7 @@ class StreamPhotoGallery extends StatelessWidget { this.thumbnailFormat = ThumbnailFormat.jpeg, this.thumbnailQuality = 100, this.thumbnailScale = 1, + this.addMoreBuilder, }); /// The [StreamPhotoGalleryController] used to control the grid of users. @@ -309,6 +308,11 @@ class StreamPhotoGallery extends StatelessWidget { /// Scale of the image. final double thumbnailScale; + /// An optional builder for a leading "Add more" tile shown as the first item + /// in the gallery grid. Useful when the user has limited photo library access + /// and needs a way to expand the selection. + final WidgetBuilder? addMoreBuilder; + @override Widget build(BuildContext context) { return PagedValueGridView( @@ -330,6 +334,7 @@ class StreamPhotoGallery extends StatelessWidget { restorationId: restorationId, clipBehavior: clipBehavior, loadMoreTriggerIndex: loadMoreTriggerIndex, + leadingItemBuilder: addMoreBuilder, gridDelegate: gridDelegate, itemBuilder: (context, mediaList, index) { final media = mediaList[index]; @@ -361,9 +366,9 @@ class StreamPhotoGallery extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(8), child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( + emptyIcon: Icon( + context.streamIcons.image32, size: 148, - icon: StreamSvgIcons.pictures, color: chatThemeData.colorTheme.disabled, ), emptyTitle: Text( @@ -384,10 +389,10 @@ class StreamPhotoGallery extends StatelessWidget { ); }, loadMoreIndicatorBuilder: (context) { - return const Center( + return Center( child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), ), ); }, diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart index eb9b21bbf9..8d25cb561e 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart @@ -3,8 +3,7 @@ import 'package:photo_manager/photo_manager.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// -class StreamPhotoGalleryController - extends PagedValueNotifier { +class StreamPhotoGalleryController extends PagedValueNotifier { /// StreamPhotoGalleryController({ this.limit = 50, diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart index 6b7ef7ebdc..0b3ced8025 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart @@ -3,8 +3,8 @@ import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/material.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/theme/stream_chat_theme.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Widget that displays a photo or video item from the gallery. class StreamPhotoGalleryTile extends StatelessWidget { @@ -60,18 +60,17 @@ class StreamPhotoGalleryTile extends StatelessWidget { ThumbnailFormat? thumbnailFormat, int? thumbnailQuality, double? thumbnailScale, - }) => - StreamPhotoGalleryTile( - key: key ?? this.key, - media: media ?? this.media, - selected: selected ?? this.selected, - onTap: onTap ?? this.onTap, - onLongPress: onLongPress ?? this.onLongPress, - thumbnailSize: thumbnailSize ?? this.thumbnailSize, - thumbnailFormat: thumbnailFormat ?? this.thumbnailFormat, - thumbnailQuality: thumbnailQuality ?? this.thumbnailQuality, - thumbnailScale: thumbnailScale ?? this.thumbnailScale, - ); + }) => StreamPhotoGalleryTile( + key: key ?? this.key, + media: media ?? this.media, + selected: selected ?? this.selected, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + thumbnailSize: thumbnailSize ?? this.thumbnailSize, + thumbnailFormat: thumbnailFormat ?? this.thumbnailFormat, + thumbnailQuality: thumbnailQuality ?? this.thumbnailQuality, + thumbnailScale: thumbnailScale ?? this.thumbnailScale, + ); @override Widget build(BuildContext context) { @@ -101,45 +100,32 @@ class StreamPhotoGalleryTile extends StatelessWidget { child: AnimatedOpacity( duration: const Duration(milliseconds: 300), opacity: selected ? 1.0 : 0.0, - child: Container( - color: - // ignore: deprecated_member_use - chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.5), - alignment: Alignment.topRight, - padding: const EdgeInsets.only( - top: 8, - right: 8, - ), - child: CircleAvatar( - radius: 12, - backgroundColor: chatThemeData.colorTheme.barsBg, - child: StreamSvgIcon( - size: 24, - icon: StreamSvgIcons.check, - color: chatThemeData.colorTheme.textHighEmphasis, - ), + child: DecoratedBox( + decoration: BoxDecoration( + // ignore: deprecated_member_use + color: chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.15), ), ), ), ), ), - if (media.type == AssetType.video) ...[ - const Positioned( - left: 8, - bottom: 10, - child: StreamSvgIcon(icon: StreamSvgIcons.videoCall), + Positioned( + top: 8, + right: 8, + child: IgnorePointer( + child: _GallerySelectedIndicator(selected: selected), ), + ), + if (media.type == AssetType.video) Positioned( - right: 4, - bottom: 10, - child: Text( - media.videoDuration.format(), - style: TextStyle( - color: chatThemeData.colorTheme.barsBg, - ), + left: 8, + bottom: 8, + child: StreamMediaBadge( + type: MediaBadgeType.video, + duration: media.videoDuration, + durationFormat: MediaBadgeDurationFormat.exact, ), ), - ], // https://stackoverflow.com/a/59317162/10036882 Positioned.fill( child: Material( @@ -155,14 +141,31 @@ class StreamPhotoGalleryTile extends StatelessWidget { } } -extension on Duration { - String format() { - final s = '$this'.split('.')[0].padLeft(8, '0'); - if (s.startsWith('00:')) { - return s.replaceFirst('00:', ''); - } +class _GallerySelectedIndicator extends StatelessWidget { + const _GallerySelectedIndicator({required this.selected}); + + final bool selected; - return s; + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: selected ? const Color(0xFF005FFF) : Colors.transparent, + border: Border.all(color: Colors.white, width: 2), + ), + child: selected + ? Icon( + context.streamIcons.checkmark12, + fontWeight: FontWeight.w900, + size: 12, + color: Colors.white, + ) + : null, + ); } } @@ -251,7 +254,8 @@ class MediaThumbnailProvider extends ImageProvider { int get hashCode => Object.hash(media, size, format, quality, scale); @override - String toString() => '$runtimeType(' + String toString() => + '$runtimeType(' 'media: $media, ' 'size: $size, ' 'format: $format, ' diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_tile.dart index 82ad280d74..1e9cad5cc8 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_tile.dart @@ -53,17 +53,16 @@ class StreamPollVoteListTile extends StatelessWidget { Color? tileColor, BorderRadiusGeometry? borderRadius, EdgeInsetsGeometry? contentPadding, - }) => - StreamPollVoteListTile( - key: key ?? this.key, - pollVote: pollVote ?? this.pollVote, - showAnswerText: showAnswerText ?? this.showAnswerText, - onTap: onTap ?? this.onTap, - onLongPress: onLongPress ?? this.onLongPress, - tileColor: tileColor ?? this.tileColor, - borderRadius: borderRadius ?? this.borderRadius, - contentPadding: contentPadding ?? this.contentPadding, - ); + }) => StreamPollVoteListTile( + key: key ?? this.key, + pollVote: pollVote ?? this.pollVote, + showAnswerText: showAnswerText ?? this.showAnswerText, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + tileColor: tileColor ?? this.tileColor, + borderRadius: borderRadius ?? this.borderRadius, + contentPadding: contentPadding ?? this.contentPadding, + ); @override Widget build(BuildContext context) { @@ -81,8 +80,7 @@ class StreamPollVoteListTile extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (pollVote.answerText case final answerText? - when showAnswerText) ...[ + if (pollVote.answerText case final answerText? when showAnswerText) ...[ Text( answerText, style: theme.textTheme.headlineBold.copyWith( @@ -95,10 +93,9 @@ class StreamPollVoteListTile extends StatelessWidget { children: [ if (pollVote.user case final user?) ...[ StreamUserAvatar( + size: .xs, user: user, - constraints: - BoxConstraints.tight(const Size.fromRadius(10)), - showOnlineStatus: false, + showOnlineIndicator: false, ), Expanded( child: Padding( @@ -118,7 +115,7 @@ class StreamPollVoteListTile extends StatelessWidget { dateTime: pollVote.updatedAt.toLocal(), ), ], - ) + ), ], ), ), diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_view.dart index 13aecdb471..dc2df2ce3c 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_view.dart @@ -1,29 +1,27 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_tile.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_empty_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_indexed_widget_builder.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_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'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default separator builder for [StreamPollVoteListView]. Widget defaultPollVoteListViewSeparatorBuilder( BuildContext context, List pollVotes, int index, -) => - const SizedBox(height: 8); +) => const SizedBox(height: 8); /// Signature for the item builder that creates the children of the /// [StreamPollVoteListView]. -typedef StreamPollVoteListViewIndexedWidgetBuilder - = StreamScrollViewIndexedWidgetBuilder; +typedef StreamPollVoteListViewIndexedWidgetBuilder = + StreamScrollViewIndexedWidgetBuilder; /// {@template streamPollVoteListView} /// A [ListView] that shows a list of [PollVote] for a poll. It uses a @@ -283,87 +281,85 @@ class StreamPollVoteListView extends StatelessWidget { @override Widget build(BuildContext context) => PagedValueListView( - scrollDirection: scrollDirection, - padding: padding, - physics: physics, - reverse: reverse, - controller: controller, - scrollController: scrollController, - primary: primary, - shrinkWrap: shrinkWrap, - addAutomaticKeepAlives: addAutomaticKeepAlives, - addRepaintBoundaries: addRepaintBoundaries, - addSemanticIndexes: addSemanticIndexes, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - dragStartBehavior: dragStartBehavior, - cacheExtent: cacheExtent, - clipBehavior: clipBehavior, - loadMoreTriggerIndex: loadMoreTriggerIndex, - separatorBuilder: separatorBuilder, - itemBuilder: (context, pollVotes, index) { - final pollVote = pollVotes[index]; - final onTap = onPollVoteTap; - final onLongPress = onPollVoteLongPress; + scrollDirection: scrollDirection, + padding: padding, + physics: physics, + reverse: reverse, + controller: controller, + scrollController: scrollController, + primary: primary, + shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + dragStartBehavior: dragStartBehavior, + cacheExtent: cacheExtent, + clipBehavior: clipBehavior, + loadMoreTriggerIndex: loadMoreTriggerIndex, + separatorBuilder: separatorBuilder, + itemBuilder: (context, pollVotes, index) { + final pollVote = pollVotes[index]; + final onTap = onPollVoteTap; + final onLongPress = onPollVoteLongPress; - final streamPollVoteListTile = StreamPollVoteListTile( - pollVote: pollVote, - onTap: onTap == null ? null : () => onTap(pollVote), - onLongPress: - onLongPress == null ? null : () => onLongPress(pollVote), - ); + final streamPollVoteListTile = StreamPollVoteListTile( + pollVote: pollVote, + onTap: onTap == null ? null : () => onTap(pollVote), + onLongPress: onLongPress == null ? null : () => onLongPress(pollVote), + ); - return itemBuilder?.call( - context, - pollVotes, - index, - streamPollVoteListTile, - ) ?? - streamPollVoteListTile; - }, - emptyBuilder: (context) { - final chatThemeData = StreamChatTheme.of(context); - return emptyBuilder?.call(context) ?? - Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( - size: 148, - icon: StreamSvgIcons.polls, - color: chatThemeData.colorTheme.disabled, - ), - emptyTitle: Text( - context.translations.noPollVotesLabel, - style: chatThemeData.textTheme.headline, - ), - ), + return itemBuilder?.call( + context, + pollVotes, + index, + streamPollVoteListTile, + ) ?? + streamPollVoteListTile; + }, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: Icon( + context.streamIcons.poll32, + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.noPollVotesLabel, + style: chatThemeData.textTheme.headline, ), - ); - }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.list( - onTap: controller.retry, - error: Text(context.translations.loadingPollVotesError), + ), + ), + ); + }, + loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.list( + onTap: controller.retry, + error: Text(context.translations.loadingPollVotesError), + ), + loadMoreIndicatorBuilder: (context) => Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), + ), + ), + loadingBuilder: (context) => + loadingBuilder?.call(context) ?? + const Center( + child: StreamScrollViewLoadingWidget(), ), - loadMoreIndicatorBuilder: (context) => const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), + errorBuilder: (context, error) => + errorBuilder?.call(context, error) ?? + Center( + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingPollVotesError), + onRetryPressed: controller.refresh, ), ), - loadingBuilder: (context) => - loadingBuilder?.call(context) ?? - const Center( - child: StreamScrollViewLoadingWidget(), - ), - errorBuilder: (context, error) => - errorBuilder?.call(context, error) ?? - Center( - child: StreamScrollViewErrorWidget( - errorTitle: Text(context.translations.loadingPollVotesError), - onRetryPressed: controller.refresh, - ), - ), - ); + ); } diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/reaction_scroll_view/stream_reaction_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/reaction_scroll_view/stream_reaction_list_view.dart new file mode 100644 index 0000000000..87afd904f3 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/scroll_view/reaction_scroll_view/stream_reaction_list_view.dart @@ -0,0 +1,224 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; +import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// Default separator builder for [StreamReactionListView]. +Widget defaultReactionListViewSeparatorBuilder( + BuildContext context, + List reactions, + int index, +) => const SizedBox.shrink(); + +/// Signature for the item builder that creates the children of the +/// [StreamReactionListView]. +typedef StreamReactionListViewIndexedWidgetBuilder = PagedValueScrollViewIndexedWidgetBuilder; + +/// {@template streamReactionListView} +/// A [ListView] that shows a list of [Reaction]s. It uses a +/// [StreamReactionListController] to load the reactions in paginated form. +/// +/// Example: +/// +/// ```dart +/// StreamReactionListView( +/// controller: controller, +/// itemBuilder: (context, reactions, index) { +/// final reaction = reactions[index]; +/// return ListTile(title: Text(reaction.type)); +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamReactionListController] +/// {@endtemplate} +class StreamReactionListView extends StatelessWidget { + /// Creates a new instance of [StreamReactionListView]. + const StreamReactionListView({ + super.key, + required this.controller, + required this.itemBuilder, + this.separatorBuilder = defaultReactionListViewSeparatorBuilder, + this.emptyBuilder, + this.loadingBuilder, + this.errorBuilder, + this.loadMoreTriggerIndex = 3, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.scrollController, + this.primary, + this.physics, + this.shrinkWrap = false, + this.padding, + this.addAutomaticKeepAlives = true, + this.addRepaintBoundaries = true, + this.addSemanticIndexes = true, + this.cacheExtent, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + }); + + /// The [StreamReactionListController] used to control the list of reactions. + final StreamReactionListController controller; + + /// A builder that is called to build items in the [ListView]. + final StreamReactionListViewIndexedWidgetBuilder itemBuilder; + + /// A builder that is called to build the list separator. + final PagedValueScrollViewIndexedWidgetBuilder separatorBuilder; + + /// A builder that is called to build the empty state of the list. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the loading state of the list. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the list. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + + /// The index to take into account when triggering [controller.loadMore]. + final int loadMoreTriggerIndex; + + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The axis along which the scroll view scrolls. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + /// Whether to wrap each child in an [AutomaticKeepAlive]. + /// + /// Defaults to true. + final bool addAutomaticKeepAlives; + + /// Whether to wrap each child in a [RepaintBoundary]. + /// + /// Defaults to true. + final bool addRepaintBoundaries; + + /// Whether to wrap each child in an [IndexedSemantics]. + /// + /// Defaults to true. + final bool addSemanticIndexes; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// {@endtemplate} + final ScrollController? scrollController; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// {@endtemplate} + final bool? primary; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// Defaults to false. + /// {@endtemplate} + final bool shrinkWrap; + + /// {@template flutter.widgets.scroll_view.physics} + /// How the scroll view should respond to user input. + /// {@endtemplate} + final ScrollPhysics? physics; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) => PagedValueListView( + scrollDirection: scrollDirection, + padding: padding, + physics: physics, + reverse: reverse, + controller: controller, + scrollController: scrollController, + primary: primary, + shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + dragStartBehavior: dragStartBehavior, + cacheExtent: cacheExtent, + clipBehavior: clipBehavior, + loadMoreTriggerIndex: loadMoreTriggerIndex, + separatorBuilder: separatorBuilder, + itemBuilder: itemBuilder, + emptyBuilder: (context) => + emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: Icon( + context.streamIcons.emoji32, + size: 148, + color: StreamChatTheme.of(context).colorTheme.disabled, + ), + emptyTitle: Text( + 'No reactions yet', + style: StreamChatTheme.of(context).textTheme.headline, + ), + ), + ), + ), + loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.list( + onTap: controller.retry, + error: const Text('Error loading reactions'), + ), + loadMoreIndicatorBuilder: (context) => Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), + ), + ), + loadingBuilder: (context) => loadingBuilder?.call(context) ?? const Center(child: StreamScrollViewLoadingWidget()), + errorBuilder: (context, error) => + errorBuilder?.call(context, error) ?? + Center( + child: StreamScrollViewErrorWidget( + errorTitle: const Text('Error loading reactions'), + onRetryPressed: controller.refresh, + ), + ), + ); +} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_error_widget.dart b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_error_widget.dart index 3dafd08931..94801f87c0 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_error_widget.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_error_widget.dart @@ -52,7 +52,8 @@ class StreamScrollViewErrorWidget extends StatelessWidget { final errorIcon = AnimatedSwitcher( duration: kThemeChangeDuration, - child: this.errorIcon ?? + child: + this.errorIcon ?? Icon( Icons.error_outline_rounded, size: 148, @@ -67,7 +68,8 @@ class StreamScrollViewErrorWidget extends StatelessWidget { ); final retryButtonText = AnimatedDefaultTextStyle( - style: errorTitleStyle ?? + style: + errorTitleStyle ?? chatThemeData.textTheme.headline.copyWith( color: Colors.white, ), diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_indexed_widget_builder.dart b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_indexed_widget_builder.dart index 0305cd1f20..9d669f04d0 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_indexed_widget_builder.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_indexed_widget_builder.dart @@ -5,11 +5,10 @@ import 'package:flutter/material.dart'; /// /// Used by [StreamChannelListView], [StreamMessageSearchListView] /// and [StreamUserListView]. -typedef StreamScrollViewIndexedWidgetBuilder - = Widget Function( - BuildContext context, - List items, - int index, - WidgetType defaultWidget, -); +typedef StreamScrollViewIndexedWidgetBuilder = + Widget Function( + BuildContext context, + List items, + int index, + WidgetType defaultWidget, + ); diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_error.dart b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_error.dart index 0b32058559..1b6aa3042a 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_error.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_error.dart @@ -74,14 +74,16 @@ class StreamScrollViewLoadMoreError extends StatelessWidget { final errorIcon = AnimatedSwitcher( duration: kThemeChangeDuration, - child: this.errorIcon ?? - const StreamSvgIcon( + child: + this.errorIcon ?? + Icon( + context.streamIcons.retry20, color: Colors.white, - icon: StreamSvgIcons.retry, ), ); - final backgroundColor = this.backgroundColor ?? + final backgroundColor = + this.backgroundColor ?? // ignore: deprecated_member_use theme.colorTheme.textLowEmphasis.withOpacity(0.9); diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_indicator.dart b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_indicator.dart index 7258522590..ab32318d69 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_load_more_indicator.dart @@ -18,8 +18,8 @@ class StreamScrollViewLoadMoreIndicator extends StatelessWidget { @override Widget build(BuildContext context) => SizedBox( - height: height, - width: width, - child: const CircularProgressIndicator.adaptive(), - ); + height: height, + width: width, + child: const CircularProgressIndicator.adaptive(), + ); } diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_loading_widget.dart b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_loading_widget.dart index 958eb80dc4..b18cab4d55 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_loading_widget.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/stream_scroll_view_loading_widget.dart @@ -17,8 +17,8 @@ class StreamScrollViewLoadingWidget extends StatelessWidget { @override Widget build(BuildContext context) => SizedBox( - height: height, - width: width, - child: const CircularProgressIndicator.adaptive(), - ); + height: height, + width: width, + child: const CircularProgressIndicator.adaptive(), + ); } diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_empty_state.dart b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_empty_state.dart new file mode 100644 index 0000000000..84e2d9666d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_empty_state.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A widget that is used to display the empty state of the thread list. +class StreamThreadListEmptyState extends StatelessWidget { + /// Creates a new instance of the [StreamThreadListEmptyState]. + const StreamThreadListEmptyState({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + context.streamIcons.messageBubbles32, + size: 32, + ), + SizedBox(height: context.streamSpacing.sm), + Text(context.translations.replyToStartThreadText), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_skeleton_loading.dart b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_skeleton_loading.dart new file mode 100644 index 0000000000..6a92bc91ee --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_skeleton_loading.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; + +/// A shimmer loading placeholder for the thread list view. +/// +/// Displays a skeleton UI with shimmer animation using +/// [StreamSkeletonLoading] and [StreamSkeletonBox] from the core package. +class StreamThreadListSkeletonLoading extends StatelessWidget { + /// Creates a new instance of [StreamThreadListSkeletonLoading]. + const StreamThreadListSkeletonLoading({ + super.key, + this.itemCount = 6, + }); + + /// The number of skeleton items to display. + final int itemCount; + + @override + Widget build(BuildContext context) { + return StreamSkeletonLoading( + child: ListView.separated( + physics: const NeverScrollableScrollPhysics(), + itemCount: itemCount, + separatorBuilder: (context, index) => const SizedBox(height: 1), + itemBuilder: (context, index) => const _StreamThreadListItemSkeleton(), + ), + ); + } +} + +class _StreamThreadListItemSkeleton extends StatelessWidget { + const _StreamThreadListItemSkeleton(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(context.streamSpacing.sm), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const StreamSkeletonBox.circular(radius: 24), + SizedBox(width: context.streamSpacing.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + flex: 2, + child: StreamSkeletonBox( + width: double.infinity, + height: 12, + borderRadius: BorderRadius.all(context.streamRadius.max), + ), + ), + SizedBox(width: context.streamSpacing.sm), + const Spacer( + flex: 2, + ), + StreamSkeletonBox( + width: 48, + height: 16, + borderRadius: BorderRadius.all(context.streamRadius.max), + ), + ], + ), + SizedBox(height: context.streamSpacing.xs), + Row( + children: [ + Expanded( + child: StreamSkeletonBox( + width: double.infinity, + height: 20, + borderRadius: BorderRadius.all(context.streamRadius.max), + ), + ), + SizedBox(width: context.streamSpacing.sm), + const SizedBox(width: 48), + ], + ), + SizedBox(height: context.streamSpacing.xs), + Row( + children: [ + const StreamSkeletonBox.circular(radius: 12), + SizedBox(width: context.streamSpacing.xs), + StreamSkeletonBox( + width: 64, + height: 12, + borderRadius: BorderRadius.all(context.streamRadius.max), + ), + SizedBox(width: context.streamSpacing.xs), + StreamSkeletonBox( + width: 64, + height: 12, + borderRadius: BorderRadius.all(context.streamRadius.max), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_tile.dart index 0617a81305..35090db82c 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_tile.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/misc/timestamp.dart'; +import 'package:stream_chat_flutter/src/utils/date_formatter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@template streamThreadListTile} @@ -13,71 +14,164 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@endtemplate} class StreamThreadListTile extends StatelessWidget { /// {@macro streamThreadListTile} - const StreamThreadListTile({ + StreamThreadListTile({ super.key, + required Thread thread, + User? currentUser, + GestureTapCallback? onTap, + GestureLongPressCallback? onLongPress, + }) : props = StreamThreadListTileProps( + thread: thread, + currentUser: currentUser, + onTap: onTap, + onLongPress: onLongPress, + ); + + /// Creates a thread list tile from pre-built [props]. + const StreamThreadListTile.fromProps({ + super.key, + required this.props, + }); + + /// The properties configuring this thread list tile. + final StreamThreadListTileProps props; + + @override + Widget build(BuildContext context) { + final builder = context.chatComponentBuilder(); + return builder?.call(context, props) ?? _DefaultStreamThreadListTile(props: props); + } +} + +/// Properties for configuring a [StreamThreadListTile]. +class StreamThreadListTileProps { + /// Creates properties for a thread list tile. + const StreamThreadListTileProps({ required this.thread, this.currentUser, this.onTap, this.onLongPress, }); - /// The thread to display. + /// The thread displayed by the tile. final Thread thread; /// The current user. final User? currentUser; - /// Called when the user taps this list tile. + /// Called when the tile is tapped. final GestureTapCallback? onTap; - /// Called when the user long-presses on this list tile. + /// Called when the tile is long pressed. final GestureLongPressCallback? onLongPress; +} + +class _DefaultStreamThreadListTile extends StatelessWidget { + const _DefaultStreamThreadListTile({ + required this.props, + }); + + final StreamThreadListTileProps props; @override Widget build(BuildContext context) { final theme = StreamThreadListTileTheme.of(context); + final defaults = _StreamThreadListTileThemeDefaults(context); + + final effectiveBackgroundColor = theme.backgroundColor ?? defaults.backgroundColor; + final effectivePadding = theme.padding ?? defaults.padding; + final effectiveChannelNameStyle = theme.threadChannelNameStyle ?? defaults.threadChannelNameStyle; + final effectiveReplyToMessageStyle = theme.threadReplyToMessageStyle ?? defaults.threadReplyToMessageStyle; + final effectiveLatestReplyMessageStyle = + theme.threadLatestReplyMessageStyle ?? defaults.threadLatestReplyMessageStyle; + final effectiveReplyCountStyle = theme.threadReplyCountStyle ?? defaults.threadReplyCountStyle; + final effectiveTimestampStyle = theme.threadLatestReplyTimestampStyle ?? defaults.threadLatestReplyTimestampStyle; + final effectiveTimestampFormatter = + theme.threadLatestReplyTimestampFormatter ?? defaults.threadLatestReplyTimestampFormatter; + final effectiveUnreadCountStyle = theme.threadUnreadMessageCountStyle ?? defaults.threadUnreadMessageCountStyle; + final effectiveUnreadCountBackgroundColor = + theme.threadUnreadMessageCountBackgroundColor ?? defaults.threadUnreadMessageCountBackgroundColor; + final thread = props.thread; + final currentUser = props.currentUser; + final parentMessage = thread.parentMessage; + final latestReply = thread.latestReplies.lastOrNull; + final channel = thread.channel; final language = currentUser?.language; - final unreadMessageCount = thread.read - ?.firstWhereOrNull((read) => read.user.id == currentUser?.id) - ?.unreadMessages; + final unreadMessageCount = thread.read?.firstWhereOrNull((read) => read.user.id == currentUser?.id)?.unreadMessages; + final latestActivityAt = thread.lastMessageAt ?? latestReply?.createdAt ?? thread.updatedAt; + final avatarUser = parentMessage?.user ?? latestReply?.user ?? thread.createdBy; + final channelName = + channel?.formatName(currentUser: currentUser) ?? avatarUser?.name ?? context.translations.noTitleText; + final participantUsers = thread.threadParticipants.map((it) => it.user).nonNulls.toList(growable: false); return Material( - color: theme.backgroundColor, + color: effectiveBackgroundColor, child: InkWell( - onTap: onTap, - onLongPress: onLongPress, - child: Container( - padding: theme.padding, - child: Column( - spacing: 6, - mainAxisSize: MainAxisSize.min, + onTap: props.onTap, + onLongPress: props.onLongPress, + child: Padding( + padding: effectivePadding, + child: Row( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (thread.channel case final channel?) - ThreadTitle( - channelName: channel.formatName(currentUser: currentUser), + children: [ + if (avatarUser case final user?) + Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: StreamUserAvatar( + user: user, + size: StreamAvatarSize.xl, + ), + ) + else + const Padding( + padding: EdgeInsetsDirectional.only(end: 12), + child: SizedBox.square(dimension: 40), ), - Row( - children: [ - if (thread.parentMessage case final parentMessage?) - Expanded( - child: ThreadReplyToContent( - language: language, - prefix: context.translations.repliedToLabel, - parentMessage: parentMessage, - ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ThreadTitle( + channelName: channelName, + style: effectiveChannelNameStyle, + ), + ), + if (unreadMessageCount case final count? when count > 0) ...[ + const SizedBox(width: 8), + ThreadUnreadCount( + unreadCount: count, + style: effectiveUnreadCountStyle, + backgroundColor: effectiveUnreadCountBackgroundColor, + ), + ], + ], ), - if (unreadMessageCount case final count? when count > 0) - ThreadUnreadCount(unreadCount: count), - ], - ), - if (thread.latestReplies.lastOrNull case final latestReply?) - ThreadLatestReply( - language: language, - latestReply: latestReply, - draftMessage: thread.draft?.message, + const SizedBox(height: 2), + ThreadRootMessagePreview( + parentMessage: parentMessage, + channel: channel, + language: language, + style: effectiveReplyToMessageStyle, + emptyStyle: effectiveLatestReplyMessageStyle, + ), + const SizedBox(height: 8), + ThreadFooter( + participantUsers: participantUsers, + replyCount: thread.replyCount, + latestActivityAt: latestActivityAt, + replyCountStyle: effectiveReplyCountStyle, + timestampStyle: effectiveTimestampStyle, + timestampFormatter: effectiveTimestampFormatter, + ), + ], ), + ), ], ), ), @@ -94,80 +188,75 @@ class ThreadTitle extends StatelessWidget { const ThreadTitle({ super.key, this.channelName, + this.style, }); /// The channel name to display. final String? channelName; + /// The text style to use. + /// + /// When null, uses the effective style resolved from the theme and defaults. + final TextStyle? style; + @override Widget build(BuildContext context) { - final theme = StreamThreadListTileTheme.of(context); - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.message_outlined, - size: 16, - color: theme.threadChannelNameStyle?.color, - ), - const SizedBox(width: 4), - Flexible( - child: Text( - channelName ?? context.translations.noTitleText, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.threadChannelNameStyle, - ), - ), - ], + return Text( + channelName ?? context.translations.noTitleText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: style, ); } } -/// {@template threadReplyToContent} -/// A widget that displays the message the thread is replying to. -/// {@endtemplate} -class ThreadReplyToContent extends StatelessWidget { - /// {@macro threadReplyToContent} - const ThreadReplyToContent({ +/// A widget that displays the original thread message as a single-line preview. +class ThreadRootMessagePreview extends StatelessWidget { + /// Creates a new instance of [ThreadRootMessagePreview]. + const ThreadRootMessagePreview({ super.key, - this.language, - this.prefix = 'replied to:', required this.parentMessage, + this.channel, + this.language, + this.style, + this.emptyStyle, }); - /// The prefix to display before the message. - /// - /// Defaults to `replied to:`. - final String prefix; + /// The root message of the thread. + final Message? parentMessage; - /// The language of the message. + /// The channel the thread belongs to. + final ChannelModel? channel; + + /// The language used for translations. final String? language; - /// The message the thread is replying to. - final Message parentMessage; + /// The text style used for the message preview. + /// + /// When null, uses the effective style resolved from the theme and defaults. + final TextStyle? style; + + /// The text style used when no parent message is available. + /// + /// When null, uses the effective style resolved from the theme and defaults. + final TextStyle? emptyStyle; @override Widget build(BuildContext context) { - final theme = StreamThreadListTileTheme.of(context); + if (parentMessage case final message?) { + return StreamMessagePreviewText( + message: message, + channel: channel, + language: language, + textStyle: style, + ); + } - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - prefix, - style: theme.threadReplyToMessageStyle, - ), - const SizedBox(width: 4), - Flexible( - child: StreamMessagePreviewText( - language: language, - message: parentMessage, - textStyle: theme.threadReplyToMessageStyle, - ), - ), - ], + return Text( + context.translations.emptyMessagesText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: emptyStyle, ); } } @@ -180,93 +269,135 @@ class ThreadUnreadCount extends StatelessWidget { const ThreadUnreadCount({ super.key, required this.unreadCount, + this.style, + this.backgroundColor, }) : assert(unreadCount > 0, 'unreadCount must be greater than 0'); /// The number of unread messages. final int unreadCount; + /// The text style for the badge label. + /// + /// When null, uses the effective style resolved from the theme and defaults. + final TextStyle? style; + + /// The background color for the badge. + /// + /// When null, uses the effective color resolved from the theme and defaults. + final Color? backgroundColor; + @override Widget build(BuildContext context) { - final theme = StreamThreadListTileTheme.of(context); - return Badge( - textStyle: theme.threadUnreadMessageCountStyle, - textColor: theme.threadUnreadMessageCountStyle?.color, - backgroundColor: theme.threadUnreadMessageCountBackgroundColor, + textStyle: style, + textColor: style?.color, + backgroundColor: backgroundColor, + largeSize: 20, label: Text('$unreadCount'), ); } } -/// {@template threadLatestReply} -/// A widget that displays the latest reply in the thread. -/// {@endtemplate} -class ThreadLatestReply extends StatelessWidget { - /// {@macro threadLatestReply} - const ThreadLatestReply({ +/// A widget that displays reply metadata for a thread. +class ThreadFooter extends StatelessWidget { + /// Creates a new instance of [ThreadFooter]. + const ThreadFooter({ super.key, - this.language, - this.draftMessage, - required this.latestReply, + required this.participantUsers, + required this.replyCount, + required this.latestActivityAt, + this.replyCountStyle, + this.timestampStyle, + this.timestampFormatter, }); - /// The language of the message. - final String? language; + /// Users participating in the thread. + final List participantUsers; - /// The draft message in the thread. - final DraftMessage? draftMessage; + /// The number of replies in the thread. + final int replyCount; - /// The latest reply in the thread. - final Message latestReply; + /// The latest activity time in the thread. + final DateTime latestActivityAt; + + /// The text style for the reply count label. + /// + /// When null, uses the effective style resolved from the theme and defaults. + final TextStyle? replyCountStyle; + + /// The text style for the timestamp. + /// + /// When null, uses the effective style resolved from the theme and defaults. + final TextStyle? timestampStyle; + + /// The formatter to use for the timestamp. + /// + /// When null, uses [formatRecentDateTime]. + final DateFormatter? timestampFormatter; @override Widget build(BuildContext context) { - final theme = StreamThreadListTileTheme.of(context); - return Row( - spacing: 8, - children: [ - if (latestReply.user case final user?) StreamUserAvatar(user: user), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - latestReply.user!.name, - style: theme.threadLatestReplyUsernameStyle, - ), - Row( - children: [ - Expanded( - child: Builder( - builder: (context) { - if (draftMessage case final draftMessage?) { - return StreamDraftMessagePreviewText( - draftMessage: draftMessage, - textStyle: theme.threadLatestReplyMessageStyle, - ); - } - - return StreamMessagePreviewText( - language: language, - message: latestReply, - textStyle: theme.threadLatestReplyMessageStyle, - ); - }, - ), - ), - StreamTimestamp( - date: latestReply.createdAt.toLocal(), - style: theme.threadLatestReplyTimestampStyle, - formatter: theme.threadLatestReplyTimestampFormatter, - ), - ], - ), - ], + children: [ + if (participantUsers.isNotEmpty) + Padding( + padding: const EdgeInsetsDirectional.only(end: 6), + child: StreamUserAvatarStack( + users: participantUsers, + size: StreamAvatarStackSize.sm, + max: 3, + ), ), + Text( + context.translations.threadReplyCountText(replyCount), + style: replyCountStyle, + ), + const SizedBox(width: 8), + StreamTimestamp( + date: latestActivityAt.toLocal(), + style: timestampStyle, + formatter: timestampFormatter ?? formatRecentDateTime, ), ], ); } } + +class _StreamThreadListTileThemeDefaults extends StreamThreadListTileThemeData { + _StreamThreadListTileThemeDefaults(this._context); + + final BuildContext _context; + + late final _colorScheme = _context.streamColorScheme; + late final _textTheme = _context.streamTextTheme; + + @override + EdgeInsetsGeometry get padding => const EdgeInsets.symmetric(vertical: 14, horizontal: 8); + + @override + Color get backgroundColor => _colorScheme.backgroundApp; + + @override + TextStyle get threadChannelNameStyle => _textTheme.captionEmphasis.copyWith(color: _colorScheme.textTertiary); + + @override + TextStyle get threadReplyToMessageStyle => _textTheme.bodyDefault; + + @override + TextStyle get threadLatestReplyMessageStyle => _textTheme.bodyDefault; + + @override + TextStyle get threadReplyCountStyle => _textTheme.captionEmphasis.copyWith(color: _colorScheme.textLink); + + @override + TextStyle get threadLatestReplyTimestampStyle => _textTheme.captionDefault.copyWith(color: _colorScheme.textTertiary); + + @override + DateFormatter get threadLatestReplyTimestampFormatter => formatRecentDateTime; + + @override + TextStyle get threadUnreadMessageCountStyle => _textTheme.numericXl.copyWith(color: _colorScheme.textOnAccent); + + @override + Color get threadUnreadMessageCountBackgroundColor => _colorScheme.accentPrimary; +} diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_view.dart index b8b2d293e3..5ccd0826d9 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_thread_list_view.dart @@ -2,26 +2,31 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; +import 'package:stream_chat_flutter/src/scroll_view/thread_scroll_view/stream_thread_list_empty_state.dart'; +import 'package:stream_chat_flutter/src/scroll_view/thread_scroll_view/stream_thread_list_skeleton_loading.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default separator builder for [StreamThreadListView]. Widget defaultThreadListViewSeparatorBuilder( BuildContext context, List threads, int index, -) => - const StreamThreadListSeparator(); +) => const StreamThreadListSeparator(); /// Signature for the item builder that creates the children of the /// [StreamThreadListView]. -typedef StreamThreadListViewIndexedWidgetBuilder - = StreamScrollViewIndexedWidgetBuilder; +/// +typedef StreamThreadListViewIndexedWidgetBuilder = StreamScrollViewIndexedWidgetBuilder; /// {@template streamThreadListView} -/// A [ListView] that shows a list of [Thread]'s. It uses a -/// [StreamThreadListController] to load the threads in paginated form. +/// A [ListView] that shows a list of [Thread]'s the current user participated +/// in. +/// +/// Uses a [StreamThreadListController] to load threads in paginated form. +/// +/// Each row is rendered using [StreamThreadListTile], which can be customized +/// app-wide through [StreamComponentFactory]. /// /// Example: /// @@ -29,16 +34,13 @@ typedef StreamThreadListViewIndexedWidgetBuilder /// StreamThreadListView( /// controller: controller, /// onThreadTap: (thread) { -/// // Handle thread tap event -/// }, -/// onThreadLongPress: (thread) { -/// // Handle thread long press event +/// // Navigate to thread conversation /// }, /// ) /// ``` /// /// See also: -/// * [StreamThreadListTile] +/// * [StreamMessageWidget], which renders each thread's parent message. /// * [StreamThreadListController] /// {@endtemplate} class StreamThreadListView extends StatelessWidget { @@ -75,6 +77,7 @@ class StreamThreadListView extends StatelessWidget { final StreamThreadListController controller; /// A builder that is called to build items in the [ListView]. + /// final StreamThreadListViewIndexedWidgetBuilder? itemBuilder; /// A builder that is called to build the list separator. @@ -89,10 +92,10 @@ class StreamThreadListView extends StatelessWidget { /// A builder that is called to build the error state of the list. final Widget Function(BuildContext, StreamChatError)? errorBuilder; - /// Called when the user taps this list tile. + /// Called when the user taps a thread. final void Function(Thread)? onThreadTap; - /// Called when the user long-presses on this list tile. + /// Called when the user long-presses on a thread. final void Function(Thread)? onThreadLongPress; /// The index to take into account when triggering [controller.loadMore]. @@ -301,7 +304,6 @@ class StreamThreadListView extends StatelessWidget { final currentUser = StreamChat.of(context).currentUser; final onTap = onThreadTap; final onLongPress = onThreadLongPress; - final tile = StreamThreadListTile( thread: thread, currentUser: currentUser, @@ -311,41 +313,21 @@ class StreamThreadListView extends StatelessWidget { return itemBuilder?.call(context, threads, index, tile) ?? tile; }, - emptyBuilder: (context) { - final chatThemeData = StreamChatTheme.of(context); - return emptyBuilder?.call(context) ?? - Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( - size: 148, - icon: StreamSvgIcons.threadReply, - color: chatThemeData.colorTheme.disabled, - ), - emptyTitle: Text( - context.translations.emptyMessagesText, - style: chatThemeData.textTheme.headline, - ), - ), - ), - ); - }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.list( + emptyBuilder: (context) => emptyBuilder?.call(context) ?? const StreamThreadListEmptyState(), + loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.list( onTap: controller.retry, error: Text(context.translations.loadingMessagesError), ), - loadMoreIndicatorBuilder: (context) => const Center( + loadMoreIndicatorBuilder: (context) => Center( child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), ), ), loadingBuilder: (context) => loadingBuilder?.call(context) ?? const Center( - child: StreamScrollViewLoadingWidget(), + child: StreamThreadListSkeletonLoading(), ), errorBuilder: (context, error) => errorBuilder?.call(context, error) ?? diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_unread_threads_banner.dart b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_unread_threads_banner.dart index 088203d4fd..8156992d79 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_unread_threads_banner.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/thread_scroll_view/stream_unread_threads_banner.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/icons/stream_svg_icon.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_core_flutter/stream_core_flutter.dart'; /// {@template unreadThreadsBanner} /// A widget that shows a banner with the number of unread threads. @@ -74,8 +74,8 @@ class StreamUnreadThreadsBanner extends StatelessWidget { ), ), ), - StreamSvgIcon( - icon: StreamSvgIcons.reload, + Icon( + context.streamIcons.refresh20, color: theme.colorTheme.barsBg, ), ], diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_tile.dart index 4d512f05ea..0a3a331365 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_tile.dart @@ -45,33 +45,21 @@ class StreamUserGridTile extends StatelessWidget { Widget? footer, GestureTapCallback? onTap, GestureLongPressCallback? onLongPress, - }) => - StreamUserGridTile( - key: key ?? this.key, - user: user ?? this.user, - footer: footer ?? this.footer, - onTap: onTap ?? this.onTap, - onLongPress: onLongPress ?? this.onLongPress, - child: child ?? this.child, - ); + }) => StreamUserGridTile( + key: key ?? this.key, + user: user ?? this.user, + footer: footer ?? this.footer, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + child: child ?? this.child, + ); @override Widget build(BuildContext context) { - final child = this.child ?? - StreamUserAvatar( - user: user, - borderRadius: BorderRadius.circular(32), - constraints: const BoxConstraints.tightFor( - height: 64, - width: 64, - ), - onlineIndicatorConstraints: const BoxConstraints.tightFor( - height: 12, - width: 12, - ), - ); + final child = this.child ?? StreamUserAvatar(size: .xl, user: user); - final footer = this.footer ?? + final footer = + this.footer ?? Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_view.dart index 5aac833955..02e0d01d92 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_grid_view.dart @@ -2,18 +2,16 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default grid delegate for [StreamUserGridView]. -const defaultUserGridViewDelegate = - SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4); +const defaultUserGridViewDelegate = SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4); /// Signature for the item builder that creates the children of the /// [StreamUserGridView]. -typedef StreamUserGridViewIndexedWidgetBuilder - = StreamScrollViewIndexedWidgetBuilder; +typedef StreamUserGridViewIndexedWidgetBuilder = StreamScrollViewIndexedWidgetBuilder; /// A [GridView] that shows a grid of [User]s, /// it uses [StreamUserGridTile] as a default item. @@ -349,9 +347,9 @@ class StreamUserGridView extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(8), child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( + emptyIcon: Icon( + context.streamIcons.user32, size: 148, - icon: StreamSvgIcons.user, color: chatThemeData.colorTheme.disabled, ), emptyTitle: Text( @@ -362,18 +360,17 @@ class StreamUserGridView extends StatelessWidget { ), ); }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.grid( + loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.grid( onTap: controller.retry, error: Text( context.translations.loadingUsersError, textAlign: TextAlign.center, ), ), - loadMoreIndicatorBuilder: (context) => const Center( + loadMoreIndicatorBuilder: (context) => Center( child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), ), ), loadingBuilder: (context) => diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_tile.dart b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_tile.dart index 32129c0110..cd5d6afc2f 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_tile.dart @@ -105,49 +105,44 @@ class StreamUserListTile extends StatelessWidget { Color? tileColor, VisualDensity? visualDensity, EdgeInsetsGeometry? contentPadding, - }) => - StreamUserListTile( - key: key ?? this.key, - user: user ?? this.user, - leading: leading ?? this.leading, - title: title ?? this.title, - subtitle: subtitle ?? this.subtitle, - selectedWidget: selectedWidget ?? this.selectedWidget, - selected: selected ?? this.selected, - onTap: onTap ?? this.onTap, - onLongPress: onLongPress ?? this.onLongPress, - tileColor: tileColor ?? this.tileColor, - visualDensity: visualDensity ?? this.visualDensity, - contentPadding: contentPadding ?? this.contentPadding, - ); + }) => StreamUserListTile( + key: key ?? this.key, + user: user ?? this.user, + leading: leading ?? this.leading, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + selectedWidget: selectedWidget ?? this.selectedWidget, + selected: selected ?? this.selected, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + tileColor: tileColor ?? this.tileColor, + visualDensity: visualDensity ?? this.visualDensity, + contentPadding: contentPadding ?? this.contentPadding, + ); @override Widget build(BuildContext context) { final chatThemeData = StreamChatTheme.of(context); - final leading = this.leading ?? - StreamUserAvatar( - user: user, - constraints: const BoxConstraints.tightFor( - height: 40, - width: 40, - ), - ); + final leading = this.leading ?? StreamUserAvatar(size: .lg, user: user); - final title = this.title ?? + final title = + this.title ?? Text( user.name, style: chatThemeData.textTheme.bodyBold, ); - final subtitle = this.subtitle ?? + final subtitle = + this.subtitle ?? UserLastActive( user: user, ); - final selectedWidget = this.selectedWidget ?? - StreamSvgIcon( - icon: StreamSvgIcons.checkSend, + final selectedWidget = + this.selectedWidget ?? + Icon( + context.streamIcons.checkmark20, color: chatThemeData.colorTheme.accentPrimary, ); @@ -184,7 +179,7 @@ class UserLastActive extends StatelessWidget { user.online ? context.translations.userOnlineText : '${context.translations.userLastOnlineText} ' - '${Jiffy.parseFromDateTime(lastActive).fromNow()}', + '${Jiffy.parseFromDateTime(lastActive).fromNow()}', style: chatTheme.textTheme.footnote.copyWith( // ignore: deprecated_member_use color: chatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), diff --git a/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_view.dart b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_view.dart index df2530aad0..7cf21be9da 100644 --- a/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/scroll_view/user_scroll_view/stream_user_list_view.dart @@ -2,22 +2,20 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart'; -import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart'; import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// Default separator builder for [StreamUserListView]. Widget defaultUserListViewSeparatorBuilder( BuildContext context, List users, int index, -) => - const StreamUserListSeparator(); +) => const StreamUserListSeparator(); /// Signature for the item builder that creates the children of the /// [StreamUserListView]. -typedef StreamUserListViewIndexedWidgetBuilder - = StreamScrollViewIndexedWidgetBuilder; +typedef StreamUserListViewIndexedWidgetBuilder = StreamScrollViewIndexedWidgetBuilder; /// A [ListView] that shows a list of [User]s, /// it uses [StreamUserListTile] as a default item. @@ -278,88 +276,87 @@ class StreamUserListView extends StatelessWidget { @override Widget build(BuildContext context) => PagedValueListView( - scrollDirection: scrollDirection, - padding: padding, - physics: physics, - reverse: reverse, - controller: controller, - scrollController: scrollController, - primary: primary, - shrinkWrap: shrinkWrap, - addAutomaticKeepAlives: addAutomaticKeepAlives, - addRepaintBoundaries: addRepaintBoundaries, - addSemanticIndexes: addSemanticIndexes, - keyboardDismissBehavior: keyboardDismissBehavior, - restorationId: restorationId, - dragStartBehavior: dragStartBehavior, - cacheExtent: cacheExtent, - clipBehavior: clipBehavior, - loadMoreTriggerIndex: loadMoreTriggerIndex, - separatorBuilder: separatorBuilder, - itemBuilder: (context, users, index) { - final user = users[index]; - final onTap = onUserTap; - final onLongPress = onUserLongPress; - - final streamUserListTile = StreamUserListTile( - user: user, - onTap: onTap == null ? null : () => onTap(user), - onLongPress: onLongPress == null ? null : () => onLongPress(user), - ); + scrollDirection: scrollDirection, + padding: padding, + physics: physics, + reverse: reverse, + controller: controller, + scrollController: scrollController, + primary: primary, + shrinkWrap: shrinkWrap, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + keyboardDismissBehavior: keyboardDismissBehavior, + restorationId: restorationId, + dragStartBehavior: dragStartBehavior, + cacheExtent: cacheExtent, + clipBehavior: clipBehavior, + loadMoreTriggerIndex: loadMoreTriggerIndex, + separatorBuilder: separatorBuilder, + itemBuilder: (context, users, index) { + final user = users[index]; + final onTap = onUserTap; + final onLongPress = onUserLongPress; + + final streamUserListTile = StreamUserListTile( + user: user, + onTap: onTap == null ? null : () => onTap(user), + onLongPress: onLongPress == null ? null : () => onLongPress(user), + ); - return itemBuilder?.call( - context, - users, - index, - streamUserListTile, - ) ?? - streamUserListTile; - }, - emptyBuilder: (context) { - final chatThemeData = StreamChatTheme.of(context); - return emptyBuilder?.call(context) ?? - Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( - size: 148, - icon: StreamSvgIcons.user, - color: chatThemeData.colorTheme.disabled, - ), - emptyTitle: Text( - context.translations.noUsersLabel, - style: chatThemeData.textTheme.headline, - ), - ), + return itemBuilder?.call( + context, + users, + index, + streamUserListTile, + ) ?? + streamUserListTile; + }, + emptyBuilder: (context) { + final chatThemeData = StreamChatTheme.of(context); + return emptyBuilder?.call(context) ?? + Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamScrollViewEmptyWidget( + emptyIcon: Icon( + context.streamIcons.user32, + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + emptyTitle: Text( + context.translations.noUsersLabel, + style: chatThemeData.textTheme.headline, ), - ); - }, - loadMoreErrorBuilder: (context, error) => - StreamScrollViewLoadMoreError.list( - onTap: controller.retry, - error: Text(context.translations.loadingUsersError), + ), + ), + ); + }, + loadMoreErrorBuilder: (context, error) => StreamScrollViewLoadMoreError.list( + onTap: controller.retry, + error: Text(context.translations.loadingUsersError), + ), + loadMoreIndicatorBuilder: (context) => Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: StreamLoadingSpinner(), + ), + ), + loadingBuilder: (context) => + loadingBuilder?.call(context) ?? + const Center( + child: StreamScrollViewLoadingWidget(), ), - loadMoreIndicatorBuilder: (context) => const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: StreamScrollViewLoadMoreIndicator(), + errorBuilder: (context, error) => + errorBuilder?.call(context, error) ?? + Center( + child: StreamScrollViewErrorWidget( + errorTitle: Text(context.translations.loadingUsersError), + onRetryPressed: controller.refresh, ), ), - loadingBuilder: (context) => - loadingBuilder?.call(context) ?? - const Center( - child: StreamScrollViewLoadingWidget(), - ), - errorBuilder: (context, error) => - errorBuilder?.call(context, error) ?? - Center( - child: StreamScrollViewErrorWidget( - errorTitle: Text(context.translations.loadingUsersError), - onRetryPressed: controller.refresh, - ), - ), - ); + ); } /// A widget that is used to display a separator between diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index 33aef8d964..0ed8077047 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -37,6 +37,7 @@ class StreamChat extends StatefulWidget { required this.child, this.streamChatThemeData, this.streamChatConfigData, + this.componentBuilders, this.onBackgroundEventReceived, this.backgroundKeepAlive = const Duration(minutes: 1), this.connectivityStream, @@ -54,6 +55,36 @@ class StreamChat extends StatefulWidget { /// Non-theme related UI configuration options. final StreamChatConfigurationData? streamChatConfigData; + /// Custom component builders for overriding default UI components. + /// + /// When provided, a [StreamComponentFactory] is inserted into the widget + /// tree below the theme and above [StreamChatCore], allowing all descendant + /// widgets to resolve custom builders. + /// + /// {@tool snippet} + /// + /// Override the default message widget with a custom builder: + /// + /// ```dart + /// StreamChat( + /// client: client, + /// componentBuilders: StreamComponentBuilders( + /// extensions: streamChatComponentBuilders( + /// messageWidget: (context, props) { + /// return DefaultStreamMessage( + /// props: props.copyWith( + /// actionsBuilder: myActionsBuilder, + /// ), + /// ); + /// }, + /// ), + /// ), + /// child: MyApp(), + /// ) + /// ``` + /// {@end-tool} + final StreamComponentBuilders? componentBuilders; + /// The amount of time that will pass before disconnecting the client /// in the background final Duration backgroundKeepAlive; @@ -141,8 +172,7 @@ class StreamChatState extends State { StreamChatClient get client => widget.client; /// Gets configuration options from widget - StreamChatConfigurationData get streamChatConfigData => - widget.streamChatConfigData ?? StreamChatConfigurationData(); + StreamChatConfigurationData get streamChatConfigData => widget.streamChatConfigData ?? StreamChatConfigurationData(); @override void initState() { @@ -156,39 +186,39 @@ class StreamChatState extends State { @override Widget build(BuildContext context) { final theme = _getTheme(context, widget.streamChatThemeData); - return Portal( - child: StreamChatConfiguration( - data: streamChatConfigData, - child: StreamChatTheme( - data: theme, - child: Builder( - builder: (context) { - final materialTheme = Theme.of(context); - final streamTheme = StreamChatTheme.of(context); - return Theme( - data: materialTheme.copyWith( - primaryIconTheme: streamTheme.primaryIconTheme, - colorScheme: materialTheme.colorScheme.copyWith( - secondary: streamTheme.colorTheme.accentPrimary, - ), - ), - child: StreamChatCore( - client: client, - onBackgroundEventReceived: widget.onBackgroundEventReceived, - backgroundKeepAlive: widget.backgroundKeepAlive, - connectivityStream: widget.connectivityStream, - child: Builder( - builder: (context) { - return widget.child ?? const Empty(); - }, - ), - ), - ); - }, - ), - ), + + Widget child = StreamChatTheme( + data: theme, + child: Builder( + builder: (context) { + final materialTheme = Theme.of(context); + final streamTheme = StreamChatTheme.of(context); + return Theme( + data: materialTheme.copyWith( + primaryIconTheme: streamTheme.primaryIconTheme, + colorScheme: materialTheme.colorScheme.copyWith( + secondary: streamTheme.colorTheme.accentPrimary, + ), + ), + child: StreamChatCore( + client: client, + onBackgroundEventReceived: widget.onBackgroundEventReceived, + backgroundKeepAlive: widget.backgroundKeepAlive, + connectivityStream: widget.connectivityStream, + child: widget.child ?? const Empty(), + ), + ); + }, ), ); + + if (widget.componentBuilders case final builders?) { + child = StreamComponentFactory(builders: builders, child: child); + } + + return Portal( + child: StreamChatConfiguration(data: streamChatConfigData, child: child), + ); } StreamChatThemeData _getTheme( @@ -208,8 +238,7 @@ class StreamChatState extends State { @override void didChangeDependencies() { - final currentLocale = - Localizations.localeOf(context).toString().toLowerCase(); + final currentLocale = Localizations.localeOf(context).toString().toLowerCase(); final availableLocales = Jiffy.getSupportedLocales(); if (availableLocales.contains(currentLocale)) { Jiffy.setLocale(currentLocale); 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 54fec9887b..0eb6f675bc 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_configuration.dart @@ -18,21 +18,70 @@ class StreamChatConfiguration extends InheritedWidget { final StreamChatConfigurationData data; @override - bool updateShouldNotify(StreamChatConfiguration oldWidget) => - data != oldWidget.data; + 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 streamChatConfiguration = - context.dependOnInheritedWidgetOfExactType(); + final result = maybeOf(context); + if (result != null) return result; - assert( - streamChatConfiguration != null, - ''' -You must have a StreamChatConfigurationProvider widget at the top of your widget tree''', - ); + 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'), + ]); + } - return streamChatConfiguration!.data; + /// 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(); + return streamChatConfiguration?.data; } } @@ -112,31 +161,42 @@ class StreamChatConfigurationData { Widget loadingIndicator = const StreamLoadingIndicator(), Widget Function(BuildContext, User)? defaultUserImage, Widget Function(BuildContext, User)? placeholderUserImage, - List? reactionIcons, + ReactionIconResolver? reactionIconResolver, bool? enforceUniqueReactions, bool draftMessagesEnabled = false, MessagePreviewFormatter? messagePreviewFormatter, + StreamImageCDN imageCDN = const StreamImageCDN(), + List? attachmentBuilders, + StreamReactionsType? reactionType, + StreamReactionsPosition? reactionPosition, }) { return StreamChatConfigurationData._( loadingIndicator: loadingIndicator, defaultUserImage: defaultUserImage ?? _defaultUserImage, placeholderUserImage: placeholderUserImage, - reactionIcons: reactionIcons ?? _defaultReactionIcons, + reactionIconResolver: reactionIconResolver ?? const DefaultReactionIconResolver(), enforceUniqueReactions: enforceUniqueReactions ?? true, draftMessagesEnabled: draftMessagesEnabled, - messagePreviewFormatter: - messagePreviewFormatter ?? MessagePreviewFormatter(), + messagePreviewFormatter: messagePreviewFormatter ?? MessagePreviewFormatter(), + imageCDN: imageCDN, + attachmentBuilders: attachmentBuilders, + reactionType: reactionType, + reactionPosition: reactionPosition, ); } - StreamChatConfigurationData._({ + const StreamChatConfigurationData._({ required this.loadingIndicator, required this.defaultUserImage, required this.placeholderUserImage, - required this.reactionIcons, + required this.reactionIconResolver, required this.enforceUniqueReactions, required this.draftMessagesEnabled, required this.messagePreviewFormatter, + required this.imageCDN, + required this.attachmentBuilders, + this.reactionType, + this.reactionPosition, }); /// Copies the configuration options from one [StreamChatConfigurationData] to @@ -145,21 +205,27 @@ class StreamChatConfigurationData { Widget? loadingIndicator, Widget Function(BuildContext, User)? defaultUserImage, Widget Function(BuildContext, User)? placeholderUserImage, - List? reactionIcons, + ReactionIconResolver? reactionIconResolver, bool? enforceUniqueReactions, bool? draftMessagesEnabled, MessagePreviewFormatter? messagePreviewFormatter, + StreamImageCDN? imageCDN, + List? attachmentBuilders, + StreamReactionsType? reactionType, + StreamReactionsPosition? reactionPosition, }) { return StreamChatConfigurationData( - reactionIcons: reactionIcons ?? this.reactionIcons, + reactionIconResolver: reactionIconResolver ?? this.reactionIconResolver, defaultUserImage: defaultUserImage ?? this.defaultUserImage, placeholderUserImage: placeholderUserImage ?? this.placeholderUserImage, loadingIndicator: loadingIndicator ?? this.loadingIndicator, - enforceUniqueReactions: - enforceUniqueReactions ?? this.enforceUniqueReactions, + enforceUniqueReactions: enforceUniqueReactions ?? this.enforceUniqueReactions, draftMessagesEnabled: draftMessagesEnabled ?? this.draftMessagesEnabled, - messagePreviewFormatter: - messagePreviewFormatter ?? this.messagePreviewFormatter, + messagePreviewFormatter: messagePreviewFormatter ?? this.messagePreviewFormatter, + imageCDN: imageCDN ?? this.imageCDN, + attachmentBuilders: attachmentBuilders ?? this.attachmentBuilders, + reactionType: reactionType ?? this.reactionType, + reactionPosition: reactionPosition ?? this.reactionPosition, ); } @@ -177,8 +243,11 @@ class StreamChatConfigurationData { /// The widget that will be built when the user image is loading. final Widget Function(BuildContext, User)? placeholderUserImage; - /// Assets used for rendering reactions. - final List reactionIcons; + /// The resolver used to convert reaction types into [StreamEmojiContent] + /// models and to provide the list of supported/default reaction types. + /// + /// Defaults to [DefaultReactionIconResolver]. + final ReactionIconResolver reactionIconResolver; /// Whether a new reaction should replace the existing one. final bool enforceUniqueReactions; @@ -188,78 +257,42 @@ class StreamChatConfigurationData { /// Defaults to [MessagePreviewFormatter]. final MessagePreviewFormatter messagePreviewFormatter; - 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, - ); - }, - ), - ]; + /// The image CDN used for generating resized image URLs and stable + /// cache keys. + /// + /// Defaults to [StreamImageCDN], which supports Stream's own CDN. + /// Extend [StreamImageCDN] to customize behavior for a custom CDN. + final StreamImageCDN imageCDN; + + /// Custom attachment builders for rendering attachment widgets in messages. + /// + /// When non-null, these builders are prepended to the default builders + /// based on the [Attachment.type], allowing custom attachment types to be + /// rendered globally across all message widgets. + final List? attachmentBuilders; + + /// The visual type of the reactions display used across all message widgets. + /// + /// When null, the widget resolves its own default + /// ([StreamReactionsType.segmented]). + final StreamReactionsType? reactionType; - static Widget _defaultUserImage(BuildContext context, User user) => Center( - child: StreamGradientAvatar( - name: user.name, - userId: user.id, - ), - ); + /// Where reactions appear relative to the message bubble across all + /// message widgets. + /// + /// When null, the widget resolves its own default + /// ([StreamReactionsPosition.header]). + final StreamReactionsPosition? reactionPosition; + + 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/audio_waveform_slider_theme.dart b/packages/stream_chat_flutter/lib/src/theme/audio_waveform_slider_theme.dart deleted file mode 100644 index 0b2b9b5424..0000000000 --- a/packages/stream_chat_flutter/lib/src/theme/audio_waveform_slider_theme.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/src/theme/audio_waveform_theme.dart'; -import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; - -/// {@template streamAudioWaveformSliderTheme} -/// Overrides the default style of [StreamAudioWaveformSlider] descendants. -/// -/// See also: -/// -/// * [StreamVoiceRecordingAttachmentThemeData], which is used to configure -/// this theme. -/// {@endtemplate} -class StreamAudioWaveformSliderTheme extends InheritedTheme { - /// Creates a [StreamAudioWaveformSliderTheme]. - /// - /// The [data] parameter must not be null. - const StreamAudioWaveformSliderTheme({ - super.key, - required this.data, - required super.child, - }); - - /// The configuration of this theme. - final StreamAudioWaveformSliderThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [StreamAudioWaveformSliderTheme] widget, - /// then [StreamAudioWaveformSliderTheme.audioWaveformSliderTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// StreamAudioWaveformSliderTheme theme = - /// StreamAudioWaveformSliderTheme.of(context); - /// ``` - static StreamAudioWaveformSliderThemeData of(BuildContext context) { - final audioWaveformSliderTheme = context - .dependOnInheritedWidgetOfExactType(); - return audioWaveformSliderTheme?.data ?? - StreamChatTheme.of(context).audioWaveformSliderTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - StreamAudioWaveformSliderTheme(data: data, child: child); - - @override - bool updateShouldNotify(StreamAudioWaveformSliderTheme oldWidget) => - data != oldWidget.data; -} - -/// {@template streamAudioWaveformSliderThemeData} -/// A style that overrides the default appearance of -/// [StreamAudioWaveformSlider] widgets when used with -/// [StreamAudioWaveformSliderTheme] or with the overall -/// [StreamChatTheme]'s [StreamChatThemeData.audioWaveformSliderTheme]. -/// {@endtemplate} -class StreamAudioWaveformSliderThemeData with Diagnosticable { - /// {@macro streamVoiceRecordingAttachmentThemeData} - const StreamAudioWaveformSliderThemeData({ - this.audioWaveformTheme, - this.thumbColor, - this.thumbBorderColor, - }); - - /// The theme of the audio waveform. - final StreamAudioWaveformThemeData? audioWaveformTheme; - - /// The color of the thumb. - final Color? thumbColor; - - /// The color of the thumb border. - final Color? thumbBorderColor; - - /// A copy of [StreamAudioWaveformSliderThemeData] with specified attributes - /// overridden. - StreamAudioWaveformSliderThemeData copyWith({ - StreamAudioWaveformThemeData? audioWaveformTheme, - Color? thumbColor, - Color? thumbBorderColor, - }) { - return StreamAudioWaveformSliderThemeData( - audioWaveformTheme: audioWaveformTheme ?? this.audioWaveformTheme, - thumbColor: thumbColor ?? this.thumbColor, - thumbBorderColor: thumbBorderColor ?? this.thumbBorderColor, - ); - } - - /// Merges this [StreamPollOptionsDialogThemeData] with the [other]. - StreamAudioWaveformSliderThemeData merge( - StreamAudioWaveformSliderThemeData? other, - ) { - if (other == null) return this; - return copyWith( - audioWaveformTheme: other.audioWaveformTheme, - thumbColor: other.thumbColor, - thumbBorderColor: other.thumbBorderColor, - ); - } - - /// Linearly interpolate between two [StreamPollOptionsDialogThemeData]. - static StreamAudioWaveformSliderThemeData lerp( - StreamAudioWaveformSliderThemeData a, - StreamAudioWaveformSliderThemeData b, - double t, - ) => - StreamAudioWaveformSliderThemeData( - audioWaveformTheme: StreamAudioWaveformThemeData.lerp( - a.audioWaveformTheme!, b.audioWaveformTheme!, t), - thumbColor: Color.lerp(a.thumbColor, b.thumbColor, t), - thumbBorderColor: Color.lerp(a.thumbBorderColor, b.thumbBorderColor, t), - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is StreamAudioWaveformSliderThemeData && - other.audioWaveformTheme == audioWaveformTheme && - other.thumbColor == thumbColor && - other.thumbBorderColor == thumbBorderColor; - - @override - int get hashCode => - audioWaveformTheme.hashCode ^ - thumbColor.hashCode ^ - thumbBorderColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(DiagnosticsProperty( - 'audioWaveformTheme', audioWaveformTheme)) - ..add(ColorProperty('thumbColor', thumbColor)) - ..add(ColorProperty('thumbBorderColor', thumbBorderColor)); - } -} diff --git a/packages/stream_chat_flutter/lib/src/theme/audio_waveform_theme.dart b/packages/stream_chat_flutter/lib/src/theme/audio_waveform_theme.dart deleted file mode 100644 index dde8fc78df..0000000000 --- a/packages/stream_chat_flutter/lib/src/theme/audio_waveform_theme.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; - -/// {@template streamAudioWaveformTheme} -/// Overrides the default style of [StreamAudioWaveform] descendants. -/// -/// See also: -/// -/// * [StreamVoiceRecordingAttachmentThemeData], which is used to configure -/// this theme. -/// {@endtemplate} -class StreamAudioWaveformTheme extends InheritedTheme { - /// Creates a [StreamAudioWaveformTheme]. - /// - /// The [data] parameter must not be null. - const StreamAudioWaveformTheme({ - super.key, - required this.data, - required super.child, - }); - - /// The configuration of this theme. - final StreamAudioWaveformThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [StreamAudioWaveformTheme] widget, - /// then [StreamAudioWaveformTheme.audioWaveformSliderTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// StreamAudioWaveformTheme theme = StreamAudioWaveformTheme.of(context); - /// ``` - static StreamAudioWaveformThemeData of(BuildContext context) { - final audioWaveformTheme = - context.dependOnInheritedWidgetOfExactType(); - return audioWaveformTheme?.data ?? - StreamChatTheme.of(context).audioWaveformTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - StreamAudioWaveformTheme(data: data, child: child); - - @override - bool updateShouldNotify(StreamAudioWaveformTheme oldWidget) => - data != oldWidget.data; -} - -/// {@template streamVoiceRecordingAttachmentThemeData} -/// A style that overrides the default appearance of -/// [StreamAudioWaveformSlider] widgets when used with -/// [StreamAudioWaveformTheme] or with the overall -/// [StreamChatTheme]'s [StreamChatThemeData.audioWaveformSliderTheme]. -/// {@endtemplate} -class StreamAudioWaveformThemeData with Diagnosticable { - /// {@macro streamAudioWaveformThemeData} - const StreamAudioWaveformThemeData({ - this.color, - this.progressColor, - this.minBarHeight, - this.spacingRatio, - this.heightScale, - }); - - /// The color of the wave bars. - final Color? color; - - /// The color of the progressed wave bars. - final Color? progressColor; - - /// The minimum height of the bars. - final double? minBarHeight; - - /// The ratio of the spacing between the bars. - final double? spacingRatio; - - /// The scale of the height of the bars. - final double? heightScale; - - /// A copy of [StreamAudioWaveformThemeData] with specified attributes - /// overridden. - StreamAudioWaveformThemeData copyWith({ - Color? color, - Color? progressColor, - double? minBarHeight, - double? spacingRatio, - double? heightScale, - }) { - return StreamAudioWaveformThemeData( - color: color ?? this.color, - progressColor: progressColor ?? this.progressColor, - minBarHeight: minBarHeight ?? this.minBarHeight, - spacingRatio: spacingRatio ?? this.spacingRatio, - heightScale: heightScale ?? this.heightScale, - ); - } - - /// Merges this [StreamPollOptionsDialogThemeData] with the [other]. - StreamAudioWaveformThemeData merge( - StreamAudioWaveformThemeData? other, - ) { - if (other == null) return this; - return copyWith( - color: other.color, - progressColor: other.progressColor, - minBarHeight: other.minBarHeight, - spacingRatio: other.spacingRatio, - heightScale: other.heightScale, - ); - } - - /// Linearly interpolate between two [StreamPollOptionsDialogThemeData]. - static StreamAudioWaveformThemeData lerp( - StreamAudioWaveformThemeData a, - StreamAudioWaveformThemeData b, - double t, - ) => - StreamAudioWaveformThemeData( - color: Color.lerp(a.color, b.color, t), - progressColor: Color.lerp(a.progressColor, b.progressColor, t), - minBarHeight: lerpDouble(a.minBarHeight, b.minBarHeight, t), - spacingRatio: lerpDouble(a.spacingRatio, b.spacingRatio, t), - heightScale: lerpDouble(a.heightScale, b.heightScale, t), - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is StreamAudioWaveformThemeData && - other.color == color && - other.progressColor == progressColor && - other.minBarHeight == minBarHeight && - other.spacingRatio == spacingRatio && - other.heightScale == heightScale; - - @override - int get hashCode => - color.hashCode ^ - progressColor.hashCode ^ - minBarHeight.hashCode ^ - spacingRatio.hashCode ^ - heightScale.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties - ..add(ColorProperty('color', color)) - ..add(ColorProperty('progressColor', progressColor)) - ..add(DoubleProperty('minBarHeight', minBarHeight)) - ..add(DoubleProperty('spacingRatio', spacingRatio)) - ..add(DoubleProperty('heightScale', heightScale)); - } -} diff --git a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart index dbc36da931..e0e6b24e6a 100644 --- a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart @@ -10,8 +10,8 @@ class StreamAvatarThemeData with Diagnosticable { const StreamAvatarThemeData({ BoxConstraints? constraints, BorderRadius? borderRadius, - }) : _constraints = constraints, - _borderRadius = borderRadius; + }) : _constraints = constraints, + _borderRadius = borderRadius; final BoxConstraints? _constraints; final BorderRadius? _borderRadius; diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart index 2df24f685e..d9ea4b009e 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart @@ -35,19 +35,15 @@ class StreamChannelHeaderTheme extends InheritedTheme { /// final theme = ChannelHeaderTheme.of(context); /// ``` static StreamChannelHeaderThemeData of(BuildContext context) { - final channelHeaderTheme = - context.dependOnInheritedWidgetOfExactType(); - return channelHeaderTheme?.data ?? - StreamChatTheme.of(context).channelHeaderTheme; + final channelHeaderTheme = context.dependOnInheritedWidgetOfExactType(); + return channelHeaderTheme?.data ?? StreamChatTheme.of(context).channelHeaderTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamChannelHeaderTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamChannelHeaderTheme(data: data, child: child); @override - bool updateShouldNotify(StreamChannelHeaderTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamChannelHeaderTheme oldWidget) => data != oldWidget.data; } /// {@template channel_header_theme_data} @@ -108,8 +104,7 @@ class StreamChannelHeaderThemeData with Diagnosticable { return StreamChannelHeaderThemeData( titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), - avatarTheme: - const StreamAvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + avatarTheme: const StreamAvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), color: Color.lerp(a.color, b.color, t), ); } @@ -119,8 +114,7 @@ class StreamChannelHeaderThemeData with Diagnosticable { if (other == null) return this; return copyWith( titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, - subtitleStyle: - subtitleStyle?.merge(other.subtitleStyle) ?? other.subtitleStyle, + subtitleStyle: subtitleStyle?.merge(other.subtitleStyle) ?? other.subtitleStyle, avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, color: other.color, ); @@ -137,11 +131,7 @@ class StreamChannelHeaderThemeData with Diagnosticable { color == other.color; @override - int get hashCode => - titleStyle.hashCode ^ - subtitleStyle.hashCode ^ - avatarTheme.hashCode ^ - color.hashCode; + int get hashCode => titleStyle.hashCode ^ subtitleStyle.hashCode ^ avatarTheme.hashCode ^ color.hashCode; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart index 7452065df7..6335400790 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart @@ -35,19 +35,15 @@ class StreamChannelListHeaderTheme extends InheritedTheme { /// final theme = ChannelListHeaderTheme.of(context); /// ``` static StreamChannelListHeaderThemeData of(BuildContext context) { - final channelListHeaderTheme = context - .dependOnInheritedWidgetOfExactType(); - return channelListHeaderTheme?.data ?? - StreamChatTheme.of(context).channelListHeaderTheme; + final channelListHeaderTheme = context.dependOnInheritedWidgetOfExactType(); + return channelListHeaderTheme?.data ?? StreamChatTheme.of(context).channelListHeaderTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamChannelListHeaderTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamChannelListHeaderTheme(data: data, child: child); @override - bool updateShouldNotify(StreamChannelListHeaderTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamChannelListHeaderTheme oldWidget) => data != oldWidget.data; } /// {@template channel_list_header_theme_data} @@ -92,8 +88,7 @@ class StreamChannelListHeaderThemeData with Diagnosticable { double t, ) { return StreamChannelListHeaderThemeData( - avatarTheme: - const StreamAvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + avatarTheme: const StreamAvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), color: Color.lerp(a.color, b.color, t), titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), ); @@ -121,8 +116,7 @@ class StreamChannelListHeaderThemeData with Diagnosticable { color == other.color; @override - int get hashCode => - titleStyle.hashCode ^ avatarTheme.hashCode ^ color.hashCode; + int get hashCode => titleStyle.hashCode ^ avatarTheme.hashCode ^ color.hashCode; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index 3b9a39f658..88ea65595e 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -11,6 +11,9 @@ import 'package:stream_chat_flutter/src/utils/date_formatter.dart'; /// /// * [StreamChannelPreviewThemeData], which is used to configure this theme. /// {@endtemplate} +/// +/// This is deprecated, but currently still used by `StreamChannelInfoBottomSheet`. +@Deprecated('Use StreamChannelListItemTheme instead.') class StreamChannelPreviewTheme extends InheritedTheme { /// Creates a [StreamChannelPreviewTheme]. /// @@ -35,19 +38,15 @@ class StreamChannelPreviewTheme extends InheritedTheme { /// final theme = ChannelPreviewTheme.of(context); /// ``` static StreamChannelPreviewThemeData of(BuildContext context) { - final channelPreviewTheme = - context.dependOnInheritedWidgetOfExactType(); - return channelPreviewTheme?.data ?? - StreamChatTheme.of(context).channelPreviewTheme; + final channelPreviewTheme = context.dependOnInheritedWidgetOfExactType(); + return channelPreviewTheme?.data ?? StreamChatTheme.of(context).channelPreviewTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamChannelPreviewTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamChannelPreviewTheme(data: data, child: child); @override - bool updateShouldNotify(StreamChannelPreviewTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamChannelPreviewTheme oldWidget) => data != oldWidget.data; } /// {@template channelPreviewThemeData} @@ -124,8 +123,7 @@ class StreamChannelPreviewThemeData with Diagnosticable { avatarTheme: avatarTheme ?? this.avatarTheme, unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor, indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize, - lastMessageAtFormatter: - lastMessageAtFormatter ?? this.lastMessageAtFormatter, + lastMessageAtFormatter: lastMessageAtFormatter ?? this.lastMessageAtFormatter, ); } @@ -136,17 +134,13 @@ class StreamChannelPreviewThemeData with Diagnosticable { double t, ) { return StreamChannelPreviewThemeData( - avatarTheme: - const StreamAvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + avatarTheme: const StreamAvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), indicatorIconSize: a.indicatorIconSize, - lastMessageAtStyle: - TextStyle.lerp(a.lastMessageAtStyle, b.lastMessageAtStyle, t), + lastMessageAtStyle: TextStyle.lerp(a.lastMessageAtStyle, b.lastMessageAtStyle, t), subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), - unreadCounterColor: - Color.lerp(a.unreadCounterColor, b.unreadCounterColor, t), - lastMessageAtFormatter: - t < 0.5 ? a.lastMessageAtFormatter : b.lastMessageAtFormatter, + unreadCounterColor: Color.lerp(a.unreadCounterColor, b.unreadCounterColor, t), + lastMessageAtFormatter: t < 0.5 ? a.lastMessageAtFormatter : b.lastMessageAtFormatter, ); } @@ -155,14 +149,11 @@ class StreamChannelPreviewThemeData with Diagnosticable { if (other == null) return this; return copyWith( titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, - subtitleStyle: - subtitleStyle?.merge(other.subtitleStyle) ?? other.subtitleStyle, - lastMessageAtStyle: lastMessageAtStyle?.merge(other.lastMessageAtStyle) ?? - other.lastMessageAtStyle, + subtitleStyle: subtitleStyle?.merge(other.subtitleStyle) ?? other.subtitleStyle, + lastMessageAtStyle: lastMessageAtStyle?.merge(other.lastMessageAtStyle) ?? other.lastMessageAtStyle, avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, unreadCounterColor: other.unreadCounterColor, - lastMessageAtFormatter: - other.lastMessageAtFormatter ?? lastMessageAtFormatter, + lastMessageAtFormatter: other.lastMessageAtFormatter ?? lastMessageAtFormatter, ); } @@ -198,7 +189,6 @@ class StreamChannelPreviewThemeData with Diagnosticable { ..add(DiagnosticsProperty('lastMessageAtStyle', lastMessageAtStyle)) ..add(DiagnosticsProperty('avatarTheme', avatarTheme)) ..add(ColorProperty('unreadCounterColor', unreadCounterColor)) - ..add(DiagnosticsProperty( - 'lastMessageAtFormatter', lastMessageAtFormatter)); + ..add(DiagnosticsProperty('lastMessageAtFormatter', lastMessageAtFormatter)); } } 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/draft_list_tile_theme.dart b/packages/stream_chat_flutter/lib/src/theme/draft_list_tile_theme.dart index 20b8973657..dce3292e6e 100644 --- a/packages/stream_chat_flutter/lib/src/theme/draft_list_tile_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/draft_list_tile_theme.dart @@ -29,19 +29,15 @@ class StreamDraftListTileTheme extends InheritedTheme { /// If there is no enclosing [StreamDraftListTileTheme] widget, then /// [StreamChatThemeData.draftListTileTheme] is used. static StreamDraftListTileThemeData of(BuildContext context) { - final draftListTileTheme = - context.dependOnInheritedWidgetOfExactType(); - return draftListTileTheme?.data ?? - StreamChatTheme.of(context).draftListTileTheme; + final draftListTileTheme = context.dependOnInheritedWidgetOfExactType(); + return draftListTileTheme?.data ?? StreamChatTheme.of(context).draftListTileTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamDraftListTileTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamDraftListTileTheme(data: data, child: child); @override - bool updateShouldNotify(StreamDraftListTileTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamDraftListTileTheme oldWidget) => data != oldWidget.data; } /// {@template streamDraftListTileThemeData} @@ -101,17 +97,14 @@ class StreamDraftListTileThemeData with Diagnosticable { TextStyle? draftTimestampStyle, DateFormatter? draftTimestampFormatter, Color? draftIconColor, - }) => - StreamDraftListTileThemeData( - padding: padding ?? this.padding, - backgroundColor: backgroundColor ?? this.backgroundColor, - draftChannelNameStyle: - draftChannelNameStyle ?? this.draftChannelNameStyle, - draftMessageStyle: draftMessageStyle ?? this.draftMessageStyle, - draftTimestampStyle: draftTimestampStyle ?? this.draftTimestampStyle, - draftTimestampFormatter: - draftTimestampFormatter ?? this.draftTimestampFormatter, - ); + }) => StreamDraftListTileThemeData( + padding: padding ?? this.padding, + backgroundColor: backgroundColor ?? this.backgroundColor, + draftChannelNameStyle: draftChannelNameStyle ?? this.draftChannelNameStyle, + draftMessageStyle: draftMessageStyle ?? this.draftMessageStyle, + draftTimestampStyle: draftTimestampStyle ?? this.draftTimestampStyle, + draftTimestampFormatter: draftTimestampFormatter ?? this.draftTimestampFormatter, + ); /// Merges this [StreamDraftListTileThemeData] with the [other]. StreamDraftListTileThemeData merge( @@ -133,28 +126,26 @@ class StreamDraftListTileThemeData with Diagnosticable { StreamDraftListTileThemeData? a, StreamDraftListTileThemeData? b, double t, - ) => - StreamDraftListTileThemeData( - padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t), - backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), - draftChannelNameStyle: TextStyle.lerp( - a?.draftChannelNameStyle, - b?.draftChannelNameStyle, - t, - ), - draftMessageStyle: TextStyle.lerp( - a?.draftMessageStyle, - b?.draftMessageStyle, - t, - ), - draftTimestampStyle: TextStyle.lerp( - a?.draftTimestampStyle, - b?.draftTimestampStyle, - t, - ), - draftTimestampFormatter: - t < 0.5 ? a?.draftTimestampFormatter : b?.draftTimestampFormatter, - ); + ) => StreamDraftListTileThemeData( + padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t), + backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), + draftChannelNameStyle: TextStyle.lerp( + a?.draftChannelNameStyle, + b?.draftChannelNameStyle, + t, + ), + draftMessageStyle: TextStyle.lerp( + a?.draftMessageStyle, + b?.draftMessageStyle, + t, + ), + draftTimestampStyle: TextStyle.lerp( + a?.draftTimestampStyle, + b?.draftTimestampStyle, + t, + ), + draftTimestampFormatter: t < 0.5 ? a?.draftTimestampFormatter : b?.draftTimestampFormatter, + ); @override bool operator ==(Object other) => diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart index 84e1688a60..48733b8632 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart @@ -33,19 +33,15 @@ class StreamGalleryFooterTheme extends InheritedTheme { /// ImageFooterTheme theme = ImageFooterTheme.of(context); /// ``` static StreamGalleryFooterThemeData of(BuildContext context) { - final imageFooterTheme = - context.dependOnInheritedWidgetOfExactType(); - return imageFooterTheme?.data ?? - StreamChatTheme.of(context).galleryFooterTheme; + final imageFooterTheme = context.dependOnInheritedWidgetOfExactType(); + return imageFooterTheme?.data ?? StreamChatTheme.of(context).galleryFooterTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamGalleryFooterTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamGalleryFooterTheme(data: data, child: child); @override - bool updateShouldNotify(StreamGalleryFooterTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamGalleryFooterTheme oldWidget) => data != oldWidget.data; } /// {@template galleryFooterThemeData} @@ -128,14 +124,10 @@ class StreamGalleryFooterThemeData with Diagnosticable { shareIconColor: shareIconColor ?? this.shareIconColor, titleTextStyle: titleTextStyle ?? this.titleTextStyle, gridIconButtonColor: gridIconButtonColor ?? this.gridIconButtonColor, - bottomSheetBarrierColor: - bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, - bottomSheetBackgroundColor: - bottomSheetBackgroundColor ?? this.bottomSheetBackgroundColor, - bottomSheetPhotosTextStyle: - bottomSheetPhotosTextStyle ?? this.bottomSheetPhotosTextStyle, - bottomSheetCloseIconColor: - bottomSheetCloseIconColor ?? this.bottomSheetCloseIconColor, + bottomSheetBarrierColor: bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, + bottomSheetBackgroundColor: bottomSheetBackgroundColor ?? this.bottomSheetBackgroundColor, + bottomSheetPhotosTextStyle: bottomSheetPhotosTextStyle ?? this.bottomSheetPhotosTextStyle, + bottomSheetCloseIconColor: bottomSheetCloseIconColor ?? this.bottomSheetCloseIconColor, ); } @@ -151,10 +143,8 @@ class StreamGalleryFooterThemeData with Diagnosticable { backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t), titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), - gridIconButtonColor: - Color.lerp(a.gridIconButtonColor, b.gridIconButtonColor, t), - bottomSheetBarrierColor: - Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), + gridIconButtonColor: Color.lerp(a.gridIconButtonColor, b.gridIconButtonColor, t), + bottomSheetBarrierColor: Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), bottomSheetBackgroundColor: Color.lerp( a.bottomSheetBackgroundColor, b.bottomSheetBackgroundColor, @@ -222,17 +212,23 @@ class StreamGalleryFooterThemeData with Diagnosticable { ..add(DiagnosticsProperty('titleTextStyle', titleTextStyle)) ..add(ColorProperty('gridIconButtonColor', gridIconButtonColor)) ..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor)) - ..add(ColorProperty( - 'bottomSheetBackgroundColor', - bottomSheetBackgroundColor, - )) - ..add(DiagnosticsProperty( - 'bottomSheetPhotosTextStyle', - bottomSheetPhotosTextStyle, - )) - ..add(ColorProperty( - 'bottomSheetCloseIconColor', - bottomSheetCloseIconColor, - )); + ..add( + ColorProperty( + 'bottomSheetBackgroundColor', + bottomSheetBackgroundColor, + ), + ) + ..add( + DiagnosticsProperty( + 'bottomSheetPhotosTextStyle', + bottomSheetPhotosTextStyle, + ), + ) + ..add( + ColorProperty( + 'bottomSheetCloseIconColor', + bottomSheetCloseIconColor, + ), + ); } } diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart index 90977d4c9f..8add8e4775 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart @@ -33,19 +33,15 @@ class StreamGalleryHeaderTheme extends InheritedTheme { /// ImageHeaderTheme theme = ImageHeaderTheme.of(context); /// ``` static StreamGalleryHeaderThemeData of(BuildContext context) { - final galleryHeaderTheme = - context.dependOnInheritedWidgetOfExactType(); - return galleryHeaderTheme?.data ?? - StreamChatTheme.of(context).galleryHeaderTheme; + final galleryHeaderTheme = context.dependOnInheritedWidgetOfExactType(); + return galleryHeaderTheme?.data ?? StreamChatTheme.of(context).galleryHeaderTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamGalleryHeaderTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamGalleryHeaderTheme(data: data, child: child); @override - bool updateShouldNotify(StreamGalleryHeaderTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamGalleryHeaderTheme oldWidget) => data != oldWidget.data; } /// {@template galleryHeaderThemeData} @@ -111,8 +107,7 @@ class StreamGalleryHeaderThemeData with Diagnosticable { iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor, titleTextStyle: titleTextStyle ?? this.titleTextStyle, subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle, - bottomSheetBarrierColor: - bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, + bottomSheetBarrierColor: bottomSheetBarrierColor ?? this.bottomSheetBarrierColor, ); } @@ -127,13 +122,10 @@ class StreamGalleryHeaderThemeData with Diagnosticable { return StreamGalleryHeaderThemeData( closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t), backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - iconMenuPointColor: - Color.lerp(a.iconMenuPointColor, b.iconMenuPointColor, t), + iconMenuPointColor: Color.lerp(a.iconMenuPointColor, b.iconMenuPointColor, t), titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), - subtitleTextStyle: - TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t), - bottomSheetBarrierColor: - Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), + subtitleTextStyle: TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t), + bottomSheetBarrierColor: Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), ); } diff --git a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart index a97df00b1e..728ebd2cfb 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart @@ -35,19 +35,15 @@ class StreamMessageInputTheme extends InheritedTheme { /// final theme = MessageInputTheme.of(context); /// ``` static StreamMessageInputThemeData of(BuildContext context) { - final messageInputTheme = - context.dependOnInheritedWidgetOfExactType(); - return messageInputTheme?.data ?? - StreamChatTheme.of(context).messageInputTheme; + final messageInputTheme = context.dependOnInheritedWidgetOfExactType(); + return messageInputTheme?.data ?? StreamChatTheme.of(context).messageInputTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamMessageInputTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamMessageInputTheme(data: data, child: child); @override - bool updateShouldNotify(StreamMessageInputTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamMessageInputTheme oldWidget) => data != oldWidget.data; } /// {@template messageInputThemeData} @@ -160,13 +156,11 @@ class StreamMessageInputThemeData with Diagnosticable { bool? useSystemAttachmentPicker, }) { return StreamMessageInputThemeData( - sendAnimationDuration: - sendAnimationDuration ?? this.sendAnimationDuration, + sendAnimationDuration: sendAnimationDuration ?? this.sendAnimationDuration, inputBackgroundColor: inputBackgroundColor ?? this.inputBackgroundColor, actionButtonColor: actionButtonColor ?? this.actionButtonColor, sendButtonColor: sendButtonColor ?? this.sendButtonColor, - actionButtonIdleColor: - actionButtonIdleColor ?? this.actionButtonIdleColor, + actionButtonIdleColor: actionButtonIdleColor ?? this.actionButtonIdleColor, linkHighlightColor: linkHighlightColor ?? this.linkHighlightColor, expandButtonColor: expandButtonColor ?? this.expandButtonColor, inputTextStyle: inputTextStyle ?? this.inputTextStyle, @@ -178,8 +172,7 @@ class StreamMessageInputThemeData with Diagnosticable { enableSafeArea: enableSafeArea ?? this.enableSafeArea, elevation: elevation ?? this.elevation, shadow: shadow ?? this.shadow, - useSystemAttachmentPicker: - useSystemAttachmentPicker ?? this.useSystemAttachmentPicker, + useSystemAttachmentPicker: useSystemAttachmentPicker ?? this.useSystemAttachmentPicker, ); } @@ -190,25 +183,17 @@ class StreamMessageInputThemeData with Diagnosticable { double t, ) { return StreamMessageInputThemeData( - actionButtonColor: - Color.lerp(a.actionButtonColor, b.actionButtonColor, t), - actionButtonIdleColor: - Color.lerp(a.actionButtonIdleColor, b.actionButtonIdleColor, t), - activeBorderGradient: - Gradient.lerp(a.activeBorderGradient, b.activeBorderGradient, t), + actionButtonColor: Color.lerp(a.actionButtonColor, b.actionButtonColor, t), + actionButtonIdleColor: Color.lerp(a.actionButtonIdleColor, b.actionButtonIdleColor, t), + activeBorderGradient: Gradient.lerp(a.activeBorderGradient, b.activeBorderGradient, t), borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), - expandButtonColor: - Color.lerp(a.expandButtonColor, b.expandButtonColor, t), - linkHighlightColor: - Color.lerp(a.linkHighlightColor, b.linkHighlightColor, t), - idleBorderGradient: - Gradient.lerp(a.idleBorderGradient, b.idleBorderGradient, t), - inputBackgroundColor: - Color.lerp(a.inputBackgroundColor, b.inputBackgroundColor, t), + expandButtonColor: Color.lerp(a.expandButtonColor, b.expandButtonColor, t), + linkHighlightColor: Color.lerp(a.linkHighlightColor, b.linkHighlightColor, t), + idleBorderGradient: Gradient.lerp(a.idleBorderGradient, b.idleBorderGradient, t), + inputBackgroundColor: Color.lerp(a.inputBackgroundColor, b.inputBackgroundColor, t), inputTextStyle: TextStyle.lerp(a.inputTextStyle, b.inputTextStyle, t), sendButtonColor: Color.lerp(a.sendButtonColor, b.sendButtonColor, t), - sendButtonIdleColor: - Color.lerp(a.sendButtonIdleColor, b.sendButtonIdleColor, t), + sendButtonIdleColor: Color.lerp(a.sendButtonIdleColor, b.sendButtonIdleColor, t), sendAnimationDuration: a.sendAnimationDuration, inputDecoration: a.inputDecoration, enableSafeArea: a.enableSafeArea, @@ -228,10 +213,8 @@ class StreamMessageInputThemeData with Diagnosticable { actionButtonIdleColor: other.actionButtonIdleColor, sendButtonColor: other.sendButtonColor, sendButtonIdleColor: other.sendButtonIdleColor, - inputTextStyle: - inputTextStyle?.merge(other.inputTextStyle) ?? other.inputTextStyle, - inputDecoration: inputDecoration?.merge(other.inputDecoration) ?? - other.inputDecoration, + inputTextStyle: inputTextStyle?.merge(other.inputTextStyle) ?? other.inputTextStyle, + inputDecoration: inputDecoration?.merge(other.inputDecoration) ?? other.inputDecoration, activeBorderGradient: other.activeBorderGradient, idleBorderGradient: other.idleBorderGradient, borderRadius: other.borderRadius, @@ -307,7 +290,6 @@ class StreamMessageInputThemeData with Diagnosticable { ..add(DiagnosticsProperty('elevation', elevation)) ..add(DiagnosticsProperty('shadow', shadow)) ..add(DiagnosticsProperty('enableSafeArea', enableSafeArea)) - ..add(DiagnosticsProperty( - 'useSystemAttachmentPicker', useSystemAttachmentPicker)); + ..add(DiagnosticsProperty('useSystemAttachmentPicker', useSystemAttachmentPicker)); } } diff --git a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart index d21b3725e9..66eb1b9644 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart @@ -33,19 +33,15 @@ class StreamMessageListViewTheme extends InheritedTheme { /// MessageListViewTheme theme = MessageListViewTheme.of(context); /// ``` static StreamMessageListViewThemeData of(BuildContext context) { - final messageListViewTheme = context - .dependOnInheritedWidgetOfExactType(); - return messageListViewTheme?.data ?? - StreamChatTheme.of(context).messageListViewTheme; + final messageListViewTheme = context.dependOnInheritedWidgetOfExactType(); + return messageListViewTheme?.data ?? StreamChatTheme.of(context).messageListViewTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamMessageListViewTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamMessageListViewTheme(data: data, child: child); @override - bool updateShouldNotify(StreamMessageListViewTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamMessageListViewTheme oldWidget) => data != oldWidget.data; } /// {@template messageListViewThemeData} diff --git a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart index 62b38d4edd..1ad66765e0 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart @@ -140,29 +140,20 @@ class StreamMessageThemeData with Diagnosticable { createdAtStyle: createdAtStyle ?? this.createdAtStyle, createdAtFormatter: createdAtFormatter ?? this.createdAtFormatter, messageDeletedStyle: messageDeletedStyle ?? this.messageDeletedStyle, - messageBackgroundColor: - messageBackgroundColor ?? this.messageBackgroundColor, - messageBackgroundGradient: - messageBackgroundGradient ?? this.messageBackgroundGradient, + messageBackgroundColor: messageBackgroundColor ?? this.messageBackgroundColor, + messageBackgroundGradient: messageBackgroundGradient ?? this.messageBackgroundGradient, messageBorderColor: messageBorderColor ?? this.messageBorderColor, avatarTheme: avatarTheme ?? this.avatarTheme, repliesStyle: repliesStyle ?? this.repliesStyle, - reactionsBackgroundColor: - reactionsBackgroundColor ?? this.reactionsBackgroundColor, + reactionsBackgroundColor: reactionsBackgroundColor ?? this.reactionsBackgroundColor, reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor, - urlAttachmentBackgroundColor: - urlAttachmentBackgroundColor ?? this.urlAttachmentBackgroundColor, - urlAttachmentHostStyle: - urlAttachmentHostStyle ?? this.urlAttachmentHostStyle, - urlAttachmentTitleStyle: - urlAttachmentTitleStyle ?? this.urlAttachmentTitleStyle, - urlAttachmentTextStyle: - urlAttachmentTextStyle ?? this.urlAttachmentTextStyle, - urlAttachmentTitleMaxLine: - urlAttachmentTitleMaxLine ?? this.urlAttachmentTitleMaxLine, - urlAttachmentTextMaxLine: - urlAttachmentTextMaxLine ?? this.urlAttachmentTextMaxLine, + urlAttachmentBackgroundColor: urlAttachmentBackgroundColor ?? this.urlAttachmentBackgroundColor, + urlAttachmentHostStyle: urlAttachmentHostStyle ?? this.urlAttachmentHostStyle, + urlAttachmentTitleStyle: urlAttachmentTitleStyle ?? this.urlAttachmentTitleStyle, + urlAttachmentTextStyle: urlAttachmentTextStyle ?? this.urlAttachmentTextStyle, + urlAttachmentTitleMaxLine: urlAttachmentTitleMaxLine ?? this.urlAttachmentTitleMaxLine, + urlAttachmentTextMaxLine: urlAttachmentTextMaxLine ?? this.urlAttachmentTextMaxLine, ); } @@ -173,41 +164,30 @@ class StreamMessageThemeData with Diagnosticable { double t, ) { return StreamMessageThemeData( - avatarTheme: - const StreamAvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), - messageAuthorStyle: - TextStyle.lerp(a.messageAuthorStyle, b.messageAuthorStyle, t), + avatarTheme: const StreamAvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + messageAuthorStyle: TextStyle.lerp(a.messageAuthorStyle, b.messageAuthorStyle, t), createdAtStyle: TextStyle.lerp(a.createdAtStyle, b.createdAtStyle, t), createdAtFormatter: t < 0.5 ? a.createdAtFormatter : b.createdAtFormatter, - messageDeletedStyle: - TextStyle.lerp(a.messageDeletedStyle, b.messageDeletedStyle, t), - messageBackgroundColor: - Color.lerp(a.messageBackgroundColor, b.messageBackgroundColor, t), - messageBackgroundGradient: - t < 0.5 ? a.messageBackgroundGradient : b.messageBackgroundGradient, - messageBorderColor: - Color.lerp(a.messageBorderColor, b.messageBorderColor, t), - messageLinksStyle: - TextStyle.lerp(a.messageLinksStyle, b.messageLinksStyle, t), - messageTextStyle: - TextStyle.lerp(a.messageTextStyle, b.messageTextStyle, t), + messageDeletedStyle: TextStyle.lerp(a.messageDeletedStyle, b.messageDeletedStyle, t), + messageBackgroundColor: Color.lerp(a.messageBackgroundColor, b.messageBackgroundColor, t), + messageBackgroundGradient: t < 0.5 ? a.messageBackgroundGradient : b.messageBackgroundGradient, + messageBorderColor: Color.lerp(a.messageBorderColor, b.messageBorderColor, t), + messageLinksStyle: TextStyle.lerp(a.messageLinksStyle, b.messageLinksStyle, t), + messageTextStyle: TextStyle.lerp(a.messageTextStyle, b.messageTextStyle, t), reactionsBackgroundColor: Color.lerp( a.reactionsBackgroundColor, b.reactionsBackgroundColor, t, ), - reactionsBorderColor: - Color.lerp(a.messageBorderColor, b.reactionsBorderColor, t), - reactionsMaskColor: - Color.lerp(a.reactionsMaskColor, b.reactionsMaskColor, t), + reactionsBorderColor: Color.lerp(a.messageBorderColor, b.reactionsBorderColor, t), + reactionsMaskColor: Color.lerp(a.reactionsMaskColor, b.reactionsMaskColor, t), repliesStyle: TextStyle.lerp(a.repliesStyle, b.repliesStyle, t), urlAttachmentBackgroundColor: Color.lerp( a.urlAttachmentBackgroundColor, b.urlAttachmentBackgroundColor, t, ), - urlAttachmentHostStyle: - TextStyle.lerp(a.urlAttachmentHostStyle, b.urlAttachmentHostStyle, t), + urlAttachmentHostStyle: TextStyle.lerp(a.urlAttachmentHostStyle, b.urlAttachmentHostStyle, t), urlAttachmentTextStyle: TextStyle.lerp( a.urlAttachmentTextStyle, b.urlAttachmentTextStyle, @@ -235,20 +215,13 @@ class StreamMessageThemeData with Diagnosticable { StreamMessageThemeData merge(StreamMessageThemeData? other) { if (other == null) return this; return copyWith( - messageTextStyle: messageTextStyle?.merge(other.messageTextStyle) ?? - other.messageTextStyle, - messageAuthorStyle: messageAuthorStyle?.merge(other.messageAuthorStyle) ?? - other.messageAuthorStyle, - messageLinksStyle: messageLinksStyle?.merge(other.messageLinksStyle) ?? - other.messageLinksStyle, - createdAtStyle: - createdAtStyle?.merge(other.createdAtStyle) ?? other.createdAtStyle, + messageTextStyle: messageTextStyle?.merge(other.messageTextStyle) ?? other.messageTextStyle, + messageAuthorStyle: messageAuthorStyle?.merge(other.messageAuthorStyle) ?? other.messageAuthorStyle, + messageLinksStyle: messageLinksStyle?.merge(other.messageLinksStyle) ?? other.messageLinksStyle, + createdAtStyle: createdAtStyle?.merge(other.createdAtStyle) ?? other.createdAtStyle, createdAtFormatter: other.createdAtFormatter ?? createdAtFormatter, - messageDeletedStyle: - messageDeletedStyle?.merge(other.messageDeletedStyle) ?? - other.messageDeletedStyle, - repliesStyle: - repliesStyle?.merge(other.repliesStyle) ?? other.repliesStyle, + messageDeletedStyle: messageDeletedStyle?.merge(other.messageDeletedStyle) ?? other.messageDeletedStyle, + repliesStyle: repliesStyle?.merge(other.repliesStyle) ?? other.repliesStyle, messageBackgroundColor: other.messageBackgroundColor, messageBackgroundGradient: other.messageBackgroundGradient, messageBorderColor: other.messageBorderColor, @@ -326,36 +299,47 @@ class StreamMessageThemeData with Diagnosticable { ..add(DiagnosticsProperty('messageDeletedStyle', messageDeletedStyle)) ..add(DiagnosticsProperty('repliesStyle', repliesStyle)) ..add(ColorProperty('messageBackgroundColor', messageBackgroundColor)) - ..add(DiagnosticsProperty( - 'messageBackgroundGradient', messageBackgroundGradient)) + ..add(DiagnosticsProperty('messageBackgroundGradient', messageBackgroundGradient)) ..add(ColorProperty('messageBorderColor', messageBorderColor)) ..add(DiagnosticsProperty('avatarTheme', avatarTheme)) ..add(ColorProperty('reactionsBackgroundColor', reactionsBackgroundColor)) ..add(ColorProperty('reactionsBorderColor', reactionsBorderColor)) ..add(ColorProperty('reactionsMaskColor', reactionsMaskColor)) - ..add(ColorProperty( - 'urlAttachmentBackgroundColor', - urlAttachmentBackgroundColor, - )) - ..add(DiagnosticsProperty( - 'urlAttachmentHostStyle', - urlAttachmentHostStyle, - )) - ..add(DiagnosticsProperty( - 'urlAttachmentTitleStyle', - urlAttachmentTitleStyle, - )) - ..add(DiagnosticsProperty( - 'urlAttachmentTextStyle', - urlAttachmentTextStyle, - )) - ..add(DiagnosticsProperty( - 'urlAttachmentTitleMaxLine', - urlAttachmentTitleMaxLine, - )) - ..add(DiagnosticsProperty( - 'urlAttachmentTextMaxLine', - urlAttachmentTextMaxLine, - )); + ..add( + ColorProperty( + 'urlAttachmentBackgroundColor', + urlAttachmentBackgroundColor, + ), + ) + ..add( + DiagnosticsProperty( + 'urlAttachmentHostStyle', + urlAttachmentHostStyle, + ), + ) + ..add( + DiagnosticsProperty( + 'urlAttachmentTitleStyle', + urlAttachmentTitleStyle, + ), + ) + ..add( + DiagnosticsProperty( + 'urlAttachmentTextStyle', + urlAttachmentTextStyle, + ), + ) + ..add( + DiagnosticsProperty( + 'urlAttachmentTitleMaxLine', + urlAttachmentTitleMaxLine, + ), + ) + ..add( + DiagnosticsProperty( + 'urlAttachmentTextMaxLine', + urlAttachmentTextMaxLine, + ), + ); } } diff --git a/packages/stream_chat_flutter/lib/src/theme/poll_comments_dialog_theme.dart b/packages/stream_chat_flutter/lib/src/theme/poll_comments_dialog_theme.dart index 9ad433546e..f404fc6c36 100644 --- a/packages/stream_chat_flutter/lib/src/theme/poll_comments_dialog_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/poll_comments_dialog_theme.dart @@ -30,19 +30,15 @@ class StreamPollCommentsDialogTheme extends InheritedTheme { /// If there is no enclosing [StreamPollCommentsDialogTheme] widget, then /// [StreamChatThemeData.pollCommentsDialogTheme] is used. static StreamPollCommentsDialogThemeData of(BuildContext context) { - final pollCommentsDialogTheme = context - .dependOnInheritedWidgetOfExactType(); - return pollCommentsDialogTheme?.data ?? - StreamChatTheme.of(context).pollCommentsDialogTheme; + final pollCommentsDialogTheme = context.dependOnInheritedWidgetOfExactType(); + return pollCommentsDialogTheme?.data ?? StreamChatTheme.of(context).pollCommentsDialogTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamPollCommentsDialogTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamPollCommentsDialogTheme(data: data, child: child); @override - bool updateShouldNotify(StreamPollCommentsDialogTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamPollCommentsDialogTheme oldWidget) => data != oldWidget.data; } /// {@template streamPollCommentsDialogThemeData} @@ -97,22 +93,16 @@ class StreamPollCommentsDialogThemeData with Diagnosticable { Color? pollCommentItemBackgroundColor, BorderRadius? pollCommentItemBorderRadius, ButtonStyle? updateYourCommentButtonStyle, - }) => - StreamPollCommentsDialogThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - appBarElevation: appBarElevation ?? this.appBarElevation, - appBarBackgroundColor: - appBarBackgroundColor ?? this.appBarBackgroundColor, - appBarForegroundColor: - appBarForegroundColor ?? this.appBarForegroundColor, - appBarTitleTextStyle: appBarTitleTextStyle ?? this.appBarTitleTextStyle, - pollCommentItemBackgroundColor: pollCommentItemBackgroundColor ?? - this.pollCommentItemBackgroundColor, - pollCommentItemBorderRadius: - pollCommentItemBorderRadius ?? this.pollCommentItemBorderRadius, - updateYourCommentButtonStyle: - updateYourCommentButtonStyle ?? this.updateYourCommentButtonStyle, - ); + }) => StreamPollCommentsDialogThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + appBarElevation: appBarElevation ?? this.appBarElevation, + appBarBackgroundColor: appBarBackgroundColor ?? this.appBarBackgroundColor, + appBarForegroundColor: appBarForegroundColor ?? this.appBarForegroundColor, + appBarTitleTextStyle: appBarTitleTextStyle ?? this.appBarTitleTextStyle, + pollCommentItemBackgroundColor: pollCommentItemBackgroundColor ?? this.pollCommentItemBackgroundColor, + pollCommentItemBorderRadius: pollCommentItemBorderRadius ?? this.pollCommentItemBorderRadius, + updateYourCommentButtonStyle: updateYourCommentButtonStyle ?? this.updateYourCommentButtonStyle, + ); /// Merges this [StreamPollCommentsDialogThemeData] with the [other]. StreamPollCommentsDialogThemeData merge( @@ -140,12 +130,9 @@ class StreamPollCommentsDialogThemeData with Diagnosticable { return StreamPollCommentsDialogThemeData( backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), appBarElevation: lerpDouble(a?.appBarElevation, b?.appBarElevation, t), - appBarBackgroundColor: - Color.lerp(a?.appBarBackgroundColor, b?.appBarBackgroundColor, t), - appBarForegroundColor: - Color.lerp(a?.appBarForegroundColor, b?.appBarForegroundColor, t), - appBarTitleTextStyle: - TextStyle.lerp(a?.appBarTitleTextStyle, b?.appBarTitleTextStyle, t), + appBarBackgroundColor: Color.lerp(a?.appBarBackgroundColor, b?.appBarBackgroundColor, t), + appBarForegroundColor: Color.lerp(a?.appBarForegroundColor, b?.appBarForegroundColor, t), + appBarTitleTextStyle: TextStyle.lerp(a?.appBarTitleTextStyle, b?.appBarTitleTextStyle, t), pollCommentItemBackgroundColor: Color.lerp( a?.pollCommentItemBackgroundColor, b?.pollCommentItemBackgroundColor, @@ -173,8 +160,7 @@ class StreamPollCommentsDialogThemeData with Diagnosticable { other.appBarBackgroundColor == appBarBackgroundColor && other.appBarForegroundColor == appBarForegroundColor && other.appBarTitleTextStyle == appBarTitleTextStyle && - other.pollCommentItemBackgroundColor == - pollCommentItemBackgroundColor && + other.pollCommentItemBackgroundColor == pollCommentItemBackgroundColor && other.pollCommentItemBorderRadius == pollCommentItemBorderRadius && other.updateYourCommentButtonStyle == updateYourCommentButtonStyle; diff --git a/packages/stream_chat_flutter/lib/src/theme/poll_creator_theme.dart b/packages/stream_chat_flutter/lib/src/theme/poll_creator_theme.dart index 5287547603..940fdbb4e2 100644 --- a/packages/stream_chat_flutter/lib/src/theme/poll_creator_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/poll_creator_theme.dart @@ -35,19 +35,15 @@ class StreamPollCreatorTheme extends InheritedTheme { /// StreamPollCreatorTheme theme = StreamPollCreatorTheme.of(context); /// ``` static StreamPollCreatorThemeData of(BuildContext context) { - final pollCreatorTheme = - context.dependOnInheritedWidgetOfExactType(); - return pollCreatorTheme?.data ?? - StreamChatTheme.of(context).pollCreatorTheme; + final pollCreatorTheme = context.dependOnInheritedWidgetOfExactType(); + return pollCreatorTheme?.data ?? StreamChatTheme.of(context).pollCreatorTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamPollCreatorTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamPollCreatorTheme(data: data, child: child); @override - bool updateShouldNotify(StreamPollCreatorTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamPollCreatorTheme oldWidget) => data != oldWidget.data; } /// {@template streamPollCreatorThemeData} @@ -173,40 +169,24 @@ class StreamPollCreatorThemeData with Diagnosticable { backgroundColor: backgroundColor ?? this.backgroundColor, appBarTitleStyle: appBarTitleStyle ?? this.appBarTitleStyle, appBarElevation: appBarElevation ?? this.appBarElevation, - appBarBackgroundColor: - appBarBackgroundColor ?? this.appBarBackgroundColor, - appBarForegroundColor: - appBarForegroundColor ?? this.appBarForegroundColor, - questionTextFieldFillColor: - questionTextFieldFillColor ?? this.questionTextFieldFillColor, + appBarBackgroundColor: appBarBackgroundColor ?? this.appBarBackgroundColor, + appBarForegroundColor: appBarForegroundColor ?? this.appBarForegroundColor, + questionTextFieldFillColor: questionTextFieldFillColor ?? this.questionTextFieldFillColor, questionHeaderStyle: questionHeaderStyle ?? this.questionHeaderStyle, - questionTextFieldStyle: - questionTextFieldStyle ?? this.questionTextFieldStyle, - questionTextFieldErrorStyle: - questionTextFieldErrorStyle ?? this.questionTextFieldErrorStyle, - questionTextFieldBorderRadius: - questionTextFieldBorderRadius ?? this.questionTextFieldBorderRadius, - optionsTextFieldFillColor: - optionsTextFieldFillColor ?? this.optionsTextFieldFillColor, + questionTextFieldStyle: questionTextFieldStyle ?? this.questionTextFieldStyle, + questionTextFieldErrorStyle: questionTextFieldErrorStyle ?? this.questionTextFieldErrorStyle, + questionTextFieldBorderRadius: questionTextFieldBorderRadius ?? this.questionTextFieldBorderRadius, + optionsTextFieldFillColor: optionsTextFieldFillColor ?? this.optionsTextFieldFillColor, optionsHeaderStyle: optionsHeaderStyle ?? this.optionsHeaderStyle, - optionsTextFieldStyle: - optionsTextFieldStyle ?? this.optionsTextFieldStyle, - optionsTextFieldErrorStyle: - optionsTextFieldErrorStyle ?? this.optionsTextFieldErrorStyle, - optionsTextFieldBorderRadius: - optionsTextFieldBorderRadius ?? this.optionsTextFieldBorderRadius, - switchListTileFillColor: - switchListTileFillColor ?? this.switchListTileFillColor, - switchListTileTitleStyle: - switchListTileTitleStyle ?? this.switchListTileTitleStyle, - switchListTileErrorStyle: - switchListTileErrorStyle ?? this.switchListTileErrorStyle, - switchListTileBorderRadius: - switchListTileBorderRadius ?? this.switchListTileBorderRadius, - actionDialogTitleStyle: - actionDialogTitleStyle ?? this.actionDialogTitleStyle, - actionDialogContentStyle: - actionDialogContentStyle ?? this.actionDialogContentStyle, + optionsTextFieldStyle: optionsTextFieldStyle ?? this.optionsTextFieldStyle, + optionsTextFieldErrorStyle: optionsTextFieldErrorStyle ?? this.optionsTextFieldErrorStyle, + optionsTextFieldBorderRadius: optionsTextFieldBorderRadius ?? this.optionsTextFieldBorderRadius, + switchListTileFillColor: switchListTileFillColor ?? this.switchListTileFillColor, + switchListTileTitleStyle: switchListTileTitleStyle ?? this.switchListTileTitleStyle, + switchListTileErrorStyle: switchListTileErrorStyle ?? this.switchListTileErrorStyle, + switchListTileBorderRadius: switchListTileBorderRadius ?? this.switchListTileBorderRadius, + actionDialogTitleStyle: actionDialogTitleStyle ?? this.actionDialogTitleStyle, + actionDialogContentStyle: actionDialogContentStyle ?? this.actionDialogContentStyle, ); } @@ -217,40 +197,24 @@ class StreamPollCreatorThemeData with Diagnosticable { backgroundColor: other.backgroundColor ?? backgroundColor, appBarTitleStyle: other.appBarTitleStyle ?? appBarTitleStyle, appBarElevation: other.appBarElevation ?? appBarElevation, - appBarBackgroundColor: - other.appBarBackgroundColor ?? appBarBackgroundColor, - appBarForegroundColor: - other.appBarForegroundColor ?? appBarForegroundColor, - questionTextFieldFillColor: - other.questionTextFieldFillColor ?? questionTextFieldFillColor, + appBarBackgroundColor: other.appBarBackgroundColor ?? appBarBackgroundColor, + appBarForegroundColor: other.appBarForegroundColor ?? appBarForegroundColor, + questionTextFieldFillColor: other.questionTextFieldFillColor ?? questionTextFieldFillColor, questionHeaderStyle: other.questionHeaderStyle ?? questionHeaderStyle, - questionTextFieldStyle: - other.questionTextFieldStyle ?? questionTextFieldStyle, - questionTextFieldErrorStyle: - other.questionTextFieldErrorStyle ?? questionTextFieldErrorStyle, - questionTextFieldBorderRadius: - other.questionTextFieldBorderRadius ?? questionTextFieldBorderRadius, - optionsTextFieldFillColor: - other.optionsTextFieldFillColor ?? optionsTextFieldFillColor, + questionTextFieldStyle: other.questionTextFieldStyle ?? questionTextFieldStyle, + questionTextFieldErrorStyle: other.questionTextFieldErrorStyle ?? questionTextFieldErrorStyle, + questionTextFieldBorderRadius: other.questionTextFieldBorderRadius ?? questionTextFieldBorderRadius, + optionsTextFieldFillColor: other.optionsTextFieldFillColor ?? optionsTextFieldFillColor, optionsHeaderStyle: other.optionsHeaderStyle ?? optionsHeaderStyle, - optionsTextFieldStyle: - other.optionsTextFieldStyle ?? optionsTextFieldStyle, - optionsTextFieldErrorStyle: - other.optionsTextFieldErrorStyle ?? optionsTextFieldErrorStyle, - optionsTextFieldBorderRadius: - other.optionsTextFieldBorderRadius ?? optionsTextFieldBorderRadius, - switchListTileFillColor: - other.switchListTileFillColor ?? switchListTileFillColor, - switchListTileTitleStyle: - other.switchListTileTitleStyle ?? switchListTileTitleStyle, - switchListTileErrorStyle: - other.switchListTileErrorStyle ?? switchListTileErrorStyle, - switchListTileBorderRadius: - other.switchListTileBorderRadius ?? switchListTileBorderRadius, - actionDialogTitleStyle: - other.actionDialogTitleStyle ?? actionDialogTitleStyle, - actionDialogContentStyle: - other.actionDialogContentStyle ?? actionDialogContentStyle, + optionsTextFieldStyle: other.optionsTextFieldStyle ?? optionsTextFieldStyle, + optionsTextFieldErrorStyle: other.optionsTextFieldErrorStyle ?? optionsTextFieldErrorStyle, + optionsTextFieldBorderRadius: other.optionsTextFieldBorderRadius ?? optionsTextFieldBorderRadius, + switchListTileFillColor: other.switchListTileFillColor ?? switchListTileFillColor, + switchListTileTitleStyle: other.switchListTileTitleStyle ?? switchListTileTitleStyle, + switchListTileErrorStyle: other.switchListTileErrorStyle ?? switchListTileErrorStyle, + switchListTileBorderRadius: other.switchListTileBorderRadius ?? switchListTileBorderRadius, + actionDialogTitleStyle: other.actionDialogTitleStyle ?? actionDialogTitleStyle, + actionDialogContentStyle: other.actionDialogContentStyle ?? actionDialogContentStyle, ); } @@ -262,45 +226,34 @@ class StreamPollCreatorThemeData with Diagnosticable { ) { return StreamPollCreatorThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - appBarTitleStyle: - TextStyle.lerp(a.appBarTitleStyle, b.appBarTitleStyle, t), + appBarTitleStyle: TextStyle.lerp(a.appBarTitleStyle, b.appBarTitleStyle, t), appBarElevation: lerpDouble(a.appBarElevation, b.appBarElevation, t), - appBarBackgroundColor: - Color.lerp(a.appBarBackgroundColor, b.appBarBackgroundColor, t), - appBarForegroundColor: - Color.lerp(a.appBarForegroundColor, b.appBarForegroundColor, t), - questionTextFieldFillColor: Color.lerp( - a.questionTextFieldFillColor, b.questionTextFieldFillColor, t), - questionHeaderStyle: - TextStyle.lerp(a.questionHeaderStyle, b.questionHeaderStyle, t), - questionTextFieldStyle: - TextStyle.lerp(a.questionTextFieldStyle, b.questionTextFieldStyle, t), - questionTextFieldErrorStyle: TextStyle.lerp( - a.questionTextFieldErrorStyle, b.questionTextFieldErrorStyle, t), + appBarBackgroundColor: Color.lerp(a.appBarBackgroundColor, b.appBarBackgroundColor, t), + appBarForegroundColor: Color.lerp(a.appBarForegroundColor, b.appBarForegroundColor, t), + questionTextFieldFillColor: Color.lerp(a.questionTextFieldFillColor, b.questionTextFieldFillColor, t), + questionHeaderStyle: TextStyle.lerp(a.questionHeaderStyle, b.questionHeaderStyle, t), + questionTextFieldStyle: TextStyle.lerp(a.questionTextFieldStyle, b.questionTextFieldStyle, t), + questionTextFieldErrorStyle: TextStyle.lerp(a.questionTextFieldErrorStyle, b.questionTextFieldErrorStyle, t), questionTextFieldBorderRadius: BorderRadius.lerp( - a.questionTextFieldBorderRadius, b.questionTextFieldBorderRadius, t), - optionsTextFieldFillColor: Color.lerp( - a.optionsTextFieldFillColor, b.optionsTextFieldFillColor, t), - optionsHeaderStyle: - TextStyle.lerp(a.optionsHeaderStyle, b.optionsHeaderStyle, t), - optionsTextFieldStyle: - TextStyle.lerp(a.optionsTextFieldStyle, b.optionsTextFieldStyle, t), - optionsTextFieldErrorStyle: TextStyle.lerp( - a.optionsTextFieldErrorStyle, b.optionsTextFieldErrorStyle, t), + a.questionTextFieldBorderRadius, + b.questionTextFieldBorderRadius, + t, + ), + optionsTextFieldFillColor: Color.lerp(a.optionsTextFieldFillColor, b.optionsTextFieldFillColor, t), + optionsHeaderStyle: TextStyle.lerp(a.optionsHeaderStyle, b.optionsHeaderStyle, t), + optionsTextFieldStyle: TextStyle.lerp(a.optionsTextFieldStyle, b.optionsTextFieldStyle, t), + optionsTextFieldErrorStyle: TextStyle.lerp(a.optionsTextFieldErrorStyle, b.optionsTextFieldErrorStyle, t), optionsTextFieldBorderRadius: BorderRadius.lerp( - a.optionsTextFieldBorderRadius, b.optionsTextFieldBorderRadius, t), - switchListTileFillColor: - Color.lerp(a.switchListTileFillColor, b.switchListTileFillColor, t), - switchListTileTitleStyle: TextStyle.lerp( - a.switchListTileTitleStyle, b.switchListTileTitleStyle, t), - switchListTileErrorStyle: TextStyle.lerp( - a.switchListTileErrorStyle, b.switchListTileErrorStyle, t), - switchListTileBorderRadius: BorderRadius.lerp( - a.switchListTileBorderRadius, b.switchListTileBorderRadius, t), - actionDialogTitleStyle: - TextStyle.lerp(a.actionDialogTitleStyle, b.actionDialogTitleStyle, t), - actionDialogContentStyle: TextStyle.lerp( - a.actionDialogContentStyle, b.actionDialogContentStyle, t), + a.optionsTextFieldBorderRadius, + b.optionsTextFieldBorderRadius, + t, + ), + switchListTileFillColor: Color.lerp(a.switchListTileFillColor, b.switchListTileFillColor, t), + switchListTileTitleStyle: TextStyle.lerp(a.switchListTileTitleStyle, b.switchListTileTitleStyle, t), + switchListTileErrorStyle: TextStyle.lerp(a.switchListTileErrorStyle, b.switchListTileErrorStyle, t), + switchListTileBorderRadius: BorderRadius.lerp(a.switchListTileBorderRadius, b.switchListTileBorderRadius, t), + actionDialogTitleStyle: TextStyle.lerp(a.actionDialogTitleStyle, b.actionDialogTitleStyle, t), + actionDialogContentStyle: TextStyle.lerp(a.actionDialogContentStyle, b.actionDialogContentStyle, t), ); } @@ -317,8 +270,7 @@ class StreamPollCreatorThemeData with Diagnosticable { other.questionHeaderStyle == questionHeaderStyle && other.questionTextFieldStyle == questionTextFieldStyle && other.questionTextFieldErrorStyle == questionTextFieldErrorStyle && - other.questionTextFieldBorderRadius == - questionTextFieldBorderRadius && + other.questionTextFieldBorderRadius == questionTextFieldBorderRadius && other.optionsTextFieldFillColor == optionsTextFieldFillColor && other.optionsHeaderStyle == optionsHeaderStyle && other.optionsTextFieldStyle == optionsTextFieldStyle && diff --git a/packages/stream_chat_flutter/lib/src/theme/poll_interactor_theme.dart b/packages/stream_chat_flutter/lib/src/theme/poll_interactor_theme.dart index 1880ad5ce7..3504e9a6d8 100644 --- a/packages/stream_chat_flutter/lib/src/theme/poll_interactor_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/poll_interactor_theme.dart @@ -1,377 +1,177 @@ -// ignore_for_file: parameter_assignments - -import 'dart:ui'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; + +part 'poll_interactor_theme.g.theme.dart'; -/// {@template streamPollInteractorTheme} -/// Overrides the default style of [StreamPollInteractorWidget] descendants. +/// Applies a poll interactor theme to descendant [StreamPollInteractor] +/// widgets. +/// +/// Wrap a subtree with [StreamPollInteractorTheme] to override poll interactor +/// styling. Access the merged theme using [StreamPollInteractorTheme.of]. +/// +/// {@tool snippet} +/// +/// Override poll interactor styling for a specific section: +/// +/// ```dart +/// StreamPollInteractorTheme( +/// data: StreamPollInteractorThemeData( +/// titleTextStyle: TextStyle(fontWeight: FontWeight.w700), +/// optionStyle: StreamPollOptionStyle( +/// progressBarStyle: StreamProgressBarStyle( +/// fillColor: Colors.green, +/// ), +/// ), +/// ), +/// child: StreamPollInteractor(poll: poll), +/// ) +/// ``` +/// {@end-tool} /// /// See also: /// -/// * [StreamPollInteractorThemeData], which is used to configure this theme. -/// {@endtemplate} +/// * [StreamPollInteractorThemeData], which describes the poll interactor +/// theme. +/// * [StreamPollInteractor], the widget affected by this theme. class StreamPollInteractorTheme extends InheritedTheme { - /// Creates a [StreamPollInteractorTheme]. - /// - /// The [data] parameter must not be null. + /// Creates a poll interactor theme that controls descendant widgets. const StreamPollInteractorTheme({ super.key, required this.data, required super.child, }); - /// The configuration of this theme. + /// The poll interactor theme data for descendant widgets. final StreamPollInteractorThemeData data; - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [StreamPollInteractorTheme] widget, then - /// [StreamChatThemeData.pollInteractorTheme] is used. + /// Returns the [StreamPollInteractorThemeData] merged from local and global + /// themes. /// - /// Typical usage is as follows: + /// Local values from the nearest [StreamPollInteractorTheme] ancestor take + /// precedence over global values from [StreamChatTheme.of]. /// - /// ```dart - /// StreamPollInteractorTheme theme = StreamPollInteractorTheme.of(context); - /// ``` + /// This allows partial overrides - for example, overriding only + /// [StreamPollInteractorThemeData.titleTextStyle] while inheriting other + /// properties from the global theme. static StreamPollInteractorThemeData of(BuildContext context) { - final pollInteractorTheme = - context.dependOnInheritedWidgetOfExactType(); - return pollInteractorTheme?.data ?? - StreamChatTheme.of(context).pollInteractorTheme; + final localTheme = context.dependOnInheritedWidgetOfExactType(); + return StreamChatTheme.of(context).pollInteractorTheme.merge(localTheme?.data); } @override - Widget wrap(BuildContext context, Widget child) => - StreamPollInteractorTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamPollInteractorTheme(data: data, child: child); @override - bool updateShouldNotify(StreamPollInteractorTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamPollInteractorTheme oldWidget) => data != oldWidget.data; } -/// {@template streamPollInteractorThemeData} -/// A style that overrides the default appearance of [StreamPollInteractor] -/// widget when used with [StreamPollCreatorTheme] or with the overall -/// [StreamChatTheme]'s [StreamChatThemeData.pollInteractorTheme]. -/// {@endtemplate} -class StreamPollInteractorThemeData with Diagnosticable { - /// {@macro streamPollInteractorThemeData} +/// Theme data for customizing [StreamPollInteractor] widgets. +/// +/// {@tool snippet} +/// +/// Customize poll interactor appearance globally: +/// +/// ```dart +/// StreamChatThemeData( +/// pollInteractorTheme: StreamPollInteractorThemeData( +/// titleTextStyle: TextStyle(fontWeight: FontWeight.w700), +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamPollInteractor], the widget that uses this theme data. +/// * [StreamPollInteractorTheme], for overriding theme in a widget subtree. +@themeGen +@immutable +class StreamPollInteractorThemeData with _$StreamPollInteractorThemeData { + /// Creates poll interactor theme data with optional style overrides. const StreamPollInteractorThemeData({ - this.pollTitleStyle, - this.pollSubtitleStyle, - this.pollOptionTextStyle, - this.pollOptionVoteCountTextStyle, - this.pollOptionCheckboxShape, - this.pollOptionCheckboxCheckColor, - this.pollOptionCheckboxActiveColor, - this.pollOptionCheckboxBorderSide, - this.pollOptionVotesProgressBarMinHeight, - this.pollOptionVotesProgressBarTrackColor, - this.pollOptionVotesProgressBarValueColor, - this.pollOptionVotesProgressBarWinnerColor, - this.pollOptionVotesProgressBarBorderRadius, - this.pollActionButtonStyle, - this.pollActionDialogTitleStyle, - this.pollActionDialogTextFieldStyle, - this.pollActionDialogTextFieldFillColor, - this.pollActionDialogTextFieldBorderRadius, + this.titleTextStyle, + this.subtitleTextStyle, + this.primaryActionStyle, + this.secondaryActionStyle, + this.optionStyle, }); - /// The text style of the poll title. - final TextStyle? pollTitleStyle; - - /// The text style of the poll subtitle. - final TextStyle? pollSubtitleStyle; - - /// The text style of the poll option. - final TextStyle? pollOptionTextStyle; - - /// The text style of the poll option vote count. - final TextStyle? pollOptionVoteCountTextStyle; - - /// The shape of the poll option checkbox. - final OutlinedBorder? pollOptionCheckboxShape; - - /// The color used for the poll option checkbox check. - final Color? pollOptionCheckboxCheckColor; - - /// The color used for the checkbox when it's active. - final Color? pollOptionCheckboxActiveColor; - - /// The border configuration of the poll option checkbox. - final BorderSide? pollOptionCheckboxBorderSide; - - /// The minimum height of the poll option votes progress bar. - final double? pollOptionVotesProgressBarMinHeight; + /// The text style for the poll question title. + final TextStyle? titleTextStyle; - /// The track color of the poll option votes progress bar. - final Color? pollOptionVotesProgressBarTrackColor; + /// The text style for the poll description or status subtitle. + final TextStyle? subtitleTextStyle; - /// The color of the poll option votes progress bar value. - final Color? pollOptionVotesProgressBarValueColor; + /// The visual styling for the primary action button. + final StreamButtonThemeStyle? primaryActionStyle; - /// The color of the poll option votes progress bar value when it's the - /// winner. - final Color? pollOptionVotesProgressBarWinnerColor; + /// The visual styling for secondary action buttons. + final StreamButtonThemeStyle? secondaryActionStyle; - /// The border radius of the poll option votes progress bar. - final BorderRadius? pollOptionVotesProgressBarBorderRadius; + /// The visual styling for poll option rows. + final StreamPollOptionStyle? optionStyle; - /// The button style of the poll action buttons. - final ButtonStyle? pollActionButtonStyle; + /// Linearly interpolate between two [StreamPollInteractorThemeData] objects. + static StreamPollInteractorThemeData? lerp( + StreamPollInteractorThemeData? a, + StreamPollInteractorThemeData? b, + double t, + ) => _$StreamPollInteractorThemeData.lerp(a, b, t); +} - /// The text style of the poll action dialog title. - final TextStyle? pollActionDialogTitleStyle; +/// Visual styling properties for poll option rows. +/// +/// Defines the appearance of individual poll options including text styles, +/// checkbox, progress bar, and voter avatar stack. +/// +/// See also: +/// +/// * [StreamPollInteractorThemeData], which wraps this style for theming. +/// * [StreamPollInteractor], which uses this styling. +@themeGen +@immutable +class StreamPollOptionStyle with _$StreamPollOptionStyle { + /// Creates poll option style properties. + const StreamPollOptionStyle({ + this.textStyle, + this.votesTextStyle, + this.votesAvatarSize, + this.checkboxStyle, + this.progressBarStyle, + }); - /// The text style of the poll action dialog text field. - final TextStyle? pollActionDialogTextFieldStyle; + /// The text style for the option label. + /// + /// If null, defaults to [StreamTextTheme.captionDefault]. + final TextStyle? textStyle; - /// The fill color of the poll action dialog text field. - final Color? pollActionDialogTextFieldFillColor; + /// The text style for the vote count displayed alongside each option. + /// + /// If null, defaults to [StreamTextTheme.metadataDefault]. + final TextStyle? votesTextStyle; - /// The border radius of the poll action dialog text field. - final BorderRadius? pollActionDialogTextFieldBorderRadius; + /// The size of the voter avatar stack shown alongside each option. + /// + /// Only visible when the poll has public voting visibility. + /// If null, defaults to [StreamAvatarStackSize.xs]. + final StreamAvatarStackSize? votesAvatarSize; - /// Copies this [StreamPollInteractorThemeData] with some new values. - StreamPollInteractorThemeData copyWith({ - TextStyle? pollTitleStyle, - TextStyle? pollSubtitleStyle, - TextStyle? pollOptionTextStyle, - TextStyle? pollOptionVoteCountTextStyle, - OutlinedBorder? pollOptionCheckboxShape, - Color? pollOptionCheckboxCheckColor, - Color? pollOptionCheckboxActiveColor, - BorderSide? pollOptionCheckboxBorderSide, - double? pollOptionVotesProgressBarMinHeight, - Color? pollOptionVotesProgressBarTrackColor, - Color? pollOptionVotesProgressBarValueColor, - Color? pollOptionVotesProgressBarWinnerColor, - BorderRadius? pollOptionVotesProgressBarBorderRadius, - ButtonStyle? pollActionButtonStyle, - TextStyle? pollActionDialogTitleStyle, - TextStyle? pollActionDialogTextFieldStyle, - Color? pollActionDialogTextFieldFillColor, - BorderRadius? pollActionDialogTextFieldBorderRadius, - }) { - return StreamPollInteractorThemeData( - pollTitleStyle: pollTitleStyle ?? this.pollTitleStyle, - pollSubtitleStyle: pollSubtitleStyle ?? this.pollSubtitleStyle, - pollOptionTextStyle: pollOptionTextStyle ?? this.pollOptionTextStyle, - pollOptionVoteCountTextStyle: - pollOptionVoteCountTextStyle ?? this.pollOptionVoteCountTextStyle, - pollOptionCheckboxShape: - pollOptionCheckboxShape ?? this.pollOptionCheckboxShape, - pollOptionCheckboxCheckColor: - pollOptionCheckboxCheckColor ?? this.pollOptionCheckboxCheckColor, - pollOptionCheckboxActiveColor: - pollOptionCheckboxActiveColor ?? this.pollOptionCheckboxActiveColor, - pollOptionCheckboxBorderSide: - pollOptionCheckboxBorderSide ?? this.pollOptionCheckboxBorderSide, - pollOptionVotesProgressBarMinHeight: - pollOptionVotesProgressBarMinHeight ?? - this.pollOptionVotesProgressBarMinHeight, - pollOptionVotesProgressBarTrackColor: - pollOptionVotesProgressBarTrackColor ?? - this.pollOptionVotesProgressBarTrackColor, - pollOptionVotesProgressBarValueColor: - pollOptionVotesProgressBarValueColor ?? - this.pollOptionVotesProgressBarValueColor, - pollOptionVotesProgressBarWinnerColor: - pollOptionVotesProgressBarWinnerColor ?? - this.pollOptionVotesProgressBarWinnerColor, - pollOptionVotesProgressBarBorderRadius: - pollOptionVotesProgressBarBorderRadius ?? - this.pollOptionVotesProgressBarBorderRadius, - pollActionButtonStyle: - pollActionButtonStyle ?? this.pollActionButtonStyle, - pollActionDialogTitleStyle: - pollActionDialogTitleStyle ?? this.pollActionDialogTitleStyle, - pollActionDialogTextFieldStyle: - pollActionDialogTextFieldStyle ?? this.pollActionDialogTextFieldStyle, - pollActionDialogTextFieldFillColor: pollActionDialogTextFieldFillColor ?? - this.pollActionDialogTextFieldFillColor, - pollActionDialogTextFieldBorderRadius: - pollActionDialogTextFieldBorderRadius ?? - this.pollActionDialogTextFieldBorderRadius, - ); - } + /// The visual styling for the option selection checkbox. + /// + /// If null, defaults to a circular checkbox with [StreamCheckboxSize.md]. + final StreamCheckboxStyle? checkboxStyle; - /// Merges [this] [StreamPollInteractorThemeData] with the [other] - StreamPollInteractorThemeData merge(StreamPollInteractorThemeData? other) { - if (other == null) return this; - return copyWith( - pollTitleStyle: other.pollTitleStyle ?? pollTitleStyle, - pollSubtitleStyle: other.pollSubtitleStyle ?? pollSubtitleStyle, - pollOptionTextStyle: other.pollOptionTextStyle ?? pollOptionTextStyle, - pollOptionVoteCountTextStyle: - other.pollOptionVoteCountTextStyle ?? pollOptionVoteCountTextStyle, - pollOptionCheckboxShape: - other.pollOptionCheckboxShape ?? pollOptionCheckboxShape, - pollOptionCheckboxCheckColor: - other.pollOptionCheckboxCheckColor ?? pollOptionCheckboxCheckColor, - pollOptionCheckboxActiveColor: - other.pollOptionCheckboxActiveColor ?? pollOptionCheckboxActiveColor, - pollOptionCheckboxBorderSide: - other.pollOptionCheckboxBorderSide ?? pollOptionCheckboxBorderSide, - pollOptionVotesProgressBarMinHeight: - other.pollOptionVotesProgressBarMinHeight ?? - pollOptionVotesProgressBarMinHeight, - pollOptionVotesProgressBarTrackColor: - other.pollOptionVotesProgressBarTrackColor ?? - pollOptionVotesProgressBarTrackColor, - pollOptionVotesProgressBarValueColor: - other.pollOptionVotesProgressBarValueColor ?? - pollOptionVotesProgressBarValueColor, - pollOptionVotesProgressBarWinnerColor: - other.pollOptionVotesProgressBarWinnerColor ?? - pollOptionVotesProgressBarWinnerColor, - pollOptionVotesProgressBarBorderRadius: - other.pollOptionVotesProgressBarBorderRadius ?? - pollOptionVotesProgressBarBorderRadius, - pollActionButtonStyle: - other.pollActionButtonStyle ?? pollActionButtonStyle, - pollActionDialogTitleStyle: - other.pollActionDialogTitleStyle ?? pollActionDialogTitleStyle, - pollActionDialogTextFieldStyle: other.pollActionDialogTextFieldStyle ?? - pollActionDialogTextFieldStyle, - pollActionDialogTextFieldFillColor: - other.pollActionDialogTextFieldFillColor ?? - pollActionDialogTextFieldFillColor, - pollActionDialogTextFieldBorderRadius: - other.pollActionDialogTextFieldBorderRadius ?? - pollActionDialogTextFieldBorderRadius, - ); - } + /// The visual styling for the vote distribution progress bar. + /// + /// If null, defaults to a progress bar with accent neutral fill. + final StreamProgressBarStyle? progressBarStyle; - /// Linearly interpolate between two [StreamPollInteractorThemeData]. - StreamPollInteractorThemeData lerp( - StreamPollInteractorThemeData a, - StreamPollInteractorThemeData b, + /// Linearly interpolate between two [StreamPollOptionStyle] objects. + static StreamPollOptionStyle? lerp( + StreamPollOptionStyle? a, + StreamPollOptionStyle? b, double t, - ) { - return StreamPollInteractorThemeData( - pollTitleStyle: TextStyle.lerp(a.pollTitleStyle, b.pollTitleStyle, t), - pollSubtitleStyle: - TextStyle.lerp(a.pollSubtitleStyle, b.pollSubtitleStyle, t), - pollOptionTextStyle: - TextStyle.lerp(a.pollOptionTextStyle, b.pollOptionTextStyle, t), - pollOptionVoteCountTextStyle: TextStyle.lerp( - a.pollOptionVoteCountTextStyle, b.pollOptionVoteCountTextStyle, t), - pollOptionCheckboxShape: OutlinedBorder.lerp( - a.pollOptionCheckboxShape, b.pollOptionCheckboxShape, t), - pollOptionCheckboxCheckColor: Color.lerp( - a.pollOptionCheckboxCheckColor, b.pollOptionCheckboxCheckColor, t), - pollOptionCheckboxActiveColor: Color.lerp( - a.pollOptionCheckboxActiveColor, b.pollOptionCheckboxActiveColor, t), - pollOptionCheckboxBorderSide: _lerpSides( - a.pollOptionCheckboxBorderSide, b.pollOptionCheckboxBorderSide, t), - pollOptionVotesProgressBarMinHeight: lerpDouble( - a.pollOptionVotesProgressBarMinHeight, - b.pollOptionVotesProgressBarMinHeight, - t), - pollOptionVotesProgressBarTrackColor: Color.lerp( - a.pollOptionVotesProgressBarTrackColor, - b.pollOptionVotesProgressBarTrackColor, - t), - pollOptionVotesProgressBarValueColor: Color.lerp( - a.pollOptionVotesProgressBarValueColor, - b.pollOptionVotesProgressBarValueColor, - t), - pollOptionVotesProgressBarWinnerColor: Color.lerp( - a.pollOptionVotesProgressBarWinnerColor, - b.pollOptionVotesProgressBarWinnerColor, - t), - pollOptionVotesProgressBarBorderRadius: BorderRadius.lerp( - a.pollOptionVotesProgressBarBorderRadius, - b.pollOptionVotesProgressBarBorderRadius, - t), - pollActionButtonStyle: - ButtonStyle.lerp(a.pollActionButtonStyle, b.pollActionButtonStyle, t), - pollActionDialogTitleStyle: TextStyle.lerp( - a.pollActionDialogTitleStyle, b.pollActionDialogTitleStyle, t), - pollActionDialogTextFieldStyle: TextStyle.lerp( - a.pollActionDialogTextFieldStyle, - b.pollActionDialogTextFieldStyle, - t), - pollActionDialogTextFieldFillColor: Color.lerp( - a.pollActionDialogTextFieldFillColor, - b.pollActionDialogTextFieldFillColor, - t), - pollActionDialogTextFieldBorderRadius: BorderRadius.lerp( - a.pollActionDialogTextFieldBorderRadius, - b.pollActionDialogTextFieldBorderRadius, - t), - ); - } - - // Special case because BorderSide.lerp() doesn't support null arguments - static BorderSide? _lerpSides(BorderSide? a, BorderSide? b, double t) { - if (a == null || b == null) return null; - if (identical(a, b)) return a; - - if (a is WidgetStateBorderSide) { - a = a.resolve({}); - } - if (b is WidgetStateBorderSide) { - b = b.resolve({}); - } - - return BorderSide.lerp(a!, b!, t); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is StreamPollInteractorThemeData && - other.pollTitleStyle == pollTitleStyle && - other.pollSubtitleStyle == pollSubtitleStyle && - other.pollOptionTextStyle == pollOptionTextStyle && - other.pollOptionVoteCountTextStyle == pollOptionVoteCountTextStyle && - other.pollOptionCheckboxShape == pollOptionCheckboxShape && - other.pollOptionCheckboxCheckColor == pollOptionCheckboxCheckColor && - other.pollOptionCheckboxActiveColor == - pollOptionCheckboxActiveColor && - other.pollOptionCheckboxBorderSide == pollOptionCheckboxBorderSide && - other.pollOptionVotesProgressBarMinHeight == - pollOptionVotesProgressBarMinHeight && - other.pollOptionVotesProgressBarTrackColor == - pollOptionVotesProgressBarTrackColor && - other.pollOptionVotesProgressBarValueColor == - pollOptionVotesProgressBarValueColor && - other.pollOptionVotesProgressBarWinnerColor == - pollOptionVotesProgressBarWinnerColor && - other.pollOptionVotesProgressBarBorderRadius == - pollOptionVotesProgressBarBorderRadius && - other.pollActionButtonStyle == pollActionButtonStyle && - other.pollActionDialogTitleStyle == pollActionDialogTitleStyle && - other.pollActionDialogTextFieldStyle == - pollActionDialogTextFieldStyle && - other.pollActionDialogTextFieldFillColor == - pollActionDialogTextFieldFillColor && - other.pollActionDialogTextFieldBorderRadius == - pollActionDialogTextFieldBorderRadius; - - @override - int get hashCode => - pollTitleStyle.hashCode ^ - pollSubtitleStyle.hashCode ^ - pollOptionTextStyle.hashCode ^ - pollOptionVoteCountTextStyle.hashCode ^ - pollOptionCheckboxShape.hashCode ^ - pollOptionCheckboxCheckColor.hashCode ^ - pollOptionCheckboxActiveColor.hashCode ^ - pollOptionCheckboxBorderSide.hashCode ^ - pollOptionVotesProgressBarMinHeight.hashCode ^ - pollOptionVotesProgressBarTrackColor.hashCode ^ - pollOptionVotesProgressBarValueColor.hashCode ^ - pollOptionVotesProgressBarWinnerColor.hashCode ^ - pollOptionVotesProgressBarBorderRadius.hashCode ^ - pollActionButtonStyle.hashCode ^ - pollActionDialogTitleStyle.hashCode ^ - pollActionDialogTextFieldStyle.hashCode ^ - pollActionDialogTextFieldFillColor.hashCode ^ - pollActionDialogTextFieldBorderRadius.hashCode; + ) => _$StreamPollOptionStyle.lerp(a, b, t); } diff --git a/packages/stream_chat_flutter/lib/src/theme/poll_interactor_theme.g.theme.dart b/packages/stream_chat_flutter/lib/src/theme/poll_interactor_theme.g.theme.dart new file mode 100644 index 0000000000..b0f9ff4da6 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/poll_interactor_theme.g.theme.dart @@ -0,0 +1,249 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'poll_interactor_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamPollInteractorThemeData { + bool get canMerge => true; + + static StreamPollInteractorThemeData? lerp( + StreamPollInteractorThemeData? a, + StreamPollInteractorThemeData? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamPollInteractorThemeData( + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + subtitleTextStyle: TextStyle.lerp( + a.subtitleTextStyle, + b.subtitleTextStyle, + t, + ), + primaryActionStyle: StreamButtonThemeStyle.lerp( + a.primaryActionStyle, + b.primaryActionStyle, + t, + ), + secondaryActionStyle: StreamButtonThemeStyle.lerp( + a.secondaryActionStyle, + b.secondaryActionStyle, + t, + ), + optionStyle: StreamPollOptionStyle.lerp(a.optionStyle, b.optionStyle, t), + ); + } + + StreamPollInteractorThemeData copyWith({ + TextStyle? titleTextStyle, + TextStyle? subtitleTextStyle, + StreamButtonThemeStyle? primaryActionStyle, + StreamButtonThemeStyle? secondaryActionStyle, + StreamPollOptionStyle? optionStyle, + }) { + final _this = (this as StreamPollInteractorThemeData); + + return StreamPollInteractorThemeData( + titleTextStyle: titleTextStyle ?? _this.titleTextStyle, + subtitleTextStyle: subtitleTextStyle ?? _this.subtitleTextStyle, + primaryActionStyle: primaryActionStyle ?? _this.primaryActionStyle, + secondaryActionStyle: secondaryActionStyle ?? _this.secondaryActionStyle, + optionStyle: optionStyle ?? _this.optionStyle, + ); + } + + StreamPollInteractorThemeData merge(StreamPollInteractorThemeData? other) { + final _this = (this as StreamPollInteractorThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + titleTextStyle: + _this.titleTextStyle?.merge(other.titleTextStyle) ?? + other.titleTextStyle, + subtitleTextStyle: + _this.subtitleTextStyle?.merge(other.subtitleTextStyle) ?? + other.subtitleTextStyle, + primaryActionStyle: + _this.primaryActionStyle?.merge(other.primaryActionStyle) ?? + other.primaryActionStyle, + secondaryActionStyle: + _this.secondaryActionStyle?.merge(other.secondaryActionStyle) ?? + other.secondaryActionStyle, + optionStyle: + _this.optionStyle?.merge(other.optionStyle) ?? other.optionStyle, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamPollInteractorThemeData); + final _other = (other as StreamPollInteractorThemeData); + + return _other.titleTextStyle == _this.titleTextStyle && + _other.subtitleTextStyle == _this.subtitleTextStyle && + _other.primaryActionStyle == _this.primaryActionStyle && + _other.secondaryActionStyle == _this.secondaryActionStyle && + _other.optionStyle == _this.optionStyle; + } + + @override + int get hashCode { + final _this = (this as StreamPollInteractorThemeData); + + return Object.hash( + runtimeType, + _this.titleTextStyle, + _this.subtitleTextStyle, + _this.primaryActionStyle, + _this.secondaryActionStyle, + _this.optionStyle, + ); + } +} + +mixin _$StreamPollOptionStyle { + bool get canMerge => true; + + static StreamPollOptionStyle? lerp( + StreamPollOptionStyle? a, + StreamPollOptionStyle? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamPollOptionStyle( + textStyle: TextStyle.lerp(a.textStyle, b.textStyle, t), + votesTextStyle: TextStyle.lerp(a.votesTextStyle, b.votesTextStyle, t), + votesAvatarSize: t < 0.5 ? a.votesAvatarSize : b.votesAvatarSize, + checkboxStyle: StreamCheckboxStyle.lerp( + a.checkboxStyle, + b.checkboxStyle, + t, + ), + progressBarStyle: StreamProgressBarStyle.lerp( + a.progressBarStyle, + b.progressBarStyle, + t, + ), + ); + } + + StreamPollOptionStyle copyWith({ + TextStyle? textStyle, + TextStyle? votesTextStyle, + StreamAvatarStackSize? votesAvatarSize, + StreamCheckboxStyle? checkboxStyle, + StreamProgressBarStyle? progressBarStyle, + }) { + final _this = (this as StreamPollOptionStyle); + + return StreamPollOptionStyle( + textStyle: textStyle ?? _this.textStyle, + votesTextStyle: votesTextStyle ?? _this.votesTextStyle, + votesAvatarSize: votesAvatarSize ?? _this.votesAvatarSize, + checkboxStyle: checkboxStyle ?? _this.checkboxStyle, + progressBarStyle: progressBarStyle ?? _this.progressBarStyle, + ); + } + + StreamPollOptionStyle merge(StreamPollOptionStyle? other) { + final _this = (this as StreamPollOptionStyle); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + textStyle: _this.textStyle?.merge(other.textStyle) ?? other.textStyle, + votesTextStyle: + _this.votesTextStyle?.merge(other.votesTextStyle) ?? + other.votesTextStyle, + votesAvatarSize: other.votesAvatarSize, + checkboxStyle: + _this.checkboxStyle?.merge(other.checkboxStyle) ?? + other.checkboxStyle, + progressBarStyle: + _this.progressBarStyle?.merge(other.progressBarStyle) ?? + other.progressBarStyle, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamPollOptionStyle); + final _other = (other as StreamPollOptionStyle); + + return _other.textStyle == _this.textStyle && + _other.votesTextStyle == _this.votesTextStyle && + _other.votesAvatarSize == _this.votesAvatarSize && + _other.checkboxStyle == _this.checkboxStyle && + _other.progressBarStyle == _this.progressBarStyle; + } + + @override + int get hashCode { + final _this = (this as StreamPollOptionStyle); + + return Object.hash( + runtimeType, + _this.textStyle, + _this.votesTextStyle, + _this.votesAvatarSize, + _this.checkboxStyle, + _this.progressBarStyle, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/poll_option_votes_dialog_theme.dart b/packages/stream_chat_flutter/lib/src/theme/poll_option_votes_dialog_theme.dart index 72ee25dc4d..e3b5307426 100644 --- a/packages/stream_chat_flutter/lib/src/theme/poll_option_votes_dialog_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/poll_option_votes_dialog_theme.dart @@ -30,19 +30,15 @@ class StreamPollOptionVotesDialogTheme extends InheritedTheme { /// If there is no enclosing [StreamPollOptionVotesDialogTheme] widget, then /// [StreamChatThemeData.pollOptionVotesDialogTheme] is used. static StreamPollOptionVotesDialogThemeData of(BuildContext context) { - final pollCommentsDialogTheme = context - .dependOnInheritedWidgetOfExactType(); - return pollCommentsDialogTheme?.data ?? - StreamChatTheme.of(context).pollOptionVotesDialogTheme; + final pollCommentsDialogTheme = context.dependOnInheritedWidgetOfExactType(); + return pollCommentsDialogTheme?.data ?? StreamChatTheme.of(context).pollOptionVotesDialogTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamPollOptionVotesDialogTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamPollOptionVotesDialogTheme(data: data, child: child); @override - bool updateShouldNotify(StreamPollOptionVotesDialogTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamPollOptionVotesDialogTheme oldWidget) => data != oldWidget.data; } /// {@template streamPollOptionVotesDialogThemeData} @@ -103,25 +99,17 @@ class StreamPollOptionVotesDialogThemeData with Diagnosticable { TextStyle? pollOptionWinnerVoteCountTextStyle, Color? pollOptionVoteItemBackgroundColor, BorderRadius? pollOptionVoteItemBorderRadius, - }) => - StreamPollOptionVotesDialogThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - appBarElevation: appBarElevation ?? this.appBarElevation, - appBarBackgroundColor: - appBarBackgroundColor ?? this.appBarBackgroundColor, - appBarForegroundColor: - appBarForegroundColor ?? this.appBarForegroundColor, - appBarTitleTextStyle: appBarTitleTextStyle ?? this.appBarTitleTextStyle, - pollOptionVoteCountTextStyle: - pollOptionVoteCountTextStyle ?? this.pollOptionVoteCountTextStyle, - pollOptionWinnerVoteCountTextStyle: - pollOptionWinnerVoteCountTextStyle ?? - this.pollOptionWinnerVoteCountTextStyle, - pollOptionVoteItemBackgroundColor: pollOptionVoteItemBackgroundColor ?? - this.pollOptionVoteItemBackgroundColor, - pollOptionVoteItemBorderRadius: pollOptionVoteItemBorderRadius ?? - this.pollOptionVoteItemBorderRadius, - ); + }) => StreamPollOptionVotesDialogThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + appBarElevation: appBarElevation ?? this.appBarElevation, + appBarBackgroundColor: appBarBackgroundColor ?? this.appBarBackgroundColor, + appBarForegroundColor: appBarForegroundColor ?? this.appBarForegroundColor, + appBarTitleTextStyle: appBarTitleTextStyle ?? this.appBarTitleTextStyle, + pollOptionVoteCountTextStyle: pollOptionVoteCountTextStyle ?? this.pollOptionVoteCountTextStyle, + pollOptionWinnerVoteCountTextStyle: pollOptionWinnerVoteCountTextStyle ?? this.pollOptionWinnerVoteCountTextStyle, + pollOptionVoteItemBackgroundColor: pollOptionVoteItemBackgroundColor ?? this.pollOptionVoteItemBackgroundColor, + pollOptionVoteItemBorderRadius: pollOptionVoteItemBorderRadius ?? this.pollOptionVoteItemBorderRadius, + ); /// Merges this [StreamPollOptionVotesDialogThemeData] with the [other]. StreamPollOptionVotesDialogThemeData merge( @@ -135,10 +123,8 @@ class StreamPollOptionVotesDialogThemeData with Diagnosticable { appBarForegroundColor: other.appBarForegroundColor, appBarTitleTextStyle: other.appBarTitleTextStyle, pollOptionVoteCountTextStyle: other.pollOptionVoteCountTextStyle, - pollOptionWinnerVoteCountTextStyle: - other.pollOptionWinnerVoteCountTextStyle, - pollOptionVoteItemBackgroundColor: - other.pollOptionVoteItemBackgroundColor, + pollOptionWinnerVoteCountTextStyle: other.pollOptionWinnerVoteCountTextStyle, + pollOptionVoteItemBackgroundColor: other.pollOptionVoteItemBackgroundColor, pollOptionVoteItemBorderRadius: other.pollOptionVoteItemBorderRadius, ); } @@ -152,12 +138,9 @@ class StreamPollOptionVotesDialogThemeData with Diagnosticable { return StreamPollOptionVotesDialogThemeData( backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), appBarElevation: lerpDouble(a?.appBarElevation, b?.appBarElevation, t), - appBarBackgroundColor: - Color.lerp(a?.appBarBackgroundColor, b?.appBarBackgroundColor, t), - appBarForegroundColor: - Color.lerp(a?.appBarForegroundColor, b?.appBarForegroundColor, t), - appBarTitleTextStyle: - TextStyle.lerp(a?.appBarTitleTextStyle, b?.appBarTitleTextStyle, t), + appBarBackgroundColor: Color.lerp(a?.appBarBackgroundColor, b?.appBarBackgroundColor, t), + appBarForegroundColor: Color.lerp(a?.appBarForegroundColor, b?.appBarForegroundColor, t), + appBarTitleTextStyle: TextStyle.lerp(a?.appBarTitleTextStyle, b?.appBarTitleTextStyle, t), pollOptionVoteCountTextStyle: TextStyle.lerp( a?.pollOptionVoteCountTextStyle, b?.pollOptionVoteCountTextStyle, @@ -173,11 +156,13 @@ class StreamPollOptionVotesDialogThemeData with Diagnosticable { b?.pollOptionVoteItemBackgroundColor, t, ), - pollOptionVoteItemBorderRadius: BorderRadiusGeometry.lerp( - a?.pollOptionVoteItemBorderRadius, - b?.pollOptionVoteItemBorderRadius, - t, - ) as BorderRadius?, + pollOptionVoteItemBorderRadius: + BorderRadiusGeometry.lerp( + a?.pollOptionVoteItemBorderRadius, + b?.pollOptionVoteItemBorderRadius, + t, + ) + as BorderRadius?, ); } @@ -191,12 +176,9 @@ class StreamPollOptionVotesDialogThemeData with Diagnosticable { other.appBarForegroundColor == appBarForegroundColor && other.appBarTitleTextStyle == appBarTitleTextStyle && other.pollOptionVoteCountTextStyle == pollOptionVoteCountTextStyle && - other.pollOptionWinnerVoteCountTextStyle == - pollOptionWinnerVoteCountTextStyle && - other.pollOptionVoteItemBackgroundColor == - pollOptionVoteItemBackgroundColor && - other.pollOptionVoteItemBorderRadius == - pollOptionVoteItemBorderRadius; + other.pollOptionWinnerVoteCountTextStyle == pollOptionWinnerVoteCountTextStyle && + other.pollOptionVoteItemBackgroundColor == pollOptionVoteItemBackgroundColor && + other.pollOptionVoteItemBorderRadius == pollOptionVoteItemBorderRadius; @override int get hashCode => diff --git a/packages/stream_chat_flutter/lib/src/theme/poll_options_dialog_theme.dart b/packages/stream_chat_flutter/lib/src/theme/poll_options_dialog_theme.dart index 8d2e07c8a8..cdf780e512 100644 --- a/packages/stream_chat_flutter/lib/src/theme/poll_options_dialog_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/poll_options_dialog_theme.dart @@ -30,19 +30,15 @@ class StreamPollOptionsDialogTheme extends InheritedTheme { /// If there is no enclosing [StreamPollOptionsDialogTheme] widget, then /// [StreamChatThemeData.pollOptionsDialogTheme] is used. static StreamPollOptionsDialogThemeData of(BuildContext context) { - final pollOptionsDialogTheme = context - .dependOnInheritedWidgetOfExactType(); - return pollOptionsDialogTheme?.data ?? - StreamChatTheme.of(context).pollOptionsDialogTheme; + final pollOptionsDialogTheme = context.dependOnInheritedWidgetOfExactType(); + return pollOptionsDialogTheme?.data ?? StreamChatTheme.of(context).pollOptionsDialogTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamPollOptionsDialogTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamPollOptionsDialogTheme(data: data, child: child); @override - bool updateShouldNotify(StreamPollOptionsDialogTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamPollOptionsDialogTheme oldWidget) => data != oldWidget.data; } /// {@template streamPollOptionsDialogThemeData} @@ -98,20 +94,16 @@ class StreamPollOptionsDialogThemeData with Diagnosticable { TextStyle? pollTitleTextStyle, Decoration? pollTitleDecoration, Decoration? pollOptionsListViewDecoration, - }) => - StreamPollOptionsDialogThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - appBarElevation: appBarElevation ?? this.appBarElevation, - appBarBackgroundColor: - appBarBackgroundColor ?? this.appBarBackgroundColor, - appBarForegroundColor: - appBarForegroundColor ?? this.appBarForegroundColor, - appBarTitleTextStyle: appBarTitleTextStyle ?? this.appBarTitleTextStyle, - pollTitleTextStyle: pollTitleTextStyle ?? this.pollTitleTextStyle, - pollTitleDecoration: pollTitleDecoration ?? this.pollTitleDecoration, - pollOptionsListViewDecoration: - pollOptionsListViewDecoration ?? this.pollOptionsListViewDecoration, - ); + }) => StreamPollOptionsDialogThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + appBarElevation: appBarElevation ?? this.appBarElevation, + appBarBackgroundColor: appBarBackgroundColor ?? this.appBarBackgroundColor, + appBarForegroundColor: appBarForegroundColor ?? this.appBarForegroundColor, + appBarTitleTextStyle: appBarTitleTextStyle ?? this.appBarTitleTextStyle, + pollTitleTextStyle: pollTitleTextStyle ?? this.pollTitleTextStyle, + pollTitleDecoration: pollTitleDecoration ?? this.pollTitleDecoration, + pollOptionsListViewDecoration: pollOptionsListViewDecoration ?? this.pollOptionsListViewDecoration, + ); /// Merges this [StreamPollOptionsDialogThemeData] with the [other]. StreamPollOptionsDialogThemeData merge( @@ -135,26 +127,20 @@ class StreamPollOptionsDialogThemeData with Diagnosticable { StreamPollOptionsDialogThemeData a, StreamPollOptionsDialogThemeData b, double t, - ) => - StreamPollOptionsDialogThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - appBarElevation: lerpDouble(a.appBarElevation, b.appBarElevation, t), - appBarBackgroundColor: - Color.lerp(a.appBarBackgroundColor, b.appBarBackgroundColor, t), - appBarForegroundColor: - Color.lerp(a.appBarForegroundColor, b.appBarForegroundColor, t), - appBarTitleTextStyle: - TextStyle.lerp(a.appBarTitleTextStyle, b.appBarTitleTextStyle, t), - pollTitleTextStyle: - TextStyle.lerp(a.pollTitleTextStyle, b.pollTitleTextStyle, t), - pollTitleDecoration: - Decoration.lerp(a.pollTitleDecoration, b.pollTitleDecoration, t), - pollOptionsListViewDecoration: Decoration.lerp( - a.pollOptionsListViewDecoration, - b.pollOptionsListViewDecoration, - t, - ), - ); + ) => StreamPollOptionsDialogThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + appBarElevation: lerpDouble(a.appBarElevation, b.appBarElevation, t), + appBarBackgroundColor: Color.lerp(a.appBarBackgroundColor, b.appBarBackgroundColor, t), + appBarForegroundColor: Color.lerp(a.appBarForegroundColor, b.appBarForegroundColor, t), + appBarTitleTextStyle: TextStyle.lerp(a.appBarTitleTextStyle, b.appBarTitleTextStyle, t), + pollTitleTextStyle: TextStyle.lerp(a.pollTitleTextStyle, b.pollTitleTextStyle, t), + pollTitleDecoration: Decoration.lerp(a.pollTitleDecoration, b.pollTitleDecoration, t), + pollOptionsListViewDecoration: Decoration.lerp( + a.pollOptionsListViewDecoration, + b.pollOptionsListViewDecoration, + t, + ), + ); @override bool operator ==(Object other) => diff --git a/packages/stream_chat_flutter/lib/src/theme/poll_results_dialog_theme.dart b/packages/stream_chat_flutter/lib/src/theme/poll_results_dialog_theme.dart index bb7d1550a5..7b2dd580dd 100644 --- a/packages/stream_chat_flutter/lib/src/theme/poll_results_dialog_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/poll_results_dialog_theme.dart @@ -36,19 +36,15 @@ class StreamPollResultsDialogTheme extends InheritedTheme { /// StreamPollCreatorTheme theme = StreamPollCreatorTheme.of(context); /// ``` static StreamPollResultsDialogThemeData of(BuildContext context) { - final pollResultsDialogTheme = context - .dependOnInheritedWidgetOfExactType(); - return pollResultsDialogTheme?.data ?? - StreamChatTheme.of(context).pollResultsDialogTheme; + final pollResultsDialogTheme = context.dependOnInheritedWidgetOfExactType(); + return pollResultsDialogTheme?.data ?? StreamChatTheme.of(context).pollResultsDialogTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamPollResultsDialogTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamPollResultsDialogTheme(data: data, child: child); @override - bool updateShouldNotify(StreamPollResultsDialogTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamPollResultsDialogTheme oldWidget) => data != oldWidget.data; } /// {@template streamPollCreatorThemeData} @@ -138,27 +134,19 @@ class StreamPollResultsDialogThemeData with Diagnosticable { return StreamPollResultsDialogThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, appBarElevation: appBarElevation ?? this.appBarElevation, - appBarBackgroundColor: - appBarBackgroundColor ?? this.appBarBackgroundColor, - appBarForegroundColor: - appBarForegroundColor ?? this.appBarForegroundColor, + appBarBackgroundColor: appBarBackgroundColor ?? this.appBarBackgroundColor, + appBarForegroundColor: appBarForegroundColor ?? this.appBarForegroundColor, appBarTitleTextStyle: appBarTitleTextStyle ?? this.appBarTitleTextStyle, pollTitleTextStyle: pollTitleTextStyle ?? this.pollTitleTextStyle, pollTitleDecoration: pollTitleDecoration ?? this.pollTitleDecoration, - pollOptionsDecoration: - pollOptionsDecoration ?? this.pollOptionsDecoration, - pollOptionsWinnerDecoration: - pollOptionsWinnerDecoration ?? this.pollOptionsWinnerDecoration, + pollOptionsDecoration: pollOptionsDecoration ?? this.pollOptionsDecoration, + pollOptionsWinnerDecoration: pollOptionsWinnerDecoration ?? this.pollOptionsWinnerDecoration, pollOptionsTextStyle: pollOptionsTextStyle ?? this.pollOptionsTextStyle, - pollOptionsWinnerTextStyle: - pollOptionsWinnerTextStyle ?? this.pollOptionsWinnerTextStyle, - pollOptionsVoteCountTextStyle: - pollOptionsVoteCountTextStyle ?? this.pollOptionsVoteCountTextStyle, + pollOptionsWinnerTextStyle: pollOptionsWinnerTextStyle ?? this.pollOptionsWinnerTextStyle, + pollOptionsVoteCountTextStyle: pollOptionsVoteCountTextStyle ?? this.pollOptionsVoteCountTextStyle, pollOptionsWinnerVoteCountTextStyle: - pollOptionsWinnerVoteCountTextStyle ?? - this.pollOptionsWinnerVoteCountTextStyle, - pollOptionsShowAllVotesButtonStyle: pollOptionsShowAllVotesButtonStyle ?? - this.pollOptionsShowAllVotesButtonStyle, + pollOptionsWinnerVoteCountTextStyle ?? this.pollOptionsWinnerVoteCountTextStyle, + pollOptionsShowAllVotesButtonStyle: pollOptionsShowAllVotesButtonStyle ?? this.pollOptionsShowAllVotesButtonStyle, ); } @@ -180,10 +168,8 @@ class StreamPollResultsDialogThemeData with Diagnosticable { pollOptionsTextStyle: other.pollOptionsTextStyle, pollOptionsWinnerTextStyle: other.pollOptionsWinnerTextStyle, pollOptionsVoteCountTextStyle: other.pollOptionsVoteCountTextStyle, - pollOptionsWinnerVoteCountTextStyle: - other.pollOptionsWinnerVoteCountTextStyle, - pollOptionsShowAllVotesButtonStyle: - other.pollOptionsShowAllVotesButtonStyle, + pollOptionsWinnerVoteCountTextStyle: other.pollOptionsWinnerVoteCountTextStyle, + pollOptionsShowAllVotesButtonStyle: other.pollOptionsShowAllVotesButtonStyle, ); } @@ -196,25 +182,18 @@ class StreamPollResultsDialogThemeData with Diagnosticable { return StreamPollResultsDialogThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), appBarElevation: lerpDouble(a.appBarElevation, b.appBarElevation, t), - appBarBackgroundColor: - Color.lerp(a.appBarBackgroundColor, b.appBarBackgroundColor, t), - appBarForegroundColor: - Color.lerp(a.appBarForegroundColor, b.appBarForegroundColor, t), - appBarTitleTextStyle: - TextStyle.lerp(a.appBarTitleTextStyle, b.appBarTitleTextStyle, t), - pollTitleTextStyle: - TextStyle.lerp(a.pollTitleTextStyle, b.pollTitleTextStyle, t), - pollTitleDecoration: - Decoration.lerp(a.pollTitleDecoration, b.pollTitleDecoration, t), - pollOptionsDecoration: - Decoration.lerp(a.pollOptionsDecoration, b.pollOptionsDecoration, t), + appBarBackgroundColor: Color.lerp(a.appBarBackgroundColor, b.appBarBackgroundColor, t), + appBarForegroundColor: Color.lerp(a.appBarForegroundColor, b.appBarForegroundColor, t), + appBarTitleTextStyle: TextStyle.lerp(a.appBarTitleTextStyle, b.appBarTitleTextStyle, t), + pollTitleTextStyle: TextStyle.lerp(a.pollTitleTextStyle, b.pollTitleTextStyle, t), + pollTitleDecoration: Decoration.lerp(a.pollTitleDecoration, b.pollTitleDecoration, t), + pollOptionsDecoration: Decoration.lerp(a.pollOptionsDecoration, b.pollOptionsDecoration, t), pollOptionsWinnerDecoration: Decoration.lerp( a.pollOptionsWinnerDecoration, b.pollOptionsWinnerDecoration, t, ), - pollOptionsTextStyle: - TextStyle.lerp(a.pollOptionsTextStyle, b.pollOptionsTextStyle, t), + pollOptionsTextStyle: TextStyle.lerp(a.pollOptionsTextStyle, b.pollOptionsTextStyle, t), pollOptionsWinnerTextStyle: TextStyle.lerp( a.pollOptionsWinnerTextStyle, b.pollOptionsWinnerTextStyle, @@ -253,12 +232,9 @@ class StreamPollResultsDialogThemeData with Diagnosticable { other.pollOptionsWinnerDecoration == pollOptionsWinnerDecoration && other.pollOptionsTextStyle == pollOptionsTextStyle && other.pollOptionsWinnerTextStyle == pollOptionsWinnerTextStyle && - other.pollOptionsVoteCountTextStyle == - pollOptionsVoteCountTextStyle && - other.pollOptionsWinnerVoteCountTextStyle == - pollOptionsWinnerVoteCountTextStyle && - other.pollOptionsShowAllVotesButtonStyle == - pollOptionsShowAllVotesButtonStyle; + other.pollOptionsVoteCountTextStyle == pollOptionsVoteCountTextStyle && + other.pollOptionsWinnerVoteCountTextStyle == pollOptionsWinnerVoteCountTextStyle && + other.pollOptionsShowAllVotesButtonStyle == pollOptionsShowAllVotesButtonStyle; @override int get hashCode => diff --git a/packages/stream_chat_flutter/lib/src/theme/stream_channel_list_item_theme.dart b/packages/stream_chat_flutter/lib/src/theme/stream_channel_list_item_theme.dart new file mode 100644 index 0000000000..d56174f744 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/stream_channel_list_item_theme.dart @@ -0,0 +1,144 @@ +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; + +part 'stream_channel_list_item_theme.g.theme.dart'; + +/// Applies a channel list item theme to descendant +/// [StreamChannelListItem] widgets. +/// +/// Wrap a subtree with [StreamChannelListItemTheme] to override styling. +/// Access the merged theme using [BuildContext.streamChannelListItemTheme]. +/// +/// {@tool snippet} +/// +/// Override channel list item colors for a specific section: +/// +/// ```dart +/// StreamChannelListItemTheme( +/// data: StreamChannelListItemThemeData( +/// backgroundColor: Colors.grey.shade50, +/// ), +/// child: StreamChannelListItem( +/// avatar: StreamAvatar(...), +/// title: 'General', +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamChannelListItemThemeData], which describes the theme. +/// * [StreamChannelListItem], the widget affected by this theme. +class StreamChannelListItemTheme extends InheritedTheme { + /// Creates a channel list item theme that controls descendant widgets. + const StreamChannelListItemTheme({ + super.key, + required this.data, + required super.child, + }); + + /// The channel list item theme data for descendant widgets. + final StreamChannelListItemThemeData data; + + /// Returns the [StreamChannelListItemThemeData] merged from local and + /// global themes. + /// + /// Local values from the nearest [StreamChannelListItemTheme] ancestor + /// take precedence over global values from [StreamTheme.of]. + static StreamChannelListItemThemeData of(BuildContext context) { + final localTheme = context.dependOnInheritedWidgetOfExactType(); + return StreamChatTheme.of(context).channelListItemTheme.merge(localTheme?.data); + } + + @override + Widget wrap(BuildContext context, Widget child) { + return StreamChannelListItemTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(StreamChannelListItemTheme oldWidget) => data != oldWidget.data; +} + +/// Theme data for customizing [StreamChannelListItem] widgets. +/// +/// {@tool snippet} +/// +/// Customize channel list item appearance globally: +/// +/// ```dart +/// StreamTheme( +/// channelListItemTheme: StreamChannelListItemThemeData( +/// backgroundColor: Colors.white, +/// borderColor: Colors.grey.shade200, +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamChannelListItem], the widget that uses this theme data. +/// * [StreamChannelListItemTheme], for overriding theme in a widget subtree. +@themeGen +@immutable +class StreamChannelListItemThemeData with _$StreamChannelListItemThemeData { + /// Creates a channel list item theme with optional style overrides. + const StreamChannelListItemThemeData({ + this.titleStyle, + this.subtitleStyle, + this.timestampStyle, + this.backgroundColor, + this.borderColor, + this.muteIconPosition, + }); + + /// The text style for the channel title. + /// + /// Falls back to [StreamTextTheme.headingSm] with [StreamColorScheme.textPrimary]. + final TextStyle? titleStyle; + + /// The text style for the message preview subtitle. + /// + /// Falls back to [StreamTextTheme.captionDefault] with [StreamColorScheme.textSecondary]. + final TextStyle? subtitleStyle; + + /// The text style for the timestamp. + /// + /// Falls back to [StreamTextTheme.captionDefault] with [StreamColorScheme.textTertiary]. + final TextStyle? timestampStyle; + + /// Defines the default background color of the tile. + /// + /// This color is resolved from [WidgetState]s. + final WidgetStateProperty? backgroundColor; + + /// The bottom border color of the list item. + /// + /// Falls back to [StreamColorScheme.borderSubtle]. + final Color? borderColor; + + /// The position of the mute icon. + /// + /// Falls back to [MuteIconPosition.title]. + final MuteIconPosition? muteIconPosition; + + /// Linearly interpolate between two [StreamChannelListItemThemeData] objects. + static StreamChannelListItemThemeData? lerp( + StreamChannelListItemThemeData? a, + StreamChannelListItemThemeData? b, + double t, + ) => _$StreamChannelListItemThemeData.lerp(a, b, t); +} + +/// The position of the mute icon. +/// By default the mute icon will be shown directly next to the title. +/// When choosing for subtitle, the mute icon will be shown at the end of the list item. +enum MuteIconPosition { + /// Top row of the list item, next to the title. + title, + + /// Bottom row, at the end of the list item. + subtitle, +} diff --git a/packages/stream_chat_flutter/lib/src/theme/stream_channel_list_item_theme.g.theme.dart b/packages/stream_chat_flutter/lib/src/theme/stream_channel_list_item_theme.g.theme.dart new file mode 100644 index 0000000000..bb1baafe13 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/stream_channel_list_item_theme.g.theme.dart @@ -0,0 +1,127 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'stream_channel_list_item_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamChannelListItemThemeData { + bool get canMerge => true; + + static StreamChannelListItemThemeData? lerp( + StreamChannelListItemThemeData? a, + StreamChannelListItemThemeData? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamChannelListItemThemeData( + titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), + subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), + timestampStyle: TextStyle.lerp(a.timestampStyle, b.timestampStyle, t), + backgroundColor: WidgetStateProperty.lerp( + a.backgroundColor, + b.backgroundColor, + t, + Color.lerp, + ), + borderColor: Color.lerp(a.borderColor, b.borderColor, t), + muteIconPosition: t < 0.5 ? a.muteIconPosition : b.muteIconPosition, + ); + } + + StreamChannelListItemThemeData copyWith({ + TextStyle? titleStyle, + TextStyle? subtitleStyle, + TextStyle? timestampStyle, + WidgetStateProperty? backgroundColor, + Color? borderColor, + MuteIconPosition? muteIconPosition, + }) { + final _this = (this as StreamChannelListItemThemeData); + + return StreamChannelListItemThemeData( + titleStyle: titleStyle ?? _this.titleStyle, + subtitleStyle: subtitleStyle ?? _this.subtitleStyle, + timestampStyle: timestampStyle ?? _this.timestampStyle, + backgroundColor: backgroundColor ?? _this.backgroundColor, + borderColor: borderColor ?? _this.borderColor, + muteIconPosition: muteIconPosition ?? _this.muteIconPosition, + ); + } + + StreamChannelListItemThemeData merge(StreamChannelListItemThemeData? other) { + final _this = (this as StreamChannelListItemThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + titleStyle: _this.titleStyle?.merge(other.titleStyle) ?? other.titleStyle, + subtitleStyle: + _this.subtitleStyle?.merge(other.subtitleStyle) ?? + other.subtitleStyle, + timestampStyle: + _this.timestampStyle?.merge(other.timestampStyle) ?? + other.timestampStyle, + backgroundColor: other.backgroundColor, + borderColor: other.borderColor, + muteIconPosition: other.muteIconPosition, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamChannelListItemThemeData); + final _other = (other as StreamChannelListItemThemeData); + + return _other.titleStyle == _this.titleStyle && + _other.subtitleStyle == _this.subtitleStyle && + _other.timestampStyle == _this.timestampStyle && + _other.backgroundColor == _this.backgroundColor && + _other.borderColor == _this.borderColor && + _other.muteIconPosition == _this.muteIconPosition; + } + + @override + int get hashCode { + final _this = (this as StreamChannelListItemThemeData); + + return Object.hash( + runtimeType, + _this.titleStyle, + _this.subtitleStyle, + _this.timestampStyle, + _this.backgroundColor, + _this.borderColor, + _this.muteIconPosition, + ); + } +} 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 3f9e42bf80..a769458286 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 @@ -1,7 +1,6 @@ // ignore_for_file: deprecated_member_use_from_same_package import 'package:flutter/material.dart' hide TextTheme; -import 'package:stream_chat_flutter/src/misc/audio_waveform.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// {@template streamChatTheme} @@ -23,8 +22,7 @@ class StreamChatTheme extends InheritedWidget { /// Use this method to get the current [StreamChatThemeData] instance static StreamChatThemeData of(BuildContext context) { - final streamChatTheme = - context.dependOnInheritedWidgetOfExactType(); + final streamChatTheme = context.dependOnInheritedWidgetOfExactType(); assert( streamChatTheme != null, @@ -53,14 +51,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, @@ -69,14 +62,12 @@ class StreamChatThemeData { StreamPollOptionVotesDialogThemeData? pollOptionVotesDialogTheme, StreamThreadListTileThemeData? threadListTileTheme, StreamDraftListTileThemeData? draftListTileTheme, - StreamAudioWaveformThemeData? audioWaveformTheme, - StreamAudioWaveformSliderThemeData? audioWaveformSliderTheme, StreamVoiceRecordingAttachmentThemeData? voiceRecordingAttachmentTheme, + StreamChannelListItemThemeData? channelListItemTheme, }) { 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 +84,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, @@ -106,21 +95,18 @@ class StreamChatThemeData { pollOptionVotesDialogTheme: pollOptionVotesDialogTheme, threadListTileTheme: threadListTileTheme, draftListTileTheme: draftListTileTheme, - audioWaveformTheme: audioWaveformTheme, - audioWaveformSliderTheme: audioWaveformSliderTheme, voiceRecordingAttachmentTheme: voiceRecordingAttachmentTheme, + channelListItemTheme: channelListItemTheme, ); return defaultData.merge(customizedData); } /// Theme initialized with light - factory StreamChatThemeData.light() => - StreamChatThemeData(brightness: Brightness.light); + factory StreamChatThemeData.light() => StreamChatThemeData(brightness: Brightness.light); /// Theme initialized with dark - factory StreamChatThemeData.dark() => - StreamChatThemeData(brightness: Brightness.dark); + factory StreamChatThemeData.dark() => StreamChatThemeData(brightness: Brightness.dark); /// Raw theme initialization const StreamChatThemeData.raw({ @@ -136,7 +122,6 @@ class StreamChatThemeData { required this.galleryHeaderTheme, required this.galleryFooterTheme, required this.messageListViewTheme, - required this.voiceRecordingTheme, required this.pollCreatorTheme, required this.pollInteractorTheme, required this.pollResultsDialogTheme, @@ -145,9 +130,8 @@ class StreamChatThemeData { required this.pollOptionVotesDialogTheme, required this.threadListTileTheme, required this.draftListTileTheme, - required this.audioWaveformTheme, - required this.audioWaveformSliderTheme, required this.voiceRecordingAttachmentTheme, + required this.channelListItemTheme, }); /// Creates a theme from a Material [Theme] @@ -178,10 +162,6 @@ class StreamChatThemeData { ), ), color: colorTheme.barsBg, - titleStyle: textTheme.headlineBold, - subtitleStyle: textTheme.footnote.copyWith( - color: const Color(0xff7A7A7A), - ), ); final channelPreviewTheme = StreamChannelPreviewThemeData( unreadCounterColor: colorTheme.accentError, @@ -203,20 +183,6 @@ class StreamChatThemeData { indicatorIconSize: 16, ); - final audioWaveformTheme = StreamAudioWaveformThemeData( - color: colorTheme.textLowEmphasis, - progressColor: colorTheme.accentPrimary, - minBarHeight: 2, - spacingRatio: 0.3, - heightScale: 1, - ); - - final audioWaveformSliderTheme = StreamAudioWaveformSliderThemeData( - audioWaveformTheme: audioWaveformTheme, - thumbColor: Colors.white, - thumbBorderColor: colorTheme.borders, - ); - return StreamChatThemeData.raw( textTheme: textTheme, colorTheme: colorTheme, @@ -235,18 +201,15 @@ class StreamChatThemeData { ), channelHeaderTheme: channelHeaderTheme, ownMessageTheme: StreamMessageThemeData( - messageAuthorStyle: - textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), + messageAuthorStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), messageTextStyle: textTheme.body, messageDeletedStyle: textTheme.body.copyWith( color: colorTheme.textLowEmphasis, fontStyle: FontStyle.italic, ), - createdAtStyle: - textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), + createdAtStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), repliesStyle: textTheme.footnoteBold.copyWith(color: accentColor), - messageBackgroundColor: colorTheme.borders, - messageBorderColor: colorTheme.borders, + messageBackgroundColor: colorTheme.inputBg, reactionsBackgroundColor: colorTheme.barsBg, reactionsBorderColor: colorTheme.borders, reactionsMaskColor: colorTheme.appBg, @@ -274,14 +237,11 @@ class StreamChatThemeData { color: colorTheme.textLowEmphasis, fontStyle: FontStyle.italic, ), - createdAtStyle: - textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), - messageAuthorStyle: - textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), + createdAtStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), + messageAuthorStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), repliesStyle: textTheme.footnoteBold.copyWith(color: accentColor), messageLinksStyle: TextStyle(color: accentColor), messageBackgroundColor: colorTheme.barsBg, - messageBorderColor: colorTheme.borders, avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -309,14 +269,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 +300,7 @@ class StreamChatThemeData { bottomSheetCloseIconColor: colorTheme.textHighEmphasis, ), messageListViewTheme: StreamMessageListViewThemeData( - backgroundColor: colorTheme.barsBg, + backgroundColor: colorTheme.appBg, ), pollCreatorTheme: StreamPollCreatorThemeData( backgroundColor: colorTheme.appBg, @@ -387,44 +347,7 @@ class StreamChatThemeData { color: colorTheme.textHighEmphasis, ), ), - pollInteractorTheme: StreamPollInteractorThemeData( - pollTitleStyle: textTheme.headlineBold.copyWith( - color: colorTheme.textHighEmphasis, - ), - pollSubtitleStyle: textTheme.footnote.copyWith( - color: colorTheme.textLowEmphasis, - ), - pollOptionTextStyle: textTheme.headline.copyWith( - color: colorTheme.textHighEmphasis, - ), - pollOptionVoteCountTextStyle: textTheme.footnote.copyWith( - color: colorTheme.textLowEmphasis, - ), - pollOptionCheckboxShape: const CircleBorder(), - pollOptionCheckboxCheckColor: Colors.white, - pollOptionCheckboxActiveColor: colorTheme.accentPrimary, - pollOptionCheckboxBorderSide: BorderSide( - width: 2, - color: colorTheme.disabled, - ), - pollOptionVotesProgressBarMinHeight: 4, - pollOptionVotesProgressBarTrackColor: colorTheme.disabled, - pollOptionVotesProgressBarValueColor: colorTheme.accentPrimary, - pollOptionVotesProgressBarWinnerColor: colorTheme.accentInfo, - pollOptionVotesProgressBarBorderRadius: BorderRadius.circular(4), - pollActionButtonStyle: TextButton.styleFrom( - textStyle: textTheme.headline, - foregroundColor: colorTheme.accentPrimary, - ), - pollActionDialogTitleStyle: textTheme.headlineBold.copyWith( - color: colorTheme.textHighEmphasis, - ), - pollActionDialogTextFieldStyle: textTheme.headline.copyWith( - color: colorTheme.textHighEmphasis, - ), - pollActionDialogTextFieldBorderRadius: BorderRadius.circular(12), - pollActionDialogTextFieldFillColor: colorTheme.inputBg, - ), + pollInteractorTheme: const StreamPollInteractorThemeData(), pollResultsDialogTheme: StreamPollResultsDialogThemeData( backgroundColor: colorTheme.appBg, appBarElevation: 1, @@ -525,30 +448,7 @@ class StreamChatThemeData { pollOptionVoteItemBackgroundColor: colorTheme.inputBg, pollOptionVoteItemBorderRadius: BorderRadius.circular(12), ), - threadListTileTheme: StreamThreadListTileThemeData( - backgroundColor: colorTheme.barsBg, - padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8), - threadUnreadMessageCountStyle: textTheme.footnoteBold.copyWith( - color: Colors.white, - ), - threadUnreadMessageCountBackgroundColor: - channelPreviewTheme.unreadCounterColor, - threadChannelNameStyle: textTheme.bodyBold.copyWith( - color: colorTheme.textHighEmphasis, - ), - threadReplyToMessageStyle: textTheme.footnote.copyWith( - color: colorTheme.textLowEmphasis, - ), - threadLatestReplyTimestampStyle: textTheme.footnote.copyWith( - color: colorTheme.textLowEmphasis, - ), - threadLatestReplyUsernameStyle: textTheme.bodyBold.copyWith( - color: colorTheme.textHighEmphasis, - ), - threadLatestReplyMessageStyle: textTheme.body.copyWith( - color: colorTheme.textLowEmphasis, - ), - ), + threadListTileTheme: const StreamThreadListTileThemeData(), draftListTileTheme: StreamDraftListTileThemeData( backgroundColor: colorTheme.barsBg, padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 8), @@ -562,50 +462,8 @@ class StreamChatThemeData { color: colorTheme.textLowEmphasis, ), ), - audioWaveformTheme: audioWaveformTheme, - audioWaveformSliderTheme: audioWaveformSliderTheme, - voiceRecordingAttachmentTheme: StreamVoiceRecordingAttachmentThemeData( - backgroundColor: colorTheme.barsBg, - playIcon: const StreamSvgIcon(icon: StreamSvgIcons.play), - pauseIcon: const StreamSvgIcon(icon: StreamSvgIcons.pause), - loadingIndicator: SizedBox.fromSize( - size: const Size.square(24 - /* Padding */ 2), - child: Center( - child: CircularProgressIndicator.adaptive( - valueColor: AlwaysStoppedAnimation(colorTheme.accentPrimary), - ), - ), - ), - audioControlButtonStyle: ElevatedButton.styleFrom( - elevation: 2, - iconColor: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 6), - backgroundColor: Colors.white, - shape: const CircleBorder(), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - minimumSize: const Size(36, 36), - ), - titleTextStyle: textTheme.bodyBold.copyWith( - color: colorTheme.textHighEmphasis, - ), - durationTextStyle: textTheme.footnote.copyWith( - color: colorTheme.textLowEmphasis, - ), - speedControlButtonStyle: ElevatedButton.styleFrom( - elevation: 2, - textStyle: textTheme.footnote, - foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 8), - backgroundColor: Colors.white, - shape: const StadiumBorder(), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - minimumSize: const Size(40, 28), - ), - audioWaveformSliderTheme: audioWaveformSliderTheme, - ), - voiceRecordingTheme: colorTheme.brightness == Brightness.dark - ? StreamVoiceRecordingThemeData.dark() - : StreamVoiceRecordingThemeData.light(), + voiceRecordingAttachmentTheme: const StreamVoiceRecordingAttachmentThemeData(), + channelListItemTheme: const StreamChannelListItemThemeData(), ); } @@ -647,10 +505,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; @@ -672,18 +526,24 @@ class StreamChatThemeData { /// Theme configuration for the [StreamThreadListTile] widget. final StreamThreadListTileThemeData threadListTileTheme; - /// Theme configuration for the [StreamAudioWaveform] widget. - final StreamAudioWaveformThemeData audioWaveformTheme; - - /// Theme configuration for the [StreamAudioWaveformSlider] widget. - final StreamAudioWaveformSliderThemeData audioWaveformSliderTheme; - /// Theme configuration for the [StreamVoiceRecordingAttachment] widget. final StreamVoiceRecordingAttachmentThemeData voiceRecordingAttachmentTheme; + /// Theme configuration for the [StreamChannelListItem] widget. + final StreamChannelListItemThemeData channelListItemTheme; + /// 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({ @@ -698,13 +558,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, @@ -713,51 +569,38 @@ class StreamChatThemeData { StreamPollOptionVotesDialogThemeData? pollOptionVotesDialogTheme, StreamThreadListTileThemeData? threadListTileTheme, StreamDraftListTileThemeData? draftListTileTheme, - StreamAudioWaveformThemeData? audioWaveformTheme, - StreamAudioWaveformSliderThemeData? audioWaveformSliderTheme, StreamVoiceRecordingAttachmentThemeData? voiceRecordingAttachmentTheme, - }) => - StreamChatThemeData.raw( - channelListHeaderTheme: - this.channelListHeaderTheme.merge(channelListHeaderTheme), - textTheme: this.textTheme.merge(textTheme), - colorTheme: this.colorTheme.merge(colorTheme), - primaryIconTheme: this.primaryIconTheme.merge(primaryIconTheme), - channelPreviewTheme: - this.channelPreviewTheme.merge(channelPreviewTheme), - channelHeaderTheme: this.channelHeaderTheme.merge(channelHeaderTheme), - ownMessageTheme: this.ownMessageTheme.merge(ownMessageTheme), - otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme), - messageInputTheme: this.messageInputTheme.merge(messageInputTheme), - 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: - pollResultsDialogTheme ?? this.pollResultsDialogTheme, - pollOptionsDialogTheme: - pollOptionsDialogTheme ?? this.pollOptionsDialogTheme, - pollCommentsDialogTheme: - pollCommentsDialogTheme ?? this.pollCommentsDialogTheme, - pollOptionVotesDialogTheme: - pollOptionVotesDialogTheme ?? this.pollOptionVotesDialogTheme, - threadListTileTheme: threadListTileTheme ?? this.threadListTileTheme, - draftListTileTheme: draftListTileTheme ?? this.draftListTileTheme, - audioWaveformTheme: audioWaveformTheme ?? this.audioWaveformTheme, - audioWaveformSliderTheme: - audioWaveformSliderTheme ?? this.audioWaveformSliderTheme, - voiceRecordingAttachmentTheme: - voiceRecordingAttachmentTheme ?? this.voiceRecordingAttachmentTheme, - ); + StreamChannelListItemThemeData? channelListItemTheme, + }) => StreamChatThemeData.raw( + channelListHeaderTheme: this.channelListHeaderTheme.merge(channelListHeaderTheme), + textTheme: this.textTheme.merge(textTheme), + colorTheme: this.colorTheme.merge(colorTheme), + primaryIconTheme: this.primaryIconTheme.merge(primaryIconTheme), + channelPreviewTheme: this.channelPreviewTheme.merge(channelPreviewTheme), + channelHeaderTheme: this.channelHeaderTheme.merge(channelHeaderTheme), + ownMessageTheme: this.ownMessageTheme.merge(ownMessageTheme), + otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme), + messageInputTheme: this.messageInputTheme.merge(messageInputTheme), + galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme, + galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme, + messageListViewTheme: messageListViewTheme ?? this.messageListViewTheme, + pollCreatorTheme: pollCreatorTheme ?? this.pollCreatorTheme, + pollInteractorTheme: pollInteractorTheme ?? this.pollInteractorTheme, + pollResultsDialogTheme: pollResultsDialogTheme ?? this.pollResultsDialogTheme, + pollOptionsDialogTheme: pollOptionsDialogTheme ?? this.pollOptionsDialogTheme, + pollCommentsDialogTheme: pollCommentsDialogTheme ?? this.pollCommentsDialogTheme, + pollOptionVotesDialogTheme: pollOptionVotesDialogTheme ?? this.pollOptionVotesDialogTheme, + threadListTileTheme: threadListTileTheme ?? this.threadListTileTheme, + draftListTileTheme: draftListTileTheme ?? this.draftListTileTheme, + voiceRecordingAttachmentTheme: voiceRecordingAttachmentTheme ?? this.voiceRecordingAttachmentTheme, + channelListItemTheme: channelListItemTheme ?? this.channelListItemTheme, + ); /// Merge themes StreamChatThemeData merge(StreamChatThemeData? other) { if (other == null) return this; return copyWith( - channelListHeaderTheme: - channelListHeaderTheme.merge(other.channelListHeaderTheme), + channelListHeaderTheme: channelListHeaderTheme.merge(other.channelListHeaderTheme), textTheme: textTheme.merge(other.textTheme), colorTheme: colorTheme.merge(other.colorTheme), primaryIconTheme: other.primaryIconTheme, @@ -768,26 +611,17 @@ class StreamChatThemeData { messageInputTheme: messageInputTheme.merge(other.messageInputTheme), galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme), galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme), - messageListViewTheme: - messageListViewTheme.merge(other.messageListViewTheme), - voiceRecordingTheme: voiceRecordingTheme.merge(other.voiceRecordingTheme), + messageListViewTheme: messageListViewTheme.merge(other.messageListViewTheme), pollCreatorTheme: pollCreatorTheme.merge(other.pollCreatorTheme), pollInteractorTheme: pollInteractorTheme.merge(other.pollInteractorTheme), - pollResultsDialogTheme: - pollResultsDialogTheme.merge(other.pollResultsDialogTheme), - pollOptionsDialogTheme: - pollOptionsDialogTheme.merge(other.pollOptionsDialogTheme), - pollCommentsDialogTheme: - pollCommentsDialogTheme.merge(other.pollCommentsDialogTheme), - pollOptionVotesDialogTheme: - pollOptionVotesDialogTheme.merge(other.pollOptionVotesDialogTheme), + pollResultsDialogTheme: pollResultsDialogTheme.merge(other.pollResultsDialogTheme), + pollOptionsDialogTheme: pollOptionsDialogTheme.merge(other.pollOptionsDialogTheme), + pollCommentsDialogTheme: pollCommentsDialogTheme.merge(other.pollCommentsDialogTheme), + pollOptionVotesDialogTheme: pollOptionVotesDialogTheme.merge(other.pollOptionVotesDialogTheme), threadListTileTheme: threadListTileTheme.merge(other.threadListTileTheme), draftListTileTheme: draftListTileTheme.merge(other.draftListTileTheme), - audioWaveformTheme: audioWaveformTheme.merge(other.audioWaveformTheme), - audioWaveformSliderTheme: - audioWaveformSliderTheme.merge(other.audioWaveformSliderTheme), - voiceRecordingAttachmentTheme: voiceRecordingAttachmentTheme - .merge(other.voiceRecordingAttachmentTheme), + voiceRecordingAttachmentTheme: voiceRecordingAttachmentTheme.merge(other.voiceRecordingAttachmentTheme), + channelListItemTheme: channelListItemTheme.merge(other.channelListItemTheme), ); } } 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..113e67f8f6 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, @@ -127,28 +140,27 @@ class StreamTextTheme { TextStyle? footnoteBold, TextStyle? footnote, TextStyle? captionBold, - }) => - brightness == Brightness.light - ? StreamTextTheme.light( - body: body ?? this.body, - title: title ?? this.title, - headlineBold: headlineBold ?? this.headlineBold, - headline: headline ?? this.headline, - bodyBold: bodyBold ?? this.bodyBold, - footnoteBold: footnoteBold ?? this.footnoteBold, - footnote: footnote ?? this.footnote, - captionBold: captionBold ?? this.captionBold, - ) - : StreamTextTheme.dark( - body: body ?? this.body, - title: title ?? this.title, - headlineBold: headlineBold ?? this.headlineBold, - headline: headline ?? this.headline, - bodyBold: bodyBold ?? this.bodyBold, - footnoteBold: footnoteBold ?? this.footnoteBold, - footnote: footnote ?? this.footnote, - captionBold: captionBold ?? this.captionBold, - ); + }) => brightness == Brightness.light + ? StreamTextTheme.light( + body: body ?? this.body, + title: title ?? this.title, + headlineBold: headlineBold ?? this.headlineBold, + headline: headline ?? this.headline, + bodyBold: bodyBold ?? this.bodyBold, + footnoteBold: footnoteBold ?? this.footnoteBold, + footnote: footnote ?? this.footnote, + captionBold: captionBold ?? this.captionBold, + ) + : StreamTextTheme.dark( + body: body ?? this.body, + title: title ?? this.title, + headlineBold: headlineBold ?? this.headlineBold, + headline: headline ?? this.headline, + bodyBold: bodyBold ?? this.bodyBold, + footnoteBold: footnoteBold ?? this.footnoteBold, + footnote: footnote ?? this.footnote, + captionBold: captionBold ?? this.captionBold, + ); /// Merge text theme StreamTextTheme merge(StreamTextTheme? other) { diff --git a/packages/stream_chat_flutter/lib/src/theme/themes.dart b/packages/stream_chat_flutter/lib/src/theme/themes.dart index 053ba9b039..7bcf5a75e6 100644 --- a/packages/stream_chat_flutter/lib/src/theme/themes.dart +++ b/packages/stream_chat_flutter/lib/src/theme/themes.dart @@ -1,5 +1,3 @@ -export 'audio_waveform_slider_theme.dart'; -export 'audio_waveform_theme.dart'; export 'avatar_theme.dart'; export 'channel_header_theme.dart'; export 'channel_list_header_theme.dart'; @@ -17,7 +15,7 @@ export 'poll_interactor_theme.dart'; export 'poll_option_votes_dialog_theme.dart'; export 'poll_options_dialog_theme.dart'; export 'poll_results_dialog_theme.dart'; +export 'stream_channel_list_item_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/thread_list_tile_theme.dart b/packages/stream_chat_flutter/lib/src/theme/thread_list_tile_theme.dart index 6becfc2a27..cf2f7320db 100644 --- a/packages/stream_chat_flutter/lib/src/theme/thread_list_tile_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/thread_list_tile_theme.dart @@ -29,26 +29,28 @@ class StreamThreadListTileTheme extends InheritedTheme { /// If there is no enclosing [StreamThreadListTileTheme] widget, then /// [StreamChatThemeData.pollOptionVotesDialogTheme] is used. static StreamThreadListTileThemeData of(BuildContext context) { - final threadListTileTheme = - context.dependOnInheritedWidgetOfExactType(); - return threadListTileTheme?.data ?? - StreamChatTheme.of(context).threadListTileTheme; + final threadListTileTheme = context.dependOnInheritedWidgetOfExactType(); + return threadListTileTheme?.data ?? StreamChatTheme.of(context).threadListTileTheme; } @override - Widget wrap(BuildContext context, Widget child) => - StreamThreadListTileTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamThreadListTileTheme(data: data, child: child); @override - bool updateShouldNotify(StreamThreadListTileTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamThreadListTileTheme oldWidget) => data != oldWidget.data; } /// {@template streamThreadListTileThemeData} -/// A style that overrides the default appearance of -/// [StreamPollOptionVotesDialog] widgets when used with -/// [StreamPollCommentsDialogTheme] or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.pollOptionVotesDialogTheme]. +/// Theme data for customizing [StreamThreadListTile] widgets. +/// +/// When a property is null the widget falls back to computed defaults derived +/// from the ambient [StreamTextTheme] and [StreamColorScheme]. See +/// [StreamThreadListTile] for the built-in default values. +/// +/// See also: +/// +/// * [StreamThreadListTileTheme], the inherited theme widget. +/// * [StreamChatThemeData.threadListTileTheme], global theme entry-point. /// {@endtemplate} class StreamThreadListTileThemeData with Diagnosticable { /// {@macro streamThreadListTileThemeData} @@ -61,6 +63,7 @@ class StreamThreadListTileThemeData with Diagnosticable { this.threadLatestReplyMessageStyle, this.threadLatestReplyTimestampStyle, this.threadLatestReplyTimestampFormatter, + this.threadReplyCountStyle, this.threadUnreadMessageCountStyle, this.threadUnreadMessageCountBackgroundColor, }); @@ -72,10 +75,15 @@ class StreamThreadListTileThemeData with Diagnosticable { final Color? backgroundColor; /// The style of the channel name in the [StreamThreadListTile] widget. + /// + /// Falls back to [StreamTextTheme.captionEmphasis] with + /// [StreamColorScheme.textTertiary]. final TextStyle? threadChannelNameStyle; - /// The style of the message the thread is replying to in the - /// [StreamThreadListTile] widget. + /// The style of the root message preview in the [StreamThreadListTile] + /// widget. + /// + /// Falls back to [StreamTextTheme.bodyDefault]. final TextStyle? threadReplyToMessageStyle; /// The style of the latest reply author username in the @@ -83,15 +91,17 @@ class StreamThreadListTileThemeData with Diagnosticable { final TextStyle? threadLatestReplyUsernameStyle; /// The style of the latest reply message in the [StreamThreadListTile]. - /// widget. final TextStyle? threadLatestReplyMessageStyle; /// The style of the latest reply timestamp in the [StreamThreadListTile]. + /// + /// Falls back to [StreamTextTheme.captionDefault] with + /// [StreamColorScheme.textTertiary]. final TextStyle? threadLatestReplyTimestampStyle; /// Formatter for the latest reply timestamp. /// - /// If null, uses the default date formatting. + /// If null, uses [formatRecentDateTime]. /// /// Example: /// ```dart @@ -104,6 +114,12 @@ class StreamThreadListTileThemeData with Diagnosticable { /// ``` final DateFormatter? threadLatestReplyTimestampFormatter; + /// The style of the reply count label in the thread footer. + /// + /// Falls back to [StreamTextTheme.captionEmphasis] with + /// [StreamColorScheme.textLink]. + final TextStyle? threadReplyCountStyle; + /// The style of the unread message count in the [StreamThreadListTile]. final TextStyle? threadUnreadMessageCountStyle; @@ -122,31 +138,24 @@ class StreamThreadListTileThemeData with Diagnosticable { TextStyle? threadLatestReplyMessageStyle, TextStyle? threadLatestReplyTimestampStyle, DateFormatter? threadLatestReplyTimestampFormatter, + TextStyle? threadReplyCountStyle, TextStyle? threadUnreadMessageCountStyle, Color? threadUnreadMessageCountBackgroundColor, - }) => - StreamThreadListTileThemeData( - padding: padding ?? this.padding, - backgroundColor: backgroundColor ?? this.backgroundColor, - threadChannelNameStyle: - threadChannelNameStyle ?? this.threadChannelNameStyle, - threadReplyToMessageStyle: - threadReplyToMessageStyle ?? this.threadReplyToMessageStyle, - threadLatestReplyUsernameStyle: threadLatestReplyUsernameStyle ?? - this.threadLatestReplyUsernameStyle, - threadLatestReplyMessageStyle: - threadLatestReplyMessageStyle ?? this.threadLatestReplyMessageStyle, - threadLatestReplyTimestampStyle: threadLatestReplyTimestampStyle ?? - this.threadLatestReplyTimestampStyle, - threadLatestReplyTimestampFormatter: - threadLatestReplyTimestampFormatter ?? - this.threadLatestReplyTimestampFormatter, - threadUnreadMessageCountStyle: - threadUnreadMessageCountStyle ?? this.threadUnreadMessageCountStyle, - threadUnreadMessageCountBackgroundColor: - threadUnreadMessageCountBackgroundColor ?? - this.threadUnreadMessageCountBackgroundColor, - ); + }) => StreamThreadListTileThemeData( + padding: padding ?? this.padding, + backgroundColor: backgroundColor ?? this.backgroundColor, + threadChannelNameStyle: threadChannelNameStyle ?? this.threadChannelNameStyle, + threadReplyToMessageStyle: threadReplyToMessageStyle ?? this.threadReplyToMessageStyle, + threadLatestReplyUsernameStyle: threadLatestReplyUsernameStyle ?? this.threadLatestReplyUsernameStyle, + threadLatestReplyMessageStyle: threadLatestReplyMessageStyle ?? this.threadLatestReplyMessageStyle, + threadLatestReplyTimestampStyle: threadLatestReplyTimestampStyle ?? this.threadLatestReplyTimestampStyle, + threadLatestReplyTimestampFormatter: + threadLatestReplyTimestampFormatter ?? this.threadLatestReplyTimestampFormatter, + threadReplyCountStyle: threadReplyCountStyle ?? this.threadReplyCountStyle, + threadUnreadMessageCountStyle: threadUnreadMessageCountStyle ?? this.threadUnreadMessageCountStyle, + threadUnreadMessageCountBackgroundColor: + threadUnreadMessageCountBackgroundColor ?? this.threadUnreadMessageCountBackgroundColor, + ); /// Merges this [StreamThreadListTileThemeData] with the [other]. StreamThreadListTileThemeData merge( @@ -161,11 +170,10 @@ class StreamThreadListTileThemeData with Diagnosticable { threadLatestReplyUsernameStyle: other.threadLatestReplyUsernameStyle, threadLatestReplyMessageStyle: other.threadLatestReplyMessageStyle, threadLatestReplyTimestampStyle: other.threadLatestReplyTimestampStyle, - threadLatestReplyTimestampFormatter: - other.threadLatestReplyTimestampFormatter, + threadLatestReplyTimestampFormatter: other.threadLatestReplyTimestampFormatter, + threadReplyCountStyle: other.threadReplyCountStyle, threadUnreadMessageCountStyle: other.threadUnreadMessageCountStyle, - threadUnreadMessageCountBackgroundColor: - other.threadUnreadMessageCountBackgroundColor, + threadUnreadMessageCountBackgroundColor: other.threadUnreadMessageCountBackgroundColor, ); } @@ -174,49 +182,53 @@ class StreamThreadListTileThemeData with Diagnosticable { StreamThreadListTileThemeData? a, StreamThreadListTileThemeData? b, double t, - ) => - StreamThreadListTileThemeData( - padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t), - backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), - threadChannelNameStyle: TextStyle.lerp( - a?.threadChannelNameStyle, - b?.threadChannelNameStyle, - t, - ), - threadReplyToMessageStyle: TextStyle.lerp( - a?.threadReplyToMessageStyle, - b?.threadReplyToMessageStyle, - t, - ), - threadLatestReplyUsernameStyle: TextStyle.lerp( - a?.threadLatestReplyUsernameStyle, - b?.threadLatestReplyUsernameStyle, - t, - ), - threadLatestReplyMessageStyle: TextStyle.lerp( - a?.threadLatestReplyMessageStyle, - b?.threadLatestReplyMessageStyle, - t, - ), - threadLatestReplyTimestampStyle: TextStyle.lerp( - a?.threadLatestReplyTimestampStyle, - b?.threadLatestReplyTimestampStyle, - t, - ), - threadLatestReplyTimestampFormatter: t < 0.5 - ? a?.threadLatestReplyTimestampFormatter - : b?.threadLatestReplyTimestampFormatter, - threadUnreadMessageCountStyle: TextStyle.lerp( - a?.threadUnreadMessageCountStyle, - b?.threadUnreadMessageCountStyle, - t, - ), - threadUnreadMessageCountBackgroundColor: Color.lerp( - a?.threadUnreadMessageCountBackgroundColor, - b?.threadUnreadMessageCountBackgroundColor, - t, - ), - ); + ) => StreamThreadListTileThemeData( + padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t), + backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), + threadChannelNameStyle: TextStyle.lerp( + a?.threadChannelNameStyle, + b?.threadChannelNameStyle, + t, + ), + threadReplyToMessageStyle: TextStyle.lerp( + a?.threadReplyToMessageStyle, + b?.threadReplyToMessageStyle, + t, + ), + threadLatestReplyUsernameStyle: TextStyle.lerp( + a?.threadLatestReplyUsernameStyle, + b?.threadLatestReplyUsernameStyle, + t, + ), + threadLatestReplyMessageStyle: TextStyle.lerp( + a?.threadLatestReplyMessageStyle, + b?.threadLatestReplyMessageStyle, + t, + ), + threadLatestReplyTimestampStyle: TextStyle.lerp( + a?.threadLatestReplyTimestampStyle, + b?.threadLatestReplyTimestampStyle, + t, + ), + threadLatestReplyTimestampFormatter: t < 0.5 + ? a?.threadLatestReplyTimestampFormatter + : b?.threadLatestReplyTimestampFormatter, + threadReplyCountStyle: TextStyle.lerp( + a?.threadReplyCountStyle, + b?.threadReplyCountStyle, + t, + ), + threadUnreadMessageCountStyle: TextStyle.lerp( + a?.threadUnreadMessageCountStyle, + b?.threadUnreadMessageCountStyle, + t, + ), + threadUnreadMessageCountBackgroundColor: Color.lerp( + a?.threadUnreadMessageCountBackgroundColor, + b?.threadUnreadMessageCountBackgroundColor, + t, + ), + ); @override bool operator ==(Object other) => @@ -226,18 +238,13 @@ class StreamThreadListTileThemeData with Diagnosticable { other.backgroundColor == backgroundColor && other.threadChannelNameStyle == threadChannelNameStyle && other.threadReplyToMessageStyle == threadReplyToMessageStyle && - other.threadLatestReplyUsernameStyle == - threadLatestReplyUsernameStyle && - other.threadLatestReplyMessageStyle == - threadLatestReplyMessageStyle && - other.threadLatestReplyTimestampStyle == - threadLatestReplyTimestampStyle && - other.threadLatestReplyTimestampFormatter == - threadLatestReplyTimestampFormatter && - other.threadUnreadMessageCountStyle == - threadUnreadMessageCountStyle && - other.threadUnreadMessageCountBackgroundColor == - threadUnreadMessageCountBackgroundColor; + other.threadLatestReplyUsernameStyle == threadLatestReplyUsernameStyle && + other.threadLatestReplyMessageStyle == threadLatestReplyMessageStyle && + other.threadLatestReplyTimestampStyle == threadLatestReplyTimestampStyle && + other.threadLatestReplyTimestampFormatter == threadLatestReplyTimestampFormatter && + other.threadReplyCountStyle == threadReplyCountStyle && + other.threadUnreadMessageCountStyle == threadUnreadMessageCountStyle && + other.threadUnreadMessageCountBackgroundColor == threadUnreadMessageCountBackgroundColor; @override int get hashCode => @@ -249,6 +256,7 @@ class StreamThreadListTileThemeData with Diagnosticable { threadLatestReplyMessageStyle.hashCode ^ threadLatestReplyTimestampStyle.hashCode ^ threadLatestReplyTimestampFormatter.hashCode ^ + threadReplyCountStyle.hashCode ^ threadUnreadMessageCountStyle.hashCode ^ threadUnreadMessageCountBackgroundColor.hashCode; } 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/theme/voice_recording_attachment_theme.dart b/packages/stream_chat_flutter/lib/src/theme/voice_recording_attachment_theme.dart index 6103156c04..e5aff52079 100644 --- a/packages/stream_chat_flutter/lib/src/theme/voice_recording_attachment_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/voice_recording_attachment_theme.dart @@ -1,198 +1,150 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/theme/audio_waveform_slider_theme.dart'; +import 'package:flutter/widgets.dart'; import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; +import 'package:theme_extensions_builder_annotation/theme_extensions_builder_annotation.dart'; -/// {@template streamVoiceRecordingAttachmentTheme} -/// Overrides the default style of [StreamVoiceRecordingAttachment] descendants. +part 'voice_recording_attachment_theme.g.theme.dart'; + +/// Applies a voice recording attachment theme to descendant +/// [StreamVoiceRecordingAttachment] widgets. +/// +/// Wrap a subtree with [StreamVoiceRecordingAttachmentTheme] to override +/// voice recording styling. Access the merged theme using +/// [StreamVoiceRecordingAttachmentTheme.of]. +/// +/// {@tool snippet} +/// +/// Override voice recording styling for a specific section: +/// +/// ```dart +/// StreamVoiceRecordingAttachmentTheme( +/// data: StreamVoiceRecordingAttachmentThemeData( +/// durationTextStyle: TextStyle(fontWeight: FontWeight.w700), +/// activeDurationTextStyle: TextStyle(color: Colors.blue), +/// ), +/// child: StreamVoiceRecordingAttachment( +/// track: track, +/// speed: speed, +/// ), +/// ) +/// ``` +/// {@end-tool} /// /// See also: /// -/// * [StreamVoiceRecordingAttachmentThemeData], which is used to configure -/// this theme. -/// {@endtemplate} +/// * [StreamVoiceRecordingAttachmentThemeData], which describes the voice +/// recording attachment theme. +/// * [StreamVoiceRecordingAttachment], the widget affected by this theme. class StreamVoiceRecordingAttachmentTheme extends InheritedTheme { - /// Creates a [StreamVoiceRecordingAttachmentTheme]. - /// - /// The [data] parameter must not be null. + /// Creates a voice recording attachment theme that controls descendant + /// widgets. const StreamVoiceRecordingAttachmentTheme({ super.key, required this.data, required super.child, }); - /// The configuration of this theme. + /// The voice recording attachment theme data for descendant widgets. final StreamVoiceRecordingAttachmentThemeData data; - /// The closest instance of this class that encloses the given context. + /// Returns the [StreamVoiceRecordingAttachmentThemeData] merged from local + /// and global themes. /// - /// If there is no enclosing [StreamVoiceRecordingAttachmentTheme] widget, - /// then [StreamVoiceRecordingAttachmentTheme.voiceRecordingTheme] is used. + /// Local values from the nearest [StreamVoiceRecordingAttachmentTheme] + /// ancestor take precedence over global values from [StreamChatTheme.of]. /// - /// Typical usage is as follows: - /// - /// ```dart - /// StreamVoiceRecordingAttachmentTheme theme = - /// StreamVoiceRecordingAttachmentTheme.of(context); - /// ``` + /// This allows partial overrides - for example, overriding only + /// [StreamVoiceRecordingAttachmentThemeData.durationTextStyle] while + /// inheriting other properties from the global theme. static StreamVoiceRecordingAttachmentThemeData of(BuildContext context) { - final voiceRecordingTheme = context.dependOnInheritedWidgetOfExactType< - StreamVoiceRecordingAttachmentTheme>(); - return voiceRecordingTheme?.data ?? - StreamChatTheme.of(context).voiceRecordingAttachmentTheme; + final localTheme = context.dependOnInheritedWidgetOfExactType(); + return StreamChatTheme.of(context).voiceRecordingAttachmentTheme.merge(localTheme?.data); } @override - Widget wrap(BuildContext context, Widget child) => - StreamVoiceRecordingAttachmentTheme(data: data, child: child); + Widget wrap(BuildContext context, Widget child) => StreamVoiceRecordingAttachmentTheme(data: data, child: child); @override - bool updateShouldNotify(StreamVoiceRecordingAttachmentTheme oldWidget) => - data != oldWidget.data; + bool updateShouldNotify(StreamVoiceRecordingAttachmentTheme oldWidget) => data != oldWidget.data; } -/// {@template streamVoiceRecordingAttachmentThemeData} -/// A style that overrides the default appearance of -/// [StreamVoiceRecordingAttachment] widgets when used with -/// [StreamVoiceRecordingAttachmentTheme] or with the overall -/// [StreamChatTheme]'s [StreamChatThemeData.voiceRecordingAttachmentTheme]. -/// {@endtemplate} -class StreamVoiceRecordingAttachmentThemeData with Diagnosticable { - /// {@macro streamVoiceRecordingAttachmentThemeData} +/// Theme data for customizing [StreamVoiceRecordingAttachment] widgets. +/// +/// {@tool snippet} +/// +/// Customize voice recording attachment appearance globally: +/// +/// ```dart +/// StreamChatThemeData( +/// voiceRecordingAttachmentTheme: StreamVoiceRecordingAttachmentThemeData( +/// durationTextStyle: TextStyle(fontWeight: FontWeight.w700), +/// speedToggleStyle: StreamPlaybackSpeedToggleStyle.from( +/// borderColor: Colors.grey, +/// ), +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [StreamVoiceRecordingAttachment], the widget that uses this theme data. +/// * [StreamVoiceRecordingAttachmentTheme], for overriding theme in a widget +/// subtree. +@themeGen +@immutable +class StreamVoiceRecordingAttachmentThemeData with _$StreamVoiceRecordingAttachmentThemeData { + /// Creates voice recording attachment theme data with optional style + /// overrides. const StreamVoiceRecordingAttachmentThemeData({ - this.backgroundColor, - this.playIcon, - this.pauseIcon, - this.loadingIndicator, - this.audioControlButtonStyle, this.titleTextStyle, this.durationTextStyle, - this.speedControlButtonStyle, - this.audioWaveformSliderTheme, + this.activeDurationTextStyle, + this.controlButtonStyle, + this.speedToggleStyle, + this.waveformStyle, }); - /// The background color of the attachment. - final Color? backgroundColor; - - /// The icon widget to show when the recording is playing. - final Widget? playIcon; - - /// The icon widget to show when the recording is paused. - final Widget? pauseIcon; - - /// The widget to show when the recording is loading. - final Widget? loadingIndicator; - - /// The style for the audio control button. - final ButtonStyle? audioControlButtonStyle; - - /// The text style for the title. + /// The text style for the audio file title. + /// + /// If null, defaults to [StreamTextTheme.metadataEmphasis]. final TextStyle? titleTextStyle; - /// The text style for the duration. + /// The text style for the duration label in default/idle state. + /// + /// If null, defaults to [StreamTextTheme.metadataEmphasis] with + /// [StreamColorScheme.textPrimary] color. final TextStyle? durationTextStyle; - /// The style for the speed control button. - final ButtonStyle? speedControlButtonStyle; - - /// The theme for the audio waveform slider. - final StreamAudioWaveformSliderThemeData? audioWaveformSliderTheme; + /// The text style for the duration label when actively playing. + /// + /// If null, defaults to [StreamTextTheme.metadataEmphasis] with + /// [StreamColorScheme.accentPrimary] color. + final TextStyle? activeDurationTextStyle; - /// A copy of [StreamVoiceRecordingAttachmentThemeData] with specified - /// attributes overridden. - StreamVoiceRecordingAttachmentThemeData copyWith({ - Color? backgroundColor, - Widget? playIcon, - Widget? pauseIcon, - Widget? loadingIndicator, - ButtonStyle? audioControlButtonStyle, - TextStyle? titleTextStyle, - TextStyle? durationTextStyle, - ButtonStyle? speedControlButtonStyle, - StreamAudioWaveformSliderThemeData? audioWaveformSliderTheme, - }) => - StreamVoiceRecordingAttachmentThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - playIcon: playIcon ?? this.playIcon, - pauseIcon: pauseIcon ?? this.pauseIcon, - loadingIndicator: loadingIndicator ?? this.loadingIndicator, - audioControlButtonStyle: - audioControlButtonStyle ?? this.audioControlButtonStyle, - titleTextStyle: titleTextStyle ?? this.titleTextStyle, - durationTextStyle: durationTextStyle ?? this.durationTextStyle, - speedControlButtonStyle: - speedControlButtonStyle ?? this.speedControlButtonStyle, - audioWaveformSliderTheme: - audioWaveformSliderTheme ?? this.audioWaveformSliderTheme, - ); + /// The visual styling for the play/pause/replay control button. + /// + /// If null, defaults to secondary outline [StreamButton] defaults with + /// chat-specific border color. + final StreamButtonThemeStyle? controlButtonStyle; - /// Merges this [StreamVoiceRecordingAttachmentThemeData] with the [other]. - StreamVoiceRecordingAttachmentThemeData merge( - StreamVoiceRecordingAttachmentThemeData? other, - ) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - playIcon: other.playIcon, - pauseIcon: other.pauseIcon, - loadingIndicator: other.loadingIndicator, - audioControlButtonStyle: other.audioControlButtonStyle, - titleTextStyle: other.titleTextStyle, - durationTextStyle: other.durationTextStyle, - speedControlButtonStyle: other.speedControlButtonStyle, - audioWaveformSliderTheme: audioWaveformSliderTheme?.merge( - other.audioWaveformSliderTheme, - ), - ); - } + /// The visual styling for the [StreamPlaybackSpeedToggle] (x1, x2, x0.5). + /// + /// If null, defaults to [StreamPlaybackSpeedToggle] defaults with + /// chat-specific border color and disabled state styling. + final StreamPlaybackSpeedToggleStyle? speedToggleStyle; - /// Linearly interpolate between two [StreamVoiceRecordingAttachmentThemeData] - /// objects. - static StreamVoiceRecordingAttachmentThemeData lerp( - StreamVoiceRecordingAttachmentThemeData a, - StreamVoiceRecordingAttachmentThemeData b, + /// The theme overrides for the waveform visualization. + /// + /// Chat-specific waveform colors for idle bars, playing bars, and thumb. + /// If null, defaults to [StreamAudioWaveformTheme] defaults. + final StreamAudioWaveformThemeData? waveformStyle; + + /// Linearly interpolate between two + /// [StreamVoiceRecordingAttachmentThemeData] objects. + static StreamVoiceRecordingAttachmentThemeData? lerp( + StreamVoiceRecordingAttachmentThemeData? a, + StreamVoiceRecordingAttachmentThemeData? b, double t, - ) { - return StreamVoiceRecordingAttachmentThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - playIcon: t < 0.5 ? a.playIcon : b.playIcon, - pauseIcon: t < 0.5 ? a.pauseIcon : b.pauseIcon, - loadingIndicator: t < 0.5 ? a.loadingIndicator : b.loadingIndicator, - audioControlButtonStyle: ButtonStyle.lerp( - a.audioControlButtonStyle, b.audioControlButtonStyle, t), - titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), - durationTextStyle: - TextStyle.lerp(a.durationTextStyle, b.durationTextStyle, t), - speedControlButtonStyle: ButtonStyle.lerp( - a.speedControlButtonStyle, b.speedControlButtonStyle, t), - audioWaveformSliderTheme: StreamAudioWaveformSliderThemeData.lerp( - a.audioWaveformSliderTheme!, b.audioWaveformSliderTheme!, t), - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is StreamVoiceRecordingAttachmentThemeData && - other.backgroundColor == backgroundColor && - other.playIcon == playIcon && - other.pauseIcon == pauseIcon && - other.loadingIndicator == loadingIndicator && - other.audioControlButtonStyle == audioControlButtonStyle && - other.titleTextStyle == titleTextStyle && - other.durationTextStyle == durationTextStyle && - other.speedControlButtonStyle == speedControlButtonStyle && - other.audioWaveformSliderTheme == audioWaveformSliderTheme; - - @override - int get hashCode => - backgroundColor.hashCode ^ - playIcon.hashCode ^ - pauseIcon.hashCode ^ - loadingIndicator.hashCode ^ - audioControlButtonStyle.hashCode ^ - titleTextStyle.hashCode ^ - durationTextStyle.hashCode ^ - speedControlButtonStyle.hashCode ^ - audioWaveformSliderTheme.hashCode; + ) => _$StreamVoiceRecordingAttachmentThemeData.lerp(a, b, t); } diff --git a/packages/stream_chat_flutter/lib/src/theme/voice_recording_attachment_theme.g.theme.dart b/packages/stream_chat_flutter/lib/src/theme/voice_recording_attachment_theme.g.theme.dart new file mode 100644 index 0000000000..881b9d49f1 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/voice_recording_attachment_theme.g.theme.dart @@ -0,0 +1,153 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, unused_element + +part of 'voice_recording_attachment_theme.dart'; + +// ************************************************************************** +// ThemeGenGenerator +// ************************************************************************** + +mixin _$StreamVoiceRecordingAttachmentThemeData { + bool get canMerge => true; + + static StreamVoiceRecordingAttachmentThemeData? lerp( + StreamVoiceRecordingAttachmentThemeData? a, + StreamVoiceRecordingAttachmentThemeData? b, + double t, + ) { + if (identical(a, b)) { + return a; + } + + if (a == null) { + return t == 1.0 ? b : null; + } + + if (b == null) { + return t == 0.0 ? a : null; + } + + return StreamVoiceRecordingAttachmentThemeData( + titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), + durationTextStyle: TextStyle.lerp( + a.durationTextStyle, + b.durationTextStyle, + t, + ), + activeDurationTextStyle: TextStyle.lerp( + a.activeDurationTextStyle, + b.activeDurationTextStyle, + t, + ), + controlButtonStyle: StreamButtonThemeStyle.lerp( + a.controlButtonStyle, + b.controlButtonStyle, + t, + ), + speedToggleStyle: StreamPlaybackSpeedToggleStyle.lerp( + a.speedToggleStyle, + b.speedToggleStyle, + t, + ), + waveformStyle: StreamAudioWaveformThemeData.lerp( + a.waveformStyle, + b.waveformStyle, + t, + ), + ); + } + + StreamVoiceRecordingAttachmentThemeData copyWith({ + TextStyle? titleTextStyle, + TextStyle? durationTextStyle, + TextStyle? activeDurationTextStyle, + StreamButtonThemeStyle? controlButtonStyle, + StreamPlaybackSpeedToggleStyle? speedToggleStyle, + StreamAudioWaveformThemeData? waveformStyle, + }) { + final _this = (this as StreamVoiceRecordingAttachmentThemeData); + + return StreamVoiceRecordingAttachmentThemeData( + titleTextStyle: titleTextStyle ?? _this.titleTextStyle, + durationTextStyle: durationTextStyle ?? _this.durationTextStyle, + activeDurationTextStyle: + activeDurationTextStyle ?? _this.activeDurationTextStyle, + controlButtonStyle: controlButtonStyle ?? _this.controlButtonStyle, + speedToggleStyle: speedToggleStyle ?? _this.speedToggleStyle, + waveformStyle: waveformStyle ?? _this.waveformStyle, + ); + } + + StreamVoiceRecordingAttachmentThemeData merge( + StreamVoiceRecordingAttachmentThemeData? other, + ) { + final _this = (this as StreamVoiceRecordingAttachmentThemeData); + + if (other == null || identical(_this, other)) { + return _this; + } + + if (!other.canMerge) { + return other; + } + + return copyWith( + titleTextStyle: + _this.titleTextStyle?.merge(other.titleTextStyle) ?? + other.titleTextStyle, + durationTextStyle: + _this.durationTextStyle?.merge(other.durationTextStyle) ?? + other.durationTextStyle, + activeDurationTextStyle: + _this.activeDurationTextStyle?.merge(other.activeDurationTextStyle) ?? + other.activeDurationTextStyle, + controlButtonStyle: + _this.controlButtonStyle?.merge(other.controlButtonStyle) ?? + other.controlButtonStyle, + speedToggleStyle: + _this.speedToggleStyle?.merge(other.speedToggleStyle) ?? + other.speedToggleStyle, + waveformStyle: + _this.waveformStyle?.merge(other.waveformStyle) ?? + other.waveformStyle, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other.runtimeType != runtimeType) { + return false; + } + + final _this = (this as StreamVoiceRecordingAttachmentThemeData); + final _other = (other as StreamVoiceRecordingAttachmentThemeData); + + return _other.titleTextStyle == _this.titleTextStyle && + _other.durationTextStyle == _this.durationTextStyle && + _other.activeDurationTextStyle == _this.activeDurationTextStyle && + _other.controlButtonStyle == _this.controlButtonStyle && + _other.speedToggleStyle == _this.speedToggleStyle && + _other.waveformStyle == _this.waveformStyle; + } + + @override + int get hashCode { + final _this = (this as StreamVoiceRecordingAttachmentThemeData); + + return Object.hash( + runtimeType, + _this.titleTextStyle, + _this.durationTextStyle, + _this.activeDurationTextStyle, + _this.controlButtonStyle, + _this.speedToggleStyle, + _this.waveformStyle, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/user/user_mention_tile.dart b/packages/stream_chat_flutter/lib/src/user/user_mention_tile.dart index f7f9ffa42d..9765954252 100644 --- a/packages/stream_chat_flutter/lib/src/user/user_mention_tile.dart +++ b/packages/stream_chat_flutter/lib/src/user/user_mention_tile.dart @@ -43,11 +43,7 @@ class StreamUserMentionTile extends StatelessWidget { const SizedBox( width: 16, ), - leading ?? - StreamUserAvatar( - user: user, - constraints: BoxConstraints.tight(const Size(40, 40)), - ), + leading ?? StreamUserAvatar(size: .lg, user: user), const SizedBox(width: 8), Expanded( child: Align( @@ -83,8 +79,8 @@ class StreamUserMentionTile extends StatelessWidget { right: 18, left: 8, ), - child: StreamSvgIcon( - icon: StreamSvgIcons.mentions, + child: Icon( + context.streamIcons.mention20, color: chatThemeData.colorTheme.accentPrimary, ), ), diff --git a/packages/stream_chat_flutter/lib/src/utils/date_formatter.dart b/packages/stream_chat_flutter/lib/src/utils/date_formatter.dart index 1b41cf6d3f..da88ef0a72 100644 --- a/packages/stream_chat_flutter/lib/src/utils/date_formatter.dart +++ b/packages/stream_chat_flutter/lib/src/utils/date_formatter.dart @@ -3,10 +3,7 @@ import 'package:jiffy/jiffy.dart'; import 'package:stream_chat_flutter/src/utils/extensions.dart'; /// Represents a function type that formats a date. -typedef DateFormatter = String Function( - BuildContext context, - DateTime date, -); +typedef DateFormatter = String Function(BuildContext context, DateTime date); /// Formats the given [date] as a String. String formatDate(BuildContext context, DateTime date) { @@ -17,6 +14,54 @@ String formatDate(BuildContext context, DateTime date) { return Jiffy.parseFromDateTime(date).yMd; } +/// Output examples: +/// - `Just now` +/// - `Today at 9:41` +/// - `Yesterday at 9:41` +/// - `Saturday at 9:41` +/// - `Jan 1st at 9:41` +String formatRecentDateTime( + BuildContext context, + DateTime date, { + DateTime? referenceDate, +}) { + final localDate = date.toLocal(); + final now = (referenceDate ?? DateTime.now()).toLocal(); + final difference = now.difference(localDate).abs(); + + if (difference < const Duration(minutes: 1)) return 'Just now'; + + final jiffyDate = Jiffy.parseFromDateTime(localDate); + final time = jiffyDate.format(pattern: 'H:mm'); + + if (_isSameDay(localDate, now)) { + return '${context.translations.todayLabel} at $time'; + } + + final yesterday = now.subtract(const Duration(days: 1)); + if (_isSameDay(localDate, yesterday)) { + return '${context.translations.yesterdayLabel} at $time'; + } + + if (_isWithinPreviousWeek(localDate, now)) { + return '${jiffyDate.EEEE} at $time'; + } + + return '${jiffyDate.format(pattern: 'MMM do')} at $time'; +} + +bool _isSameDay(DateTime a, DateTime b) { + final jiffyA = Jiffy.parseFromDateTime(a); + final jiffyB = Jiffy.parseFromDateTime(b); + return jiffyA.isSame(jiffyB, unit: Unit.day); +} + +bool _isWithinPreviousWeek(DateTime date, DateTime referenceDate) { + final jiffyDate = Jiffy.parseFromDateTime(date); + final jiffyReference = Jiffy.parseFromDateTime(referenceDate); + return jiffyDate.isAfter(jiffyReference.subtract(days: 7), unit: Unit.day); +} + /// Extension on [DateTime] to provide common date comparison utilities. extension DateTimeComparisonUtils on DateTime { /// Returns true if the date is today. diff --git a/packages/stream_chat_flutter/lib/src/utils/device_segmentation.dart b/packages/stream_chat_flutter/lib/src/utils/device_segmentation.dart index bd65820c5e..334e4cfe30 100644 --- a/packages/stream_chat_flutter/lib/src/utils/device_segmentation.dart +++ b/packages/stream_chat_flutter/lib/src/utils/device_segmentation.dart @@ -7,16 +7,12 @@ bool get isWeb => CurrentPlatform.isWeb; bool get isMobileDevice => CurrentPlatform.isIos || CurrentPlatform.isAndroid; /// Returns true if the app is running in a desktop device. -bool get isDesktopDevice => - CurrentPlatform.isMacOS || - CurrentPlatform.isWindows || - CurrentPlatform.isLinux; +bool get isDesktopDevice => CurrentPlatform.isMacOS || CurrentPlatform.isWindows || CurrentPlatform.isLinux; /// Returns true if the app is running on windows or linux platform. bool get isDesktopVideoPlayerSupported => // Dart VLC is not supported on MacOS. - !CurrentPlatform.isMacOS && - (CurrentPlatform.isWindows || CurrentPlatform.isLinux); + !CurrentPlatform.isMacOS && (CurrentPlatform.isWindows || CurrentPlatform.isLinux); /// Returns true if the app is running in a mobile or web. bool get isMobileDeviceOrWeb => isWeb || isMobileDevice; diff --git a/packages/stream_chat_flutter/lib/src/utils/extensions.dart b/packages/stream_chat_flutter/lib/src/utils/extensions.dart index 164a0ec053..8590a4a9c9 100644 --- a/packages/stream_chat_flutter/lib/src/utils/extensions.dart +++ b/packages/stream_chat_flutter/lib/src/utils/extensions.dart @@ -23,8 +23,7 @@ extension IntExtension on int { if (this <= 0) return '0 B'; const suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; final i = (log(this) / log(_byteUnitConversionFactor)).floor(); - final numberValue = - (this / pow(_byteUnitConversionFactor, i)).toStringAsFixed(2); + final numberValue = (this / pow(_byteUnitConversionFactor, i)).toStringAsFixed(2); final suffix = suffixes[i]; return '$numberValue $suffix'; } @@ -44,14 +43,25 @@ extension DurationExtension on Duration { /// String extension extension StringExtension on String { /// Returns the capitalized string - String capitalize() => - isNotEmpty ? '${this[0].toUpperCase()}${substring(1).toLowerCase()}' : ''; + @Deprecated('Use sentenceCase instead') + String capitalize() => sentenceCase; + + /// Returns the string in sentence case. + /// + /// Example: 'hello WORLD' -> 'Hello world' + String get sentenceCase { + if (isEmpty) return this; + + final firstChar = this[0].toUpperCase(); + final restOfString = substring(1).toLowerCase(); + + return '$firstChar$restOfString'; + } /// Returns the biggest line of a text. String biggestLine() { if (contains('\n')) { - return split('\n') - .reduce((curr, next) => curr.length > next.length ? curr : next); + return split('\n').reduce((curr, next) => curr.length > next.length ? curr : next); } else { return this; } @@ -91,55 +101,15 @@ extension StringExtension on String { /// Levenshtein distance between this and [t]. int levenshteinDistance(String t) => levenshtein(this, t); - - /// Returns a resized imageUrl with the given [width], [height], [resize] - /// and [crop] if it is from Stream CDN or Dashboard. - /// - /// Read more at https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing - String getResizedImageUrl({ - // TODO: Are these sizes optimal? Consider web/desktop - double width = 400, - double height = 400, - String /*clip|crop|scale|fill*/ resize = 'clip', - String /*center|top|bottom|left|right*/ crop = 'center', - }) { - final uri = Uri.parse(this); - final host = uri.host; - - final fromStreamCDN = host.endsWith('stream-io-cdn.com'); - final fromStreamDashboard = host.endsWith('stream-cloud-uploads.imgix.net'); - - if (!fromStreamCDN && !fromStreamDashboard) return this; - - final queryParameters = {...uri.queryParameters}; - - if (fromStreamCDN) { - if (queryParameters['h'].isNullOrMatches('*') && - queryParameters['w'].isNullOrMatches('*') && - queryParameters['crop'].isNullOrMatches('*') && - queryParameters['resize'].isNullOrMatches('*')) { - queryParameters['h'] = height.floor().toString(); - queryParameters['w'] = width.floor().toString(); - queryParameters['crop'] = crop; - queryParameters['resize'] = resize; - } - } else if (fromStreamDashboard) { - queryParameters['height'] = height.floor().toString(); - queryParameters['width'] = width.floor().toString(); - queryParameters['fit'] = crop; - } - - return uri.replace(queryParameters: queryParameters).toString(); - } } /// List extension extension IterableExtension on Iterable { /// Insert any item inBetween the list items List insertBetween(T item) => expand((e) sync* { - yield item; - yield e; - }).skip(1).toList(growable: false); + yield item; + yield e; + }).skip(1).toList(growable: false); } /// Useful extension for [PlatformFile] @@ -251,8 +221,7 @@ extension InputDecorationX on InputDecoration { suffixIconConstraints: other.suffixIconConstraints, counter: other.counter, counterText: other.counterText, - counterStyle: - counterStyle?.merge(other.counterStyle) ?? other.counterStyle, + counterStyle: counterStyle?.merge(other.counterStyle) ?? other.counterStyle, filled: other.filled, fillColor: other.fillColor, focusColor: other.focusColor, @@ -279,8 +248,7 @@ extension BuildContextX on BuildContext { /// Retrieves current translations according to locale /// Defaults to [DefaultTranslations] - Translations get translations => - StreamChatLocalizations.of(this) ?? DefaultTranslations.instance; + Translations get translations => StreamChatLocalizations.of(this) ?? DefaultTranslations.instance; } /// Extension on [BorderRadius] @@ -372,8 +340,7 @@ extension UserListX on List { final entries = matchingUsers.entries.toList(growable: false) ..sort((prev, curr) { bool containsQuery(User user) => - normalize(user.id).contains(normalizedQuery) || - normalize(user.name).contains(normalizedQuery); + normalize(user.id).contains(normalizedQuery) || normalize(user.name).contains(normalizedQuery); final containsInPrev = containsQuery(prev.key); final containsInCurr = containsQuery(curr.key); @@ -401,7 +368,7 @@ extension MessageX on Message { messageTextToRender = messageTextToRender?.replaceAll( RegExp('@(${RegExp.escape(userId)}|${RegExp.escape(userName)})'), - linkify ? '[@$userName]($userId)' : '@$userName', + linkify ? '[@$userName](mention:$userId)' : '@$userName', ); } @@ -413,8 +380,7 @@ extension MessageX on Message { var messageTextLength = min(text?.biggestLine().length ?? 0, 65); if (quotedMessage != null) { - var quotedMessageLength = - (min(quotedMessage!.text?.biggestLine().length ?? 0, 65)) + 8; + var quotedMessageLength = (min(quotedMessage!.text?.biggestLine().length ?? 0, 65)) + 8; if (quotedMessage!.attachments.isNotEmpty) { quotedMessageLength += 8; @@ -436,8 +402,7 @@ extension MessageX on Message { } /// It returns the message with the translated text if available locally - Message translate(String language) => - copyWith(text: i18n?['${language}_text'] ?? text); + Message translate(String language) => copyWith(text: i18n?['${language}_text'] ?? text); /// It returns the message replacing the mentioned user names with /// the respective user ids @@ -479,18 +444,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, + }; } } @@ -498,18 +457,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.', + ), + }; } } @@ -576,24 +533,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]. @@ -619,16 +558,11 @@ extension ChannelModelX on ChannelModel { // Otherwise, we return the names of the first `maxMembers` members sorted // alphabetically, followed by the number of remaining members if there are // more than `maxMembers` members. - final memberNames = otherMembers - .map((it) => it.user?.name) - .whereType() - .take(maxMembers) - .sorted(); + final memberNames = otherMembers.map((it) => it.user?.name).whereType().take(maxMembers).sorted(); return switch (otherMembers.length <= maxMembers) { true => memberNames.join(', '), - false => - '${memberNames.join(', ')} + ${otherMembers.length - maxMembers}', + false => '${memberNames.join(', ')} + ${otherMembers.length - maxMembers}', }; } } @@ -657,6 +591,15 @@ extension VoiceRecordingAttachmentExtension on Attachment { } } +/// {@template singleAttachmentPlaylistExtension} +/// Extension on [Attachment] to provide the playlist specific +/// properties. +/// {@endtemplate} +extension SingleAttachmentPlaylistExtension on Attachment { + /// Converts the attachment to a list of [PlaylistTrack]. + List toPlaylist() => [this].toPlaylist(); +} + /// {@template attachmentPlaylistExtension} /// Extension on [Iterable] to provide the playlist specific /// properties. @@ -668,25 +611,25 @@ extension AttachmentPlaylistExtension on Iterable { ...map((it) { final uri = switch (it.uploadState) { Preparing() || InProgress() || Failed() => () { - if (CurrentPlatform.isWeb) { - final bytes = it.file?.bytes; - final mimeType = it.file?.mediaType?.mimeType; - if (bytes == null || mimeType == null) return null; + if (CurrentPlatform.isWeb) { + final bytes = it.file?.bytes; + final mimeType = it.file?.mediaType?.mimeType; + if (bytes == null || mimeType == null) return null; - return Uri.dataFromBytes(bytes, mimeType: mimeType); - } + return Uri.dataFromBytes(bytes, mimeType: mimeType); + } - final path = it.file?.path; - if (path == null) return null; + final path = it.file?.path; + if (path == null) return null; - return Uri.file(path, windows: CurrentPlatform.isWindows); - }(), + return Uri.file(path, windows: CurrentPlatform.isWindows); + }(), Success() => () { - final url = it.assetUrl; - if (url == null) return null; + final url = it.assetUrl; + if (url == null) return null; - return Uri.tryParse(url); - }(), + return Uri.tryParse(url); + }(), }; if (uri == null) return null; @@ -696,8 +639,33 @@ extension AttachmentPlaylistExtension on Iterable { title: it.title, waveform: it.waveform, duration: it.duration, + key: it, ); }).nonNulls, ]; } } + +/// 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/utils/helpers.dart b/packages/stream_chat_flutter/lib/src/utils/helpers.dart index 1e732bc40d..3f6226489f 100644 --- a/packages/stream_chat_flutter/lib/src/utils/helpers.dart +++ b/packages/stream_chat_flutter/lib/src/utils/helpers.dart @@ -113,10 +113,9 @@ Future showConfirmationBottomSheet( onPressed: () => Navigator.of(context).pop(false), style: TextButton.styleFrom( textStyle: chatThemeData.textTheme.bodyBold, - foregroundColor: - chatThemeData.colorTheme.textHighEmphasis - // ignore: deprecated_member_use - .withOpacity(0.5), + foregroundColor: chatThemeData.colorTheme.textHighEmphasis + // ignore: deprecated_member_use + .withOpacity(0.5), ), child: Text(cancelText), ), @@ -155,8 +154,7 @@ Future showInfoBottomSheet( }) { final chatThemeData = StreamChatTheme.of(context); return showModalBottomSheet( - backgroundColor: - theme?.colorTheme.barsBg ?? chatThemeData.colorTheme.barsBg, + backgroundColor: theme?.colorTheme.barsBg ?? chatThemeData.colorTheme.barsBg, context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -177,8 +175,7 @@ Future showInfoBottomSheet( ), Text( title, - style: theme?.textTheme.headlineBold ?? - chatThemeData.textTheme.headlineBold, + style: theme?.textTheme.headlineBold ?? chatThemeData.textTheme.headlineBold, ), const SizedBox( height: 7, @@ -188,10 +185,9 @@ Future showInfoBottomSheet( height: 36, ), Container( - // ignore: deprecated_member_use - color: theme?.colorTheme.textHighEmphasis.withOpacity(0.08) ?? - // ignore: deprecated_member_use - chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.08), + color: + theme?.colorTheme.textHighEmphasis.withValues(alpha: 0.08) ?? + chatThemeData.colorTheme.textHighEmphasis.withValues(alpha: 0.08), height: 1, ), Center( @@ -203,8 +199,7 @@ Future showInfoBottomSheet( okText, style: TextStyle( // ignore: deprecated_member_use - color: theme?.colorTheme.textHighEmphasis.withOpacity(0.5) ?? - chatThemeData.colorTheme.accentPrimary, + color: theme?.colorTheme.textHighEmphasis.withOpacity(0.5) ?? chatThemeData.colorTheme.accentPrimary, fontWeight: FontWeight.w400, ), ), @@ -217,8 +212,7 @@ Future showInfoBottomSheet( } /// Get random png with initials -String getRandomPicUrl(User user) => - 'https://getstream.io/random_png/?id=${user.id}&name=${user.name}'; +String getRandomPicUrl(User user) => 'https://getstream.io/random_png/?id=${user.id}&name=${user.name}'; /// Get websiteName from [hostName] String? getWebsiteName(String hostName) { @@ -308,8 +302,7 @@ String fileSize(dynamic size, [int round = 2]) { return '${(_size / divider / divider / divider).toStringAsFixed(round)} GB'; } - if (_size < divider * divider * divider * divider * divider && - _size % divider == 0) { + if (_size < divider * divider * divider * divider * divider && _size % divider == 0) { final num r = _size / divider / divider / divider / divider; return '${r.toStringAsFixed(0)} TB'; } @@ -319,8 +312,7 @@ String fileSize(dynamic size, [int round = 2]) { return '${r.toStringAsFixed(round)} TB'; } - if (_size < divider * divider * divider * divider * divider * divider && - _size % divider == 0) { + if (_size < divider * divider * divider * divider * divider * divider && _size % divider == 0) { final num r = _size / divider / divider / divider / divider / divider; return '${r.toStringAsFixed(0)} PB'; } else { @@ -346,8 +338,7 @@ StreamSvgIcon getFileTypeImage([String? mimeType]) { 'application/zip' => StreamSvgIcons.filetypeCompressionZip, 'application/x-7z-compressed' => StreamSvgIcons.filetypeCompression7z, 'application/x-arj' => StreamSvgIcons.filetypeCompressionArj, - 'application/vnd.debian.binary-package' => - StreamSvgIcons.filetypeCompressionDeb, + 'application/vnd.debian.binary-package' => StreamSvgIcons.filetypeCompressionDeb, 'application/x-apple-diskimage' => StreamSvgIcons.filetypeCompressionPkg, 'application/x-rar-compressed' => StreamSvgIcons.filetypeCompressionRar, 'application/x-rpm' => StreamSvgIcons.filetypeCompressionRpm, @@ -357,20 +348,14 @@ StreamSvgIcon getFileTypeImage([String? mimeType]) { 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => StreamSvgIcons.filetypePresentationPptx, 'application/vnd.apple.keynote' => StreamSvgIcons.filetypePresentationKey, - 'application/vnd.oasis.opendocument.presentation' => - StreamSvgIcons.filetypePresentationOdp, + 'application/vnd.oasis.opendocument.presentation' => StreamSvgIcons.filetypePresentationOdp, 'application/vnd.ms-excel' => StreamSvgIcons.filetypeSpreadsheetXls, - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => - StreamSvgIcons.filetypeSpreadsheetXlsx, - 'application/vnd.ms-excel.sheet.macroEnabled.12' => - StreamSvgIcons.filetypeSpreadsheetXlsm, - 'application/vnd.oasis.opendocument.spreadsheet' => - StreamSvgIcons.filetypeSpreadsheetOds, + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => StreamSvgIcons.filetypeSpreadsheetXlsx, + 'application/vnd.ms-excel.sheet.macroEnabled.12' => StreamSvgIcons.filetypeSpreadsheetXlsm, + 'application/vnd.oasis.opendocument.spreadsheet' => StreamSvgIcons.filetypeSpreadsheetOds, 'application/msword' => StreamSvgIcons.filetypeTextDoc, - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => - StreamSvgIcons.filetypeTextDocx, - 'application/vnd.oasis.opendocument.text' => - StreamSvgIcons.filetypeTextOdt, + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => StreamSvgIcons.filetypeTextDocx, + 'application/vnd.oasis.opendocument.text' => StreamSvgIcons.filetypeTextOdt, 'text/plain' => StreamSvgIcons.filetypeTextTxt, 'application/rtf' => StreamSvgIcons.filetypeTextRtf, 'application/x-tex' => StreamSvgIcons.filetypeTextTex, diff --git a/packages/stream_chat_flutter/lib/src/utils/message_preview_formatter.dart b/packages/stream_chat_flutter/lib/src/utils/message_preview_formatter.dart index 2ba4a6b726..ae5a2f2fab 100644 --- a/packages/stream_chat_flutter/lib/src/utils/message_preview_formatter.dart +++ b/packages/stream_chat_flutter/lib/src/utils/message_preview_formatter.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.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'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; /// {@template messagePreviewFormatter} /// Formats message previews for display. @@ -46,13 +45,14 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// ```dart /// class CustomFormatter extends StreamMessagePreviewFormatter { /// @override -/// String formatGroupMessage( +/// TextSpan? formatGroupMessage( /// BuildContext context, /// User? messageAuthor, -/// String messageText, +/// User? currentUser, /// ) { -/// if (messageAuthor == null) return messageText; -/// return '${messageAuthor.name} says: $messageText'; +/// final name = messageAuthor?.name; +/// if (name == null || name.isEmpty) return null; +/// return TextSpan(text: '$name says: '); /// } /// } /// ``` @@ -93,25 +93,29 @@ abstract interface class MessagePreviewFormatter { /// /// This formatter applies context-aware formatting based on message type, /// sender identity, and channel configuration. It handles various message -/// types including regular text, attachments, polls, system messages, and -/// deleted messages. +/// types including regular text, attachments, polls, locations, system +/// messages, and deleted messages. /// /// ## Message Type Handling /// /// The formatter handles messages differently based on their type: /// -/// * **Deleted messages** - Shows "Message deleted" +/// * **Deleted messages** - Shows a ban icon with localized deleted label /// * **System messages** - Shows the message text directly -/// * **Poll messages** - Shows poll emoji with voter/creator info +/// * **Poll messages** - Shows a poll icon with the poll name +/// * **Location messages** - Shows a map pin icon with location label or +/// message caption /// * **Regular messages** - Shows text with optional attachment previews /// /// ## Sender Context /// -/// The formatting adapts based on who sent the message: +/// In group channels (member count > 2), [formatGroupMessage] prepends +/// a sender prefix: +/// +/// * **Current user** - Adds bold "You:" prefix +/// * **Other users** - Adds bold first-name prefix /// -/// * **Current user** - Adds "You:" prefix -/// * **Direct messages (1-on-1)** - No prefix -/// * **Group messages** - Adds sender name prefix +/// In direct (1-on-1) channels, no sender prefix is added. /// /// ## Customization /// @@ -120,19 +124,23 @@ abstract interface class MessagePreviewFormatter { /// ```dart /// class ShortFormatter extends StreamMessagePreviewFormatter { /// @override -/// String formatCurrentUserMessage(BuildContext context, String text) { -/// // Remove "You:" prefix for cleaner display. -/// return text; +/// TextSpan? formatGroupMessage( +/// BuildContext context, +/// User? messageAuthor, +/// User? currentUser, +/// ) { +/// // Remove sender prefix for cleaner display. +/// return null; /// } /// /// @override -/// String formatPollMessage( +/// TextSpan formatPollMessage( /// BuildContext context, /// Poll poll, -/// User? currentUser, -/// ) { -/// // Always show just the poll name. -/// return poll.name.isEmpty ? '📊 Poll' : '📊 ${poll.name}'; +/// User? currentUser, { +/// TextStyle? textStyle, +/// }) { +/// return TextSpan(text: poll.name.isEmpty ? 'Poll' : poll.name); /// } /// } /// ``` @@ -143,18 +151,17 @@ abstract interface class MessagePreviewFormatter { /// /// **Content Extraction:** /// * [formatRegularMessage] - Extracts message content (text + attachments) -/// * [formatMessageAttachments] - Formats attachment previews +/// * [formatMessageAttachments] - Formats attachment previews with icons /// /// **Message Types:** /// * [formatDeletedMessage] - Formats deleted messages /// * [formatSystemMessage] - Formats system messages /// * [formatEmptyMessage] - Formats empty messages /// * [formatPollMessage] - Formats poll messages +/// * [formatLocationMessage] - Formats shared location messages /// /// **Sender Context:** -/// * [formatCurrentUserMessage] - Formats messages from current user -/// * [formatDirectMessage] - Formats messages in 1-on-1 channels -/// * [formatGroupMessage] - Formats messages in group channels +/// * [formatGroupMessage] - Adds sender prefix in group channels /// /// **Draft Messages:** /// * [getDraftPrefix] - Returns the draft message prefix text @@ -167,52 +174,35 @@ class StreamMessagePreviewFormatter implements MessagePreviewFormatter { TextSpan formatMessage( BuildContext context, Message message, { + bool showCaption = true, ChannelModel? channel, User? currentUser, TextStyle? textStyle, }) { + final effectiveTextStyle = textStyle ?? DefaultTextStyle.of(context).style; + final previewText = _buildPreviewText( context, message, channel, currentUser, + showCaption: showCaption, + textStyle: effectiveTextStyle, ); - final mentionedUsers = message.mentionedUsers; - if (mentionedUsers.isEmpty) { - return TextSpan(text: previewText, style: textStyle); - } - - final mentionedUsersRegex = RegExp( - mentionedUsers.map((it) => '@${RegExp.escape(it.name)}').join('|'), - ); - - final children = [ - ...previewText.splitByRegExp(mentionedUsersRegex).map( - (text) { - if (mentionedUsers.any((it) => '@${it.name}' == text)) { - return TextSpan( - text: text, - style: textStyle?.copyWith(fontWeight: FontWeight.bold), - ); - } - - return TextSpan(text: text, style: textStyle); - }, - ) - ]; - - return TextSpan(children: children); + return TextSpan(children: [previewText], style: textStyle); } - String _buildPreviewText( + TextSpan _buildPreviewText( BuildContext context, Message message, ChannelModel? channel, - User? currentUser, - ) { + User? currentUser, { + bool showCaption = true, + TextStyle? textStyle, + }) { if (message.isDeleted) { - return formatDeletedMessage(context, message); + return formatDeletedMessage(context, message, textStyle: textStyle); } if (message.isSystem) { @@ -220,262 +210,478 @@ class StreamMessagePreviewFormatter implements MessagePreviewFormatter { } if (message.poll case final poll?) { - return formatPollMessage(context, poll, currentUser); + return formatPollMessage(context, poll, currentUser, textStyle: textStyle); } - final messagePreviewText = formatRegularMessage(context, message); - if (messagePreviewText == null) return formatEmptyMessage(context, message); + TextSpan? messageSpan; + + if (message.sharedLocation case final location?) { + messageSpan = formatLocationMessage( + context, + message, + location, + showCaption: showCaption, + textStyle: textStyle, + ); + } else { + messageSpan = formatRegularMessage( + context, + message, + showCaption: showCaption, + textStyle: textStyle, + ); + } - if (channel == null) return messagePreviewText; + if (messageSpan == null) return formatEmptyMessage(context, message); - if (message.user?.id == currentUser?.id) { - return formatCurrentUserMessage(context, messagePreviewText); - } + if (channel == null) return messageSpan; - if (channel.memberCount > 2) { - return formatGroupMessage(context, message.user, messagePreviewText); - } + return TextSpan( + children: [ + if (channel.memberCount > 2) ?formatGroupMessage(context, message.user, currentUser), + messageSpan, + ], + ); + } - return formatDirectMessage(context, messagePreviewText); + TextSpan _textSpanWithMentions(String text, List mentionedUsers, StreamColorScheme colorScheme) { + if (mentionedUsers.isEmpty) return TextSpan(text: text); + + final mentionRegex = RegExp( + mentionedUsers.map((it) => '@${RegExp.escape(it.name)}').join('|'), + ); + + final parts = text.splitByRegExp(mentionRegex); + if (parts.length <= 1) return TextSpan(text: text); + + return TextSpan( + children: parts.map((part) { + if (mentionedUsers.any((it) => '@${it.name}' == part)) { + return TextSpan( + text: part, + style: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.accentPrimary), + ); + } + return TextSpan(text: part); + }).toList(), + ); } - /// The text content of a regular [message], including attachment previews. + /// The content of a regular [message] as a [TextSpan], including attachment + /// previews. /// /// Extracts the message text and formats any attachments using - /// [formatMessageAttachments]. Returns `null` if the message has no text - /// or attachments. + /// [formatMessageAttachments]. Mentions within the text are bolded. + /// Returns `null` if the message has no text or attachments. + /// + /// When [showCaption] is `true` and the message has both text and + /// attachments, the text is shown alongside the attachment icon. /// /// Override to customize how message content is extracted: /// /// ```dart /// @override - /// String? formatRegularMessage(BuildContext context, Message message) { - /// // Only show text, ignore attachments - /// return message.text; + /// TextSpan? formatRegularMessage( + /// BuildContext context, + /// Message message, { + /// bool showCaption = true, + /// TextStyle? textStyle, + /// }) { + /// final text = message.text; + /// if (text == null || text.isEmpty) return null; + /// return TextSpan(text: text); /// } /// ``` @protected - String? formatRegularMessage(BuildContext context, Message message) { + TextSpan? formatRegularMessage( + BuildContext context, + Message message, { + bool showCaption = true, + TextStyle? textStyle, + }) { final messageText = switch (message.text?.trim()) { final text? when text.isNotEmpty => text, _ => null, }; final attachments = message.attachments; - if (attachments.isEmpty) return messageText; - - return formatMessageAttachments(context, messageText, message.attachments); - } + final mentionedUsers = message.mentionedUsers; + final colorScheme = context.streamColorScheme; - /// The preview text for a deleted [message]. - @protected - String formatDeletedMessage(BuildContext context, Message message) { - return context.translations.messageDeletedLabel; - } + if (attachments.isEmpty) { + return messageText != null ? _textSpanWithMentions(messageText, mentionedUsers, colorScheme) : null; + } - /// The preview text for a system [message]. - @protected - String formatSystemMessage(BuildContext context, Message message) { - if (message.text case final text? when text.isNotEmpty) return text; - return context.translations.systemMessageLabel; + return formatMessageAttachments( + context, + messageText, + message.attachments, + mentionedUsers: mentionedUsers, + showCaption: showCaption, + textStyle: textStyle, + ); } - /// The preview text for an empty [message]. + /// The preview [TextSpan] for a deleted [message]. + /// + /// Shows a ban icon followed by the localized deleted message label, + /// both styled with the tertiary text color. @protected - String formatEmptyMessage(BuildContext context, Message message) { - return context.translations.emptyMessagePreviewText; + TextSpan formatDeletedMessage(BuildContext context, Message message, {TextStyle? textStyle}) { + final iconSize = (textStyle?.fontSize ?? 16) + 2; + return TextSpan( + style: textStyle, + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + context.streamIcons.noSign16, + size: iconSize, + color: context.streamColorScheme.textTertiary, + ), + ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: SizedBox(width: context.streamSpacing.xxs), + ), + TextSpan( + text: context.translations.messageDeletedLabel, + style: TextStyle(color: context.streamColorScheme.textTertiary), + ), + ], + ); } - /// The formatted [messageText] with "You:" prefix for the current user. - /// - /// Override this to customize how messages from the current user are - /// displayed: + /// The preview [TextSpan] for a system [message]. /// - /// ```dart - /// @override - /// String formatCurrentUserMessage( - /// BuildContext context, - /// String messageText, - /// ) { - /// return messageText; // Remove prefix - /// } - /// ``` + /// Returns the message text if available, otherwise a localized + /// system message label. @protected - String formatCurrentUserMessage(BuildContext context, String messageText) { - return '${context.translations.youText}: $messageText'; + TextSpan formatSystemMessage(BuildContext context, Message message) { + if (message.text case final text? when text.isNotEmpty) return TextSpan(text: text); + return TextSpan(text: context.translations.systemMessageLabel); } - /// The [messageText] without prefix for 1-on-1 channels. - /// - /// No prefix is added since the other user's identity is clear from the - /// channel itself. Override to add context if needed: + /// The preview [TextSpan] for an empty [message]. /// - /// ```dart - /// @override - /// String formatDirectMessage(BuildContext context, String messageText) { - /// return '💬 $messageText'; - /// } - /// ``` + /// Returns the localized empty message preview text, styled with the + /// tertiary text color. @protected - String formatDirectMessage(BuildContext context, String messageText) { - return messageText; + TextSpan formatEmptyMessage(BuildContext context, Message message) { + return TextSpan( + text: context.translations.emptyMessagePreviewText, + style: TextStyle(color: context.streamColorScheme.textTertiary), + ); } - /// The formatted [messageText] with [messageAuthor] name prefix for groups. + /// A bold sender prefix [TextSpan] for group channel previews. /// - /// Adds the author's name as a prefix. Returns [messageText] without - /// prefix if [messageAuthor] is `null`. + /// Returns a "You: " prefix when [messageAuthor] matches [currentUser], + /// or the author's first name followed by ": " for other users. Returns + /// `null` if the author name is unavailable. /// - /// Override to customize author name formatting: + /// Override to customize the sender prefix: /// /// ```dart /// @override - /// String formatGroupMessage( + /// TextSpan? formatGroupMessage( /// BuildContext context, /// User? messageAuthor, - /// String messageText, + /// User? currentUser, /// ) { - /// if (messageAuthor == null) return messageText; - /// return '${messageAuthor.name} says: $messageText'; + /// final name = messageAuthor?.name; + /// if (name == null || name.isEmpty) return null; + /// return TextSpan(text: '$name: '); /// } /// ``` @protected - String formatGroupMessage( + TextSpan? formatGroupMessage( BuildContext context, User? messageAuthor, - String messageText, + User? currentUser, ) { - final authorName = messageAuthor?.name; - if (authorName == null || authorName.isEmpty) return messageText; + if (messageAuthor?.id == currentUser?.id) { + return TextSpan( + text: '${context.translations.youText}: ', + style: TextStyle( + fontWeight: FontWeight.bold, + color: context.streamColorScheme.textTertiary, + ), + ); + } - return '$authorName: $messageText'; + final authorName = messageAuthor?.name.split(' ')[0]; + if (authorName == null || authorName.isEmpty) return null; + + return TextSpan( + text: '$authorName: ', + style: TextStyle( + fontWeight: FontWeight.bold, + color: context.streamColorScheme.textTertiary, + ), + ); } - /// The formatted preview for the first attachment in [attachments]. + /// The formatted preview [TextSpan] for [attachments]. + /// + /// Renders an icon prefix based on the attachment type, followed by either + /// the [messageText] (when [showCaption] is `true`) or a descriptive suffix + /// (attachment name, count, or duration). [mentionedUsers] in the message + /// text are bolded. /// - /// Formats each attachment type with an emoji icon and title. The - /// [messageText] is used as fallback for certain types. Returns - /// [messageText] if no attachments are present or the type is unsupported. + /// Returns `null` if [attachments] is empty and [messageText] is `null`. /// - /// Supported types: Audio (🎧), File (📄), Image (📷), Video (📹), - /// Giphy (/giphy), and Voice Recording (🎤). + /// When attachments have mixed types, a generic file icon is used with the + /// total file count. For uniform types, a type-specific icon is shown: + /// Audio/Voice Recording (microphone), Image (camera), Video (video), + /// Giphy (/giphy), and File (file). /// /// Override to handle custom attachment types: /// /// ```dart /// @override - /// String? formatMessageAttachments( + /// TextSpan? formatMessageAttachments( /// BuildContext context, /// String? messageText, - /// Iterable attachments, - /// ) { + /// Iterable attachments, { + /// List mentionedUsers = const [], + /// bool showCaption = true, + /// TextStyle? textStyle, + /// }) { /// final attachment = attachments.firstOrNull; /// if (attachment?.type == 'product') { - /// return '🛍️ ${attachment?.extraData['title'] ?? "Product"}'; + /// return TextSpan(text: '🛍️ Product'); /// } /// return super.formatMessageAttachments( /// context, /// messageText, /// attachments, + /// mentionedUsers: mentionedUsers, + /// showCaption: showCaption, + /// textStyle: textStyle, /// ); /// } /// ``` @protected - String? formatMessageAttachments( + TextSpan? formatMessageAttachments( BuildContext context, String? messageText, - Iterable attachments, - ) { - final translations = context.translations; + Iterable attachments, { + List mentionedUsers = const [], + bool showCaption = true, + TextStyle? textStyle, + }) { + final colorScheme = context.streamColorScheme; final attachment = attachments.firstOrNull; - if (attachment == null) return messageText; - - // If the message contains some attachments, we will show the first one - // and the text if it exists. - final attachmentIcon = switch (attachment.type) { - AttachmentType.audio => '🎧', - AttachmentType.file => '📄', - AttachmentType.image => '📷', - AttachmentType.video => '📹', - AttachmentType.giphy => '/giphy', - AttachmentType.voiceRecording => '🎤', - _ => null, - }; + if (attachment == null) { + return messageText != null ? _textSpanWithMentions(messageText, mentionedUsers, colorScheme) : null; + } - final attachmentTitle = switch (attachment.type) { - AttachmentType.audio => messageText ?? translations.audioAttachmentText, - AttachmentType.file => attachment.title ?? messageText, - AttachmentType.image => messageText ?? translations.imageAttachmentText, - AttachmentType.video => messageText ?? translations.videoAttachmentText, - AttachmentType.giphy => messageText, - AttachmentType.voiceRecording => translations.voiceRecordingText, - _ => null, + final mixedTypes = attachments.any((it) => it.type != attachment.type); + final prefix = _attachmentPrefix(context, mixedTypes ? null : attachment.type, textStyle: textStyle); + + if (showCaption && messageText != null) { + return TextSpan( + children: [ + prefix, + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: SizedBox(width: context.streamSpacing.xxs), + ), + _textSpanWithMentions(messageText, mentionedUsers, colorScheme), + ], + ); + } + + final suffix = _attachmentSuffix( + context, + attachment, + count: attachments.length, + isMixed: mixedTypes, + ); + + return TextSpan( + children: [ + prefix, + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: SizedBox(width: context.streamSpacing.xxs), + ), + ?suffix, + ], + ); + } + + InlineSpan _attachmentPrefix(BuildContext context, String? type, {TextStyle? textStyle}) { + final size = (textStyle?.fontSize ?? 14) + 2; + final icons = context.streamIcons; + return switch (type) { + AttachmentType.audio || AttachmentType.voiceRecording => WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon(icons.voice16, size: size), + ), + AttachmentType.image => WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon(icons.camera16, size: size), + ), + AttachmentType.video => WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon(icons.video16, size: size), + ), + AttachmentType.giphy => const TextSpan(text: '/giphy'), + _ => WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon(icons.file16, size: size), + ), }; + } - if (attachmentIcon != null || attachmentTitle != null) { - return [attachmentIcon, attachmentTitle].nonNulls.join(' '); - } + TextSpan? _attachmentSuffix( + BuildContext context, + Attachment attachment, { + required int count, + required bool isMixed, + }) { + final translations = context.translations; + + if (isMixed) return TextSpan(text: translations.filesAttachmentCountText(count)); - return messageText; + return switch (attachment.type) { + AttachmentType.audio => TextSpan(text: translations.audioAttachmentText), + AttachmentType.voiceRecording => TextSpan( + text: '${translations.voiceRecordingText} (${attachment.duration.toMinutesAndSeconds()})', + ), + AttachmentType.file => TextSpan( + text: (count == 1 ? attachment.file?.name : null) ?? translations.filesAttachmentCountText(count), + ), + AttachmentType.image => TextSpan(text: translations.photosAttachmentCountText(count)), + AttachmentType.video => TextSpan(text: translations.videosAttachmentCountText(count)), + _ => null, + }; } - /// The formatted preview for a [poll] message with voter or creator info. + /// The formatted preview [TextSpan] for a [poll] message. /// - /// Shows the latest voter and poll name if the poll has votes, otherwise - /// shows the creator and poll name. If the poll has no votes or creator, - /// shows just the poll name. Actions by [currentUser] show as "You", - /// while actions by other users show their name. + /// Shows a poll chart icon followed by the latest vote activity when + /// available, or the poll name as a fallback. Specifically: + /// + /// - If the [currentUser] cast the latest vote, shows "You voted: {answer}". + /// - If another user cast the latest vote, shows "{name} voted: {answer}". + /// - Otherwise, falls back to displaying the [poll] name (trimmed). If the + /// name is empty, only the icon is shown. /// /// Override to customize poll formatting: /// /// ```dart /// @override - /// String formatPollMessage( + /// TextSpan formatPollMessage( /// BuildContext context, /// Poll poll, - /// User? currentUser, - /// ) { - /// return poll.name.isEmpty ? '📊 Poll' : '📊 ${poll.name}'; + /// User? currentUser, { + /// TextStyle? textStyle, + /// }) { + /// return TextSpan( + /// text: poll.name.isEmpty ? 'Poll' : poll.name, + /// ); /// } /// ``` @protected - String formatPollMessage( + TextSpan formatPollMessage( BuildContext context, Poll poll, - User? currentUser, - ) { + User? currentUser, { + TextStyle? textStyle, + }) { final translations = context.translations; + final iconSize = (textStyle?.fontSize ?? 16) + 2; + TextSpan? latestVoterSpan; - // If the poll already contains some votes, we will preview the latest voter - // and the poll name - if (poll.latestVotes.firstOrNull?.user case final latestVoter?) { - if (latestVoter.id == currentUser?.id) { + if (poll.latestVotes.firstOrNull case final latestVote? + when latestVote.answerText != null && latestVote.answerText!.isNotEmpty) { + final answerText = latestVote.answerText!; + if (latestVote.user?.id == currentUser?.id) { final youVoted = translations.pollYouVotedText; - return '📊 $youVoted: "${poll.name}"'; + latestVoterSpan = TextSpan(text: '$youVoted: $answerText'); + } else if (latestVote.user case final latestVoter?) { + final someoneVoted = translations.pollSomeoneVotedText(latestVoter.name.split(' ')[0]); + latestVoterSpan = TextSpan(text: '$someoneVoted: $answerText'); } - - final someoneVoted = translations.pollSomeoneVotedText(latestVoter.name); - return '📊 $someoneVoted: "${poll.name}"'; } - // Otherwise, we will show the creator of the poll and the poll name - if (poll.createdBy case final creator?) { - if (creator.id == currentUser?.id) { - final youCreated = translations.pollYouCreatedText; - return '📊 $youCreated: "${poll.name}"'; - } - - final someoneCreated = translations.pollSomeoneCreatedText(creator.name); - return '📊 $someoneCreated: "${poll.name}"'; - } - - // Otherwise, we will show the poll name if it exists. - if (poll.name.trim() case final pollName when pollName.isNotEmpty) { - return '📊 $pollName'; - } + return TextSpan( + style: textStyle, + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon(context.streamIcons.poll16, size: iconSize), + ), + if (latestVoterSpan case final latestVoterSpan?) ...[ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: SizedBox(width: context.streamSpacing.xxs), + ), + latestVoterSpan, + ] else if (poll.name.trim() case final pollName when pollName.isNotEmpty) ...[ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: SizedBox(width: context.streamSpacing.xxs), + ), + TextSpan(text: pollName), + ], + ], + ); + } - // If nothing else, we will show the default poll emoji. - return '📊'; + /// The formatted preview [TextSpan] for a shared [location] message. + /// + /// Shows a map pin icon followed by the [message] text (when [showCaption] + /// is `true` and text is available) or a localized location label. Live + /// locations use a distinct label from static ones. + /// + /// Override to customize shared location formatting: + /// + /// ```dart + /// @override + /// TextSpan formatLocationMessage( + /// BuildContext context, + /// Message message, + /// Location location, { + /// bool showCaption = true, + /// TextStyle? textStyle, + /// }) { + /// return TextSpan( + /// text: '(${location.latitude}, ${location.longitude})', + /// ); + /// } + /// ``` + @protected + TextSpan formatLocationMessage( + BuildContext context, + Message message, + Location location, { + bool showCaption = true, + TextStyle? textStyle, + }) { + final colorScheme = context.streamColorScheme; + final iconSize = (textStyle?.fontSize ?? 16) + 2; + return TextSpan( + style: textStyle, + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon(context.streamIcons.location16, size: iconSize), + ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: SizedBox(width: context.streamSpacing.xxs), + ), + if (message.text?.trim() case final messageText? when messageText.isNotEmpty && showCaption) ...[ + _textSpanWithMentions(messageText, message.mentionedUsers, colorScheme), + ] else ...[ + TextSpan(text: context.translations.locationLabel(isLive: location.isLive)), + ], + ], + ); } @override @@ -484,16 +690,16 @@ class StreamMessagePreviewFormatter implements MessagePreviewFormatter { DraftMessage draftMessage, { TextStyle? textStyle, }) { - final theme = StreamChatTheme.of(context); - final colorTheme = theme.colorTheme; + final colorScheme = context.streamColorScheme; return TextSpan( - text: getDraftPrefix(context), - style: textStyle?.copyWith( - fontWeight: FontWeight.bold, - color: colorTheme.accentPrimary, - ), children: [ + TextSpan( + text: getDraftPrefix(context), + style: (textStyle ?? context.streamTextTheme.captionEmphasis).copyWith( + color: colorScheme.accentPrimary, + ), + ), const TextSpan(text: ' '), // Space between prefix and message TextSpan(text: draftMessage.text, style: textStyle), ], diff --git a/packages/stream_chat_flutter/lib/src/utils/stream_image_cdn.dart b/packages/stream_chat_flutter/lib/src/utils/stream_image_cdn.dart new file mode 100644 index 0000000000..2b9b569b78 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/utils/stream_image_cdn.dart @@ -0,0 +1,175 @@ +/// Resize mode for CDN image transformations. +/// +/// See the [Stream Image Resizing docs](https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing) +/// for more information. +enum ResizeMode { + /// Resizes the image to fit within the given dimensions, preserving the + /// aspect ratio. The image may be smaller than the requested size. + clip('clip'), + + /// Resizes and crops the image to exactly fill the given dimensions. + crop('crop'), + + /// Stretches the image to exactly fill the given dimensions, + /// ignoring the aspect ratio. + scale('scale'), + + /// Resizes the image to fill the given dimensions, preserving the + /// aspect ratio. Parts of the image may be cropped. + fill('fill') + ; + + const ResizeMode(this.value); + + /// The raw string value used as a CDN query parameter. + final String value; +} + +/// Crop alignment for CDN image transformations. +/// +/// This determines which part of the image is preserved when cropping. +enum CropMode { + /// Crop from the center of the image. + center('center'), + + /// Crop from the top of the image. + top('top'), + + /// Crop from the bottom of the image. + bottom('bottom'), + + /// Crop from the left of the image. + left('left'), + + /// Crop from the right of the image. + right('right') + ; + + const CropMode(this.value); + + /// The raw string value used as a CDN query parameter. + final String value; +} + +/// Configuration for resizing an image via a CDN. +/// +/// When passed to [StreamImageCDN.resolveUrl], the CDN will resize the image +/// to the given [width] and [height] using the specified [mode] and [crop]. +class ImageResize { + /// Creates a new [ImageResize] configuration. + const ImageResize({ + required this.width, + required this.height, + this.mode = .clip, + this.crop = .center, + }); + + /// The target width in logical pixels. + final double width; + + /// The target height in logical pixels. + final double height; + + /// The resize mode to use. + /// + /// Defaults to [ResizeMode.clip]. + final ResizeMode mode; + + /// The crop alignment when the resize mode requires cropping. + /// + /// Defaults to [CropMode.center]. + final CropMode crop; +} + +/// Handles CDN URL resolution and cache key generation for Stream Chat images. +/// +/// The default implementation supports Stream's own CDN +/// (`stream-io-cdn.com`). +/// +/// To customize behavior for a custom CDN, extend this class and override +/// [resolveUrl] and/or [cacheKey]: +/// +/// ```dart +/// class MyImageCDN extends StreamImageCDN { +/// @override +/// String cacheKey(String imageUrl) { +/// // Custom cache key logic for your CDN. +/// return Uri.parse(imageUrl).path; +/// } +/// } +/// ``` +/// +/// Then inject it via [StreamChatConfigurationData]: +/// +/// ```dart +/// StreamChat( +/// client: client, +/// config: StreamChatConfigurationData( +/// imageCDN: MyImageCDN(), +/// ), +/// child: ..., +/// ) +/// ``` +class StreamImageCDN { + /// Creates a new [StreamImageCDN] instance. + const StreamImageCDN(); + + // The host suffix for Stream's image CDN. + static const _streamCDNHost = 'stream-io-cdn.com'; + + // Query parameter names that are preserved in cache keys. + // + // These are the image-transformation parameters that affect + // which rendition of the image is returned. All other parameters + // (e.g. signed URL tokens) are stripped. + static const _persistedParameters = {'w', 'h', 'resize', 'crop'}; + + /// Resolves the [sourceUrl] by appending resize/transform parameters + /// appropriate for the CDN. + /// + /// When [resize] is null, no resizing parameters are added and the + /// [sourceUrl] is returned unchanged. + /// + /// For non-Stream CDN URLs, returns [sourceUrl] unchanged regardless + /// of [resize]. + /// + /// Override this to customize URL rewriting for a custom CDN. + String resolveUrl(String sourceUrl, {ImageResize? resize}) { + final uri = Uri.tryParse(sourceUrl); + if (uri == null || !uri.host.contains(_streamCDNHost)) return sourceUrl; + if (resize == null) return sourceUrl; + + final queryParameters = { + ...uri.queryParameters, + 'w': resize.width == 0 ? '*' : resize.width.floor().toString(), + 'h': resize.height == 0 ? '*' : resize.height.floor().toString(), + 'resize': resize.mode.value, + 'ro': '0', + if (resize.mode == ResizeMode.crop) 'crop': resize.crop.value, + }; + + return uri.replace(queryParameters: queryParameters).toString(); + } + + /// Returns a stable cache key for [imageUrl], stripping volatile + /// authentication parameters (e.g. CloudFront signed URL tokens) + /// while preserving those that identify distinct image renditions. + /// + /// This uses an allowlist approach, keeping only the parameters in + /// [_persistedParameters] for Stream CDN URLs. + /// + /// For non-Stream CDN URLs, returns the full URL string unchanged. + /// + /// Override this to customize cache key generation for a custom CDN. + String cacheKey(String imageUrl) { + final uri = Uri.tryParse(imageUrl); + if (uri == null || !uri.host.contains(_streamCDNHost)) return imageUrl; + + final filteredParams = { + for (final MapEntry(:key, :value) in uri.queryParameters.entries) + if (_persistedParameters.contains(key)) key: value, + }; + + return uri.replace(queryParameters: filteredParams).toString(); + } +} diff --git a/packages/stream_chat_flutter/lib/src/utils/typedefs.dart b/packages/stream_chat_flutter/lib/src/utils/typedefs.dart index ab6a9d318a..99c2b4a37f 100644 --- a/packages/stream_chat_flutter/lib/src/utils/typedefs.dart +++ b/packages/stream_chat_flutter/lib/src/utils/typedefs.dart @@ -42,10 +42,11 @@ typedef ReplyMessageCallback = void Function(Message message); /// The action to perform when a specific image attachment in an [ImageGroup] /// is tapped or clicked. /// {@endtemplate} -typedef OnImageGroupAttachmentTap = void Function( - Message message, - Attachment attachment, -); +typedef OnImageGroupAttachmentTap = + void Function( + Message message, + Attachment attachment, + ); /// {@template onUserAvatarPress} /// The action to perform when a user's avatar is tapped, clicked, or @@ -68,11 +69,12 @@ typedef EditMessageInputBuilder = Widget Function(BuildContext, Message); /// {@template channelListHeaderTitleBuilder} /// A widget builder for custom [ChannelListHeader] title widgets. /// {@endtemplate} -typedef ChannelListHeaderTitleBuilder = Widget Function( - BuildContext context, - ConnectionStatus status, - StreamChatClient client, -); +typedef ChannelListHeaderTitleBuilder = + Widget Function( + BuildContext context, + ConnectionStatus status, + StreamChatClient client, + ); /// {@template channelTapCallback} /// The action to perform when a channel is tapped or clicked. @@ -97,11 +99,12 @@ typedef ViewInfoCallback = void Function(Channel); /// [defaultActionsModal] is the default [AttachmentActionsModal] configuration. /// Use [defaultActionsModal.copyWith] to easily customize it /// {@endtemplate} -typedef AttachmentActionsBuilder = Widget Function( - BuildContext context, - Attachment attachment, - AttachmentActionsModal defaultActionsModal, -); +typedef AttachmentActionsBuilder = + Widget Function( + BuildContext context, + Attachment attachment, + AttachmentActionsModal defaultActionsModal, + ); /// {@template errorListener} /// A callback that can be passed to [StreamMessageInput.onError]. @@ -110,10 +113,11 @@ typedef AttachmentActionsBuilder = Widget Function( /// /// It exists merely for error reporting, and should not be used otherwise. /// {@endtemplate} -typedef ErrorListener = void Function( - Object error, - StackTrace? stackTrace, -); +typedef ErrorListener = + void Function( + Object error, + StackTrace? stackTrace, + ); /// {@template attachmentLimitExceededListener} /// A callback that can be passed to @@ -123,45 +127,50 @@ typedef ErrorListener = void Function( /// /// It exists merely for showing custom error, and should not be used otherwise. /// {@endtemplate} -typedef AttachmentLimitExceedListener = void Function( - int limit, - String error, -); +typedef AttachmentLimitExceedListener = + void Function( + int limit, + String error, + ); /// {@template attachmentThumbnailBuilder} /// A widget builder for representing attachment thumbnails. /// {@endtemplate} -typedef AttachmentThumbnailBuilder = Widget Function( - BuildContext, - Attachment, -); +typedef AttachmentThumbnailBuilder = + Widget Function( + BuildContext, + Attachment, + ); /// {@template mentionTileBuilder} /// A widget builder for representing a custom mention tile. /// {@endtemplate} -typedef MentionTileBuilder = Widget Function( - BuildContext context, - Member member, -); +typedef MentionTileBuilder = + Widget Function( + BuildContext context, + Member member, + ); /// {@template mentionTileOverlayBuilder} /// A widget builder for representing a custom mention tile within a /// [UserMentionsOverlay]. /// {@endtemplate} -typedef MentionTileOverlayBuilder = Widget Function( - BuildContext context, - User user, -); +typedef MentionTileOverlayBuilder = + Widget Function( + BuildContext context, + User user, + ); /// {@template userMentionTileBuilder} /// A builder function for representing a custom user mention tile. /// /// Use [UserMentionTile] for the default implementation. /// {@endtemplate} -typedef UserMentionTileBuilder = Widget Function( - BuildContext context, - User user, -); +typedef UserMentionTileBuilder = + Widget Function( + BuildContext context, + User user, + ); /// {@template actionButtonBuilder} /// A widget builder for building a custom command button. @@ -169,10 +178,11 @@ typedef UserMentionTileBuilder = Widget Function( /// [commandButton] is the default [CommandButton] configuration, /// use [commandButton.copyWith] to easily customize it. /// {@endtemplate} -typedef CommandButtonBuilder = Widget Function( - BuildContext context, - CommandButton commandButton, -); +typedef CommandButtonBuilder = + Widget Function( + BuildContext context, + CommandButton commandButton, + ); /// {@template actionButtonBuilder} /// A widget builder for building a custom action button. @@ -180,27 +190,30 @@ typedef CommandButtonBuilder = Widget Function( /// [attachmentButton] is the default [AttachmentButton] configuration, /// use [attachmentButton.copyWith] to easily customize it. /// {@endtemplate} -typedef AttachmentButtonBuilder = Widget Function( - BuildContext context, - AttachmentButton attachmentButton, -); +typedef AttachmentButtonBuilder = + Widget Function( + BuildContext context, + AttachmentButton attachmentButton, + ); /// {@template quotedMessageAttachmentThumbnailBuilder} /// A widget builder for building a custom quoted message attachment thumbnail. /// {@endtemplate} -typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function( - BuildContext, - Attachment, -); +typedef QuotedMessageAttachmentThumbnailBuilder = + Widget Function( + BuildContext, + Attachment, + ); /// {@template attachmentBuilder} /// A widget builder for representing attachments. /// {@endtemplate} -typedef AttachmentBuilder = Widget Function( - BuildContext, - Message, - List, -); +typedef AttachmentBuilder = + Widget Function( + BuildContext, + Message, + List, + ); /// {@template onQuotedMessageTap} /// The action to perform when a quoted message is tapped. @@ -237,59 +250,41 @@ typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); /// {@template messageSearchItemBuilder} /// A widget builder used to create a custom [ListUserItem] from a [User]. /// {@endtemplate} -typedef MessageSearchItemBuilder = Widget Function( - BuildContext, - GetMessageResponse, -); +typedef MessageSearchItemBuilder = + Widget Function( + BuildContext, + GetMessageResponse, + ); -/// {@template messageBuilder} -/// A widget builder for creating custom message UI. -/// -/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration. -/// Use [defaultMessageWidget.copyWith] to customize it. -/// {@endtemplate} -typedef MessageBuilder = Widget Function( - BuildContext, - MessageDetails, - List, - StreamMessageWidget defaultMessageWidget, -); - -/// {@template parentMessageBuilder} -/// A widget builder for creating custom parent message UI. -/// -/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration. -/// Use [defaultMessageWidget.copyWith] to customize it. -/// {@endtemplate} -typedef ParentMessageBuilder = Widget Function( - BuildContext, - Message?, - StreamMessageWidget defaultMessageWidget, -); +// Legacy MessageBuilder and ParentMessageBuilder typedefs removed. +// Use StreamMessageWidgetBuilder from message_list_view.dart instead. /// {@template systemMessageBuilder} /// A widget builder for creating custom system messages. /// {@endtemplate} -typedef SystemMessageBuilder = Widget Function( - BuildContext, - Message, -); +typedef SystemMessageBuilder = + Widget Function( + BuildContext, + Message, + ); /// {@template ephemeralMessageBuilder} /// A widget builder for creating custom ephemeral messages. /// {@endtemplate} -typedef EphemeralMessageBuilder = Widget Function( - BuildContext, - Message, -); +typedef EphemeralMessageBuilder = + Widget Function( + BuildContext, + Message, + ); /// {@template moderatedMessageBuilder} /// A widget builder for creating custom moderated messages. /// {@endtemplate} -typedef ModeratedMessageBuilder = Widget Function( - BuildContext, - Message, -); +typedef ModeratedMessageBuilder = + Widget Function( + BuildContext, + Message, + ); /// {@template threadBuilder} /// A widget builder for creating custom thread UI. @@ -323,23 +318,25 @@ typedef ThreadTapCallback = void Function(Message, Widget?); /// ), /// ```dart /// {@endtemplate} -typedef SpacingWidgetBuilder = Widget Function( - BuildContext context, - List spacingTypes, -); +typedef SpacingWidgetBuilder = + Widget Function( + BuildContext context, + List spacingTypes, + ); /// {@template attachmentDownloader} /// A callback for downloading an attachment asset. /// {@endtemplate} /// Callback to download an attachment asset -typedef AttachmentDownloader = Future Function( - Attachment attachment, { - ProgressCallback? onReceiveProgress, - Map? queryParameters, - CancelToken? cancelToken, - bool deleteOnError, - Options? options, -}); +typedef AttachmentDownloader = + Future Function( + Attachment attachment, { + ProgressCallback? onReceiveProgress, + Map? queryParameters, + CancelToken? cancelToken, + bool deleteOnError, + Options? options, + }); /// Callback to receive the path once the attachment asset is downloaded typedef DownloadedPathCallback = void Function(String? path); @@ -366,10 +363,11 @@ typedef OnScrollToBottom = Function(int unreadCount); /// Widget builder for widgets that may require data from the /// [MessageInputController]. -typedef MessageRelatedBuilder = Widget Function( - BuildContext context, - StreamMessageInputController messageInputController, -); +typedef MessageRelatedBuilder = + Widget Function( + BuildContext context, + StreamMessageInputController messageInputController, + ); /// A function that returns true if the message is valid and can be sent. typedef MessageValidator = bool Function(Message message); 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..8134b62191 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 @@ -41,8 +41,7 @@ import 'package:stream_chat_flutter/src/video/video_service.dart'; /// ``` /// {@end-tool} /// {@endtemplate} -class StreamVideoThumbnailImage - extends ImageProvider { +class StreamVideoThumbnailImage extends ImageProvider { /// {@macro video_thumbnail_image} const StreamVideoThumbnailImage({ required this.video, @@ -87,10 +86,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 +101,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'); @@ -133,9 +130,7 @@ class StreamVideoThumbnailImage if (other.runtimeType != runtimeType) { return false; } - return other is StreamVideoThumbnailImage && - other.video == video && - other.scale == scale; + return other is StreamVideoThumbnailImage && other.video == video && other.scale == scale; } @override diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 3d149312ea..eb8866c4bc 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -1,19 +1,65 @@ export 'package:jiffy/jiffy.dart'; -export 'package:photo_manager/photo_manager.dart' - show ThumbnailSize, ThumbnailFormat; +export 'package:photo_manager/photo_manager.dart' show ThumbnailSize, ThumbnailFormat; export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +export 'package:stream_core_flutter/stream_core_flutter.dart' + show + StreamAvatarGroupSize, + StreamAvatarSize, + StreamAvatarStackSize, + StreamButtonThemeStyle, + StreamCheckboxStyle, + StreamProgressBarStyle, + StreamPlaybackSpeedToggleStyle, + StreamAudioWaveformSlider, + StreamAudioWaveform, + StreamTheme, + StreamIcons, + StreamImageSourceBadge, + StreamThemeExtension, + StreamComponentFactory, + StreamComponentBuilder, + StreamComponentBuilders, + StreamComponentBuilderExtension, + StreamContextMenu, + StreamContextMenuAction, + StreamContextMenuSeparator, + StreamEmoji, + StreamEmojiButton, + StreamEmojiChipBar, + StreamEmojiChipItem, + StreamEmojiContent, + StreamEmojiData, + StreamEmojiPickerSheet, + StreamEmojiSize, + StreamImageEmoji, + StreamMessageAlignment, + StreamMessageLayout, + StreamMessageStackPosition, + StreamMessageChannelKind, + StreamMessageListKind, + StreamMessageContentKind, + StreamMessageText, + StreamPlaybackSpeedToggle, + StreamPlaybackSpeed, + StreamReactionPicker, + StreamReactionPickerItem, + StreamReactionPickerProps, + StreamReactionPickerTheme, + StreamReactionPickerThemeData, + StreamReactionsPosition, + StreamReactionsType, + StreamUnicodeEmoji, + streamSupportedEmojis; export 'src/ai_assistant/ai_typing_indicator_view.dart'; 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'; +export 'src/attachment/link_preview_attachment.dart'; export 'src/attachment/stream_attachment_package.dart'; export 'src/attachment/thumbnail/file_attachment_thumbnail.dart'; export 'src/attachment/thumbnail/giphy_attachment_thumbnail.dart'; @@ -21,14 +67,11 @@ export 'src/attachment/thumbnail/image_attachment_thumbnail.dart'; export 'src/attachment/thumbnail/media_attachment_thumbnail.dart'; export 'src/attachment/thumbnail/thumbnail_error.dart'; export 'src/attachment/thumbnail/video_attachment_thumbnail.dart'; -export 'src/attachment/url_attachment.dart'; export 'src/attachment/video_attachment.dart'; export 'src/attachment/voice_recording_attachment_playlist.dart'; export 'src/attachment_actions_modal/attachment_actions_modal.dart'; export 'src/autocomplete/stream_autocomplete.dart'; export 'src/avatars/gradient_avatar.dart'; -export 'src/avatars/group_avatar.dart'; -export 'src/avatars/user_avatar.dart'; export 'src/bottom_sheets/attachment_modal_sheet.dart'; export 'src/bottom_sheets/edit_message_sheet.dart'; export 'src/bottom_sheets/error_alert_sheet.dart'; @@ -37,10 +80,17 @@ export 'src/channel/channel_header.dart'; export 'src/channel/channel_info.dart'; export 'src/channel/channel_list_header.dart'; export 'src/channel/channel_name.dart'; -export 'src/channel/stream_channel_avatar.dart'; export 'src/channel/stream_channel_name.dart'; export 'src/channel/stream_draft_message_preview_text.dart'; export 'src/channel/stream_message_preview_text.dart'; +// region SDK Design Refresh Components +export 'src/components/avatar/stream_channel_avatar.dart'; +export 'src/components/avatar/stream_user_avatar.dart'; +export 'src/components/avatar/stream_user_avatar_group.dart'; +export 'src/components/avatar/stream_user_avatar_stack.dart'; +export 'src/components/message_composer/message_composer.dart'; +export 'src/components/stream_chat_component_builders.dart'; +// endregion export 'src/fullscreen_media/full_screen_media.dart'; export 'src/fullscreen_media/full_screen_media_builder.dart'; export 'src/gallery/gallery_footer.dart'; @@ -53,9 +103,13 @@ 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_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_feedback.dart'; export 'src/message_input/audio_recorder/audio_recorder_state.dart'; @@ -63,21 +117,21 @@ export 'src/message_input/audio_recorder/stream_audio_recorder.dart'; export 'src/message_input/countdown_button.dart'; export 'src/message_input/enums.dart'; export 'src/message_input/quoted_message_widget.dart'; +export 'src/message_input/stream_message_composer_attachment_list.dart'; export 'src/message_input/stream_message_input.dart'; -export 'src/message_input/stream_message_input_attachment_list.dart'; 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_widget/deleted_message.dart'; -export 'src/message_widget/message_text.dart'; +export 'src/message_list_view/unread_indicator_button.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/moderated_message_actions_modal.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'; @@ -85,7 +139,9 @@ export 'src/misc/date_divider.dart'; 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/reaction_icon_resolver.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'; @@ -98,9 +154,12 @@ 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/detail/reaction_detail_sheet.dart'; +export 'src/reactions/picker/reaction_picker.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'; +export 'src/scroll_view/channel_scroll_view/stream_channel_list_item.dart'; export 'src/scroll_view/channel_scroll_view/stream_channel_list_view.dart'; export 'src/scroll_view/draft_scroll_view/stream_draft_list_tile.dart'; export 'src/scroll_view/draft_scroll_view/stream_draft_list_view.dart'; @@ -114,6 +173,7 @@ export 'src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart'; export 'src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart'; export 'src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_tile.dart'; export 'src/scroll_view/poll_vote_scroll_view/stream_poll_vote_list_view.dart'; +export 'src/scroll_view/reaction_scroll_view/stream_reaction_list_view.dart'; export 'src/scroll_view/stream_scroll_view_empty_widget.dart'; export 'src/scroll_view/stream_scroll_view_indexed_widget_builder.dart'; export 'src/scroll_view/thread_scroll_view/stream_thread_list_tile.dart'; @@ -133,6 +193,5 @@ export 'src/utils/device_segmentation.dart'; export 'src/utils/extensions.dart'; export 'src/utils/helpers.dart'; export 'src/utils/message_preview_formatter.dart'; +export 'src/utils/stream_image_cdn.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 71a69c7d6f..6ad7c00369 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.23.0 +version: 10.0.0-beta.13 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -18,8 +18,8 @@ issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues # 2. Add it to the melos.yaml file for future updates. environment: - sdk: ^3.6.2 - flutter: ">=3.27.4" + sdk: ^3.10.0 + flutter: ">=3.38.1" dependencies: cached_network_image: ^3.3.1 @@ -48,22 +48,25 @@ dependencies: media_kit_video: ^2.0.0 meta: ^1.9.1 path_provider: ^2.1.3 - photo_manager: ^3.2.0 + photo_manager: ^3.8.3 photo_view: ^0.15.0 rate_limiter: ^1.0.0 - record: ">=5.2.0 <7.0.0" + record: ^6.2.0 rxdart: ^0.28.0 share_plus: ">=11.0.0 <13.0.0" shimmer: ^3.0.0 - stream_chat_flutter_core: ^9.23.0 + stream_chat_flutter_core: ^10.0.0-beta.13 + stream_core_flutter: ^0.1.0 svg_icon_widget: ^0.0.1 synchronized: ^3.1.0+1 + theme_extensions_builder_annotation: ^7.1.0 thumblr: ^0.0.4 url_launcher: ^6.3.0 video_player: ^2.8.7 dev_dependencies: - alchemist: ">=0.11.0 <0.14.0" + alchemist: ^0.13.0 + build_runner: ^2.4.9 connectivity_plus_platform_interface: ^2.0.0 faker_dart: ^0.2.1 flutter_test: @@ -72,6 +75,7 @@ dev_dependencies: path: ^1.8.3 path_provider_platform_interface: ^2.0.0 plugin_platform_interface: ^2.0.0 + theme_extensions_builder: ^7.2.0 flutter: assets: diff --git a/packages/stream_chat_flutter/test/conditional_parent_builder/conditional_parent_builder_test.dart b/packages/stream_chat_flutter/test/conditional_parent_builder/conditional_parent_builder_test.dart index c52c28ab4b..b029f97917 100644 --- a/packages/stream_chat_flutter/test/conditional_parent_builder/conditional_parent_builder_test.dart +++ b/packages/stream_chat_flutter/test/conditional_parent_builder/conditional_parent_builder_test.dart @@ -3,8 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/conditional_parent_builder/conditional_parent_builder.dart'; void main() { - testWidgets('ConditionalParentBuilder builds the parent widget', - (tester) async { + testWidgets('ConditionalParentBuilder builds the parent widget', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -26,8 +25,7 @@ void main() { expect(find.byType(Text), findsOneWidget); }); - testWidgets('ConditionalParentBuilder does not build the parent widget', - (tester) async { + testWidgets('ConditionalParentBuilder does not build the parent widget', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( diff --git a/packages/stream_chat_flutter/test/flutter_test_config.dart b/packages/stream_chat_flutter/test/flutter_test_config.dart index 7b00df9385..5bc88f9814 100644 --- a/packages/stream_chat_flutter/test/flutter_test_config.dart +++ b/packages/stream_chat_flutter/test/flutter_test_config.dart @@ -4,14 +4,12 @@ import 'dart:io'; import 'package:alchemist/alchemist.dart'; Future testExecutable(FutureOr Function() testMain) async { - final isRunningInCi = Platform.environment.containsKey('CI') || - Platform.environment.containsKey('GITHUB_ACTIONS'); + final isRunningInCi = Platform.environment.containsKey('CI') || Platform.environment.containsKey('GITHUB_ACTIONS'); return AlchemistConfig.runWithConfig( config: AlchemistConfig( - platformGoldensConfig: PlatformGoldensConfig( - enabled: !isRunningInCi, - ), + ciGoldensConfig: CiGoldensConfig(enabled: isRunningInCi), + platformGoldensConfig: PlatformGoldensConfig(enabled: !isRunningInCi), ), run: testMain, ); diff --git a/packages/stream_chat_flutter/test/platform_widget_builder/desktop_widget_builder_test.dart b/packages/stream_chat_flutter/test/platform_widget_builder/desktop_widget_builder_test.dart index a7b867ce38..1136d8a5d1 100644 --- a/packages/stream_chat_flutter/test/platform_widget_builder/desktop_widget_builder_test.dart +++ b/packages/stream_chat_flutter/test/platform_widget_builder/desktop_widget_builder_test.dart @@ -1,5 +1,4 @@ -import 'package:flutter/foundation.dart' - show debugDefaultTargetPlatformOverride; +import 'package:flutter/foundation.dart' show debugDefaultTargetPlatformOverride; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart'; 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..854eacf3f4 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 @@ -1,5 +1,4 @@ -import 'package:flutter/foundation.dart' - show debugDefaultTargetPlatformOverride; +import 'package:flutter/foundation.dart' show debugDefaultTargetPlatformOverride; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart'; @@ -71,4 +70,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/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart index 6e851ada5c..8bceed8ffe 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/horizontal_scrollable_positioned_list_test.dart @@ -58,119 +58,94 @@ void main() { expect(tester.getBottomRight(find.text('Item 9')).dx, screenWidth); expect(find.text('Item 10'), findsNothing); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemTrailingEdge, - 1 / 10); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemTrailingEdge, + 1 / 10, + ); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); }); testWidgets('List positioned with 0 at right', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemPositionsListener: itemPositionsListener, reverse: true); + await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener, reverse: true); expect(tester.getBottomRight(find.text('Item 0')).dx, screenWidth); expect(tester.getTopLeft(find.text('Item 9')).dx, 0); expect(find.text('Item 10'), findsNothing); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemTrailingEdge, - 1 / 10); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemTrailingEdge, + 1 / 10, + ); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); }); testWidgets('Scroll to 2 (already on screen)', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 2, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 2, duration: scrollDuration)); await tester.pump(); await tester.pump(scrollDuration); expect(find.text('Item 1'), findsNothing); expect(tester.getTopLeft(find.text('Item 2')).dx, 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 2).itemLeadingEdge, 0); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemTrailingEdge, - 1 / 10); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 2).itemTrailingEdge, + 1 / 10, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 11) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 11).itemTrailingEdge, + 1, + ); }); - testWidgets('Scroll to 100 (not already on screen)', - (WidgetTester tester) async { + testWidgets('Scroll to 100 (not already on screen)', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 99'), findsNothing); expect(find.text('Item 100'), findsOneWidget); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemTrailingEdge, - 1 / 10); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemTrailingEdge, + 1 / 10, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 109) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 109).itemTrailingEdge, + 1, + ); }); testWidgets('Jump to 100', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); itemScrollController.jumpTo(index: 100); await tester.pumpAndSettle(); @@ -179,39 +154,36 @@ void main() { expect(tester.getBottomRight(find.text('Item 109')).dy, screenHeight); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemTrailingEdge, - 1 / 10); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemTrailingEdge, + 1 / 10, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 109) - .itemLeadingEdge, - 9 / 10); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 109).itemLeadingEdge, + 9 / 10, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 109) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 109).itemTrailingEdge, + 1, + ); }); testWidgets('Scroll to 20 without fading', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); var fadeTransition = tester.widget(fadeTransitionFinder); final initialOpacity = fadeTransition.opacity; - unawaited( - itemScrollController.scrollTo(index: 20, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 20, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -225,8 +197,7 @@ void main() { expect(find.text('Item 20'), findsOneWidget); }); - testWidgets('padding test - centered sliver at left', - (WidgetTester tester) async { + testWidgets('padding test - centered sliver at left', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -235,25 +206,19 @@ void main() { ); expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); - expect(tester.getTopLeft(find.text('Item 1')), - const Offset(itemWidth + 10, 10)); - expect(tester.getBottomRight(find.text('Item 1')), - const Offset(10 + itemWidth * 2, screenHeight - 10)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(itemWidth + 10, 10)); + expect(tester.getBottomRight(find.text('Item 1')), const Offset(10 + itemWidth * 2, screenHeight - 10)); - unawaited( - itemScrollController.scrollTo(index: 490, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 490, duration: scrollDuration)); await tester.pumpAndSettle(); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(-100, 0)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(-100, 0)); await tester.pumpAndSettle(); - expect(tester.getBottomRight(find.text('Item 499')), - const Offset(screenWidth - 10, screenHeight - 10)); + expect(tester.getBottomRight(find.text('Item 499')), const Offset(screenWidth - 10, screenHeight - 10)); }); - testWidgets('padding test - centered sliver not at left', - (WidgetTester tester) async { + testWidgets('padding test - centered sliver not at left', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -262,19 +227,15 @@ void main() { padding: const EdgeInsets.all(10), ); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(200, 0)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(200, 0)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); - expect(tester.getTopLeft(find.text('Item 2')), - const Offset(10 + itemWidth * 2, 10)); - expect(tester.getTopLeft(find.text('Item 3')), - const Offset(10 + itemWidth * 3, 10)); + expect(tester.getTopLeft(find.text('Item 2')), const Offset(10 + itemWidth * 2, 10)); + expect(tester.getTopLeft(find.text('Item 3')), const Offset(10 + itemWidth * 3, 10)); }); - testWidgets('padding test - reversed - centered sliver at right', - (WidgetTester tester) async { + testWidgets('padding test - reversed - centered sliver at right', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -283,26 +244,23 @@ void main() { reverse: true, ); - expect(tester.getTopRight(find.text('Item 0')), - const Offset(screenWidth - 10, 10)); - expect(tester.getTopRight(find.text('Item 1')), - const Offset(screenWidth - (itemWidth + 10), 10)); - expect(tester.getBottomLeft(find.text('Item 1')), - const Offset(screenWidth - (10 + itemWidth * 2), screenHeight - 10)); + expect(tester.getTopRight(find.text('Item 0')), const Offset(screenWidth - 10, 10)); + expect(tester.getTopRight(find.text('Item 1')), const Offset(screenWidth - (itemWidth + 10), 10)); + expect( + tester.getBottomLeft(find.text('Item 1')), + const Offset(screenWidth - (10 + itemWidth * 2), screenHeight - 10), + ); - unawaited( - itemScrollController.scrollTo(index: 490, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 490, duration: scrollDuration)); await tester.pumpAndSettle(); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(100, 0)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(100, 0)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 499')), const Offset(10, 10)); }); - testWidgets('padding test - reversed - centered sliver not at right', - (WidgetTester tester) async { + testWidgets('padding test - reversed - centered sliver not at right', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -312,15 +270,11 @@ void main() { reverse: true, ); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(-200, 0)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(-200, 0)); await tester.pumpAndSettle(); - expect(tester.getTopRight(find.text('Item 0')), - const Offset(screenWidth - 10, 10)); - expect(tester.getTopRight(find.text('Item 2')), - const Offset(screenWidth - (10 + itemWidth * 2), 10)); - expect(tester.getTopRight(find.text('Item 3')), - const Offset(screenWidth - (10 + itemWidth * 3), 10)); + expect(tester.getTopRight(find.text('Item 0')), const Offset(screenWidth - 10, 10)); + expect(tester.getTopRight(find.text('Item 2')), const Offset(screenWidth - (10 + itemWidth * 2), 10)); + expect(tester.getTopRight(find.text('Item 3')), const Offset(screenWidth - (10 + itemWidth * 3), 10)); }); } diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart index 887165a512..c43fea6fa9 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/positioned_list_test.dart @@ -53,16 +53,11 @@ void main() { expect(find.text('Item 4'), findsOneWidget); expect(find.text('Item 5'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemTrailingEdge, - 1 / 2); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemTrailingEdge, + 1 / 2, + ); }); testWidgets('List positioned with 0 at top', (WidgetTester tester) async { @@ -73,26 +68,13 @@ void main() { expect(find.text('Item 9'), findsOneWidget); expect(find.text('Item 10'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 10).itemLeadingEdge, 1); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 10) - .itemLeadingEdge, - 1); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 10) - .itemTrailingEdge, - 11 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 10).itemTrailingEdge, + 11 / 10, + ); }); testWidgets('List positioned with 5 at top', (WidgetTester tester) async { @@ -105,25 +87,15 @@ void main() { expect(find.text('Item 15'), findsNothing); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemLeadingEdge, - -1 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemTrailingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemLeadingEdge, + -1 / 10, + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemTrailingEdge, 0); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 14) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 14).itemTrailingEdge, + 1, + ); }); testWidgets('List positioned with 20 at bottom', (WidgetTester tester) async { @@ -134,69 +106,50 @@ void main() { expect(find.text('Item 19'), findsOneWidget); expect(find.text('Item 10'), findsOneWidget); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 10).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 10) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 19) - .itemLeadingEdge, - 9 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 19) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 19).itemLeadingEdge, + 9 / 10, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemLeadingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 19).itemTrailingEdge, + 1, + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemLeadingEdge, 1); }); - testWidgets('List positioned with 20 at halfway', - (WidgetTester tester) async { + testWidgets('List positioned with 20 at halfway', (WidgetTester tester) async { await setUpWidgetTest(tester, topItem: 20, anchor: 0.5); await tester.pump(); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemLeadingEdge, - 0.5); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemLeadingEdge, + 0.5, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemTrailingEdge, - 0.5 + itemHeight / screenHeight); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemTrailingEdge, + 0.5 + itemHeight / screenHeight, + ); }); - testWidgets('List positioned with 20 half off top of screen', - (WidgetTester tester) async { - await setUpWidgetTest(tester, - topItem: 20, anchor: -(itemHeight / screenHeight) / 2); + testWidgets('List positioned with 20 half off top of screen', (WidgetTester tester) async { + await setUpWidgetTest(tester, topItem: 20, anchor: -(itemHeight / screenHeight) / 2); await tester.pump(); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemLeadingEdge, - -(itemHeight / screenHeight) / 2); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemLeadingEdge, + -(itemHeight / screenHeight) / 2, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemTrailingEdge, - (itemHeight / screenHeight) / 2); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemTrailingEdge, + (itemHeight / screenHeight) / 2, + ); }); - testWidgets('List positioned with 5 at top then scroll up 2', - (WidgetTester tester) async { + testWidgets('List positioned with 5 at top then scroll up 2', (WidgetTester tester) async { await setUpWidgetTest(tester, topItem: 5); - await tester.drag( - find.byType(PositionedList), const Offset(0, itemHeight * 2)); + await tester.drag(find.byType(PositionedList), const Offset(0, itemHeight * 2)); await tester.pump(); expect(find.text('Item 2'), findsNothing); @@ -205,44 +158,33 @@ void main() { expect(find.text('Item 13'), findsNothing); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemLeadingEdge, - -1 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 3) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 2).itemLeadingEdge, + -1 / 10, + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 3).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 12) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 12).itemTrailingEdge, + 1, + ); }); - testWidgets('List positioned with 5 at top then scroll down 1/2', - (WidgetTester tester) async { + testWidgets('List positioned with 5 at top then scroll down 1/2', (WidgetTester tester) async { await setUpWidgetTest(tester, topItem: 5); - await tester.drag( - find.byType(PositionedList), const Offset(0, -1 / 2 * itemHeight)); + await tester.drag(find.byType(PositionedList), const Offset(0, -1 / 2 * itemHeight)); await tester.pump(); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemTrailingEdge, - 1 / 20); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemTrailingEdge, + 1 / 20, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 14) - .itemLeadingEdge, - 17 / 20); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 14).itemLeadingEdge, + 17 / 20, + ); }); - testWidgets('List positioned with 0 at top scroll up 5', - (WidgetTester tester) async { + testWidgets('List positioned with 0 at top scroll up 5', (WidgetTester tester) async { final scrollController = ScrollController(); await setUpWidgetTest(tester, scrollController: scrollController); await tester.pump(); @@ -256,23 +198,16 @@ void main() { expect(find.text('Item 14'), findsOneWidget); expect(find.text('Item 15'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemLeadingEdge, - -1 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemLeadingEdge, + -1 / 10, + ); }); - testWidgets('List positioned with 5 at top then scroll up 2 programatically', - (WidgetTester tester) async { + testWidgets('List positioned with 5 at top then scroll up 2 programatically', (WidgetTester tester) async { final scrollController = ScrollController(); - await setUpWidgetTest(tester, - topItem: 5, scrollController: scrollController); + await setUpWidgetTest(tester, topItem: 5, scrollController: scrollController); scrollController.jumpTo(-2 * itemHeight); await tester.pump(); @@ -283,92 +218,67 @@ void main() { expect(find.text('Item 13'), findsNothing); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemLeadingEdge, - -1 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 3) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 2).itemLeadingEdge, + -1 / 10, + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 3).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 12) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 12).itemTrailingEdge, + 1, + ); }); - testWidgets( - 'List positioned with 5 at top then scroll down 20 programatically', - (WidgetTester tester) async { + testWidgets('List positioned with 5 at top then scroll down 20 programatically', (WidgetTester tester) async { final scrollController = ScrollController(); - await setUpWidgetTest(tester, - topItem: 5, scrollController: scrollController); + await setUpWidgetTest(tester, topItem: 5, scrollController: scrollController); scrollController.jumpTo(itemHeight * 20); await tester.pump(); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 23) - .itemLeadingEdge, - -2 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 24) - .itemLeadingEdge, - -1 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 23).itemLeadingEdge, + -2 / 10, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 25) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 24).itemLeadingEdge, + -1 / 10, + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 25).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemLeadingEdge, - -21 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemLeadingEdge, + -21 / 10, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - -20 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, + -20 / 10, + ); }); - testWidgets('List positioned with 5 at top and initial scroll offset', - (WidgetTester tester) async { - final scrollController = - ScrollController(initialScrollOffset: -2 * itemHeight); - await setUpWidgetTest(tester, - topItem: 5, scrollController: scrollController); + testWidgets('List positioned with 5 at top and initial scroll offset', (WidgetTester tester) async { + final scrollController = ScrollController(initialScrollOffset: -2 * itemHeight); + await setUpWidgetTest(tester, topItem: 5, scrollController: scrollController); expect(find.text('Item 2'), findsNothing); expect(find.text('Item 3'), findsOneWidget); expect(find.text('Item 12'), findsOneWidget); expect(find.text('Item 13'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 3).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 3) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 12) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 12).itemTrailingEdge, + 1, + ); }); - testWidgets('Does not crash when updated offscreen', - (WidgetTester tester) async { + testWidgets('Does not crash when updated offscreen', (WidgetTester tester) async { late StateSetter setState; var updated = false; // There's 0 relayout boundaries in this subtree. - final widget = StatefulBuilder(builder: (context, stateSetter) { - setState = stateSetter; - return Positioned( + final widget = StatefulBuilder( + builder: (context, stateSetter) { + setState = stateSetter; + return Positioned( left: 0, right: 0, child: PositionedList( @@ -378,17 +288,21 @@ void main() { // RenderIndexedSemantics to the render tree. addSemanticIndexes: updated, itemBuilder: (context, index) => const SizedBox(height: itemHeight), - )); - }); + ), + ); + }, + ); - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: Overlay( - initialEntries: [ - OverlayEntry(builder: (context) => widget, maintainState: true), - ], + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Overlay( + initialEntries: [ + OverlayEntry(builder: (context) => widget, maintainState: true), + ], + ), ), - )); + ); // Insert a new opaque OverlayEntry that would prevent the first // OverlayEntry from doing re-layout. Since there's no relayout boundaries diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_positioned_list_test.dart index 828f17e390..3077126080 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_positioned_list_test.dart @@ -52,16 +52,11 @@ void main() { expect(find.text('Item 4'), findsOneWidget); expect(find.text('Item 5'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemTrailingEdge, - 1 / 2); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemTrailingEdge, + 1 / 2, + ); }); testWidgets('List positioned with 0 at bottom', (WidgetTester tester) async { @@ -72,16 +67,8 @@ void main() { expect(tester.getTopLeft(find.text('Item 9')).dy, 0); expect(find.text('Item 10'), findsNothing); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); }); testWidgets('List positioned with 5 at bottom', (WidgetTester tester) async { @@ -94,25 +81,15 @@ void main() { expect(find.text('Item 15'), findsNothing); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemLeadingEdge, - -1 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemTrailingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemLeadingEdge, + -1 / 10, + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemTrailingEdge, 0); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 14) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 14).itemTrailingEdge, + 1, + ); }); testWidgets('List positioned with 15 at bottom', (WidgetTester tester) async { @@ -134,53 +111,35 @@ void main() { expect(find.text('Item 5'), findsOneWidget); expect(find.text('Item 4'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 15).itemLeadingEdge, 1); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 15) - .itemLeadingEdge, - 1); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 14) - .itemTrailingEdge, - 1); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 14) - .itemLeadingEdge, - 9 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 14).itemTrailingEdge, + 1, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 14).itemLeadingEdge, + 9 / 10, + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, 0); }); - testWidgets('List positioned with 5 at bottom then scroll up 2', - (WidgetTester tester) async { + testWidgets('List positioned with 5 at bottom then scroll up 2', (WidgetTester tester) async { await setUpWidgetTest(tester, topItem: 5); - await tester.drag( - find.byType(PositionedList), const Offset(0, itemHeight * 2)); + await tester.drag(find.byType(PositionedList), const Offset(0, itemHeight * 2)); await tester.pump(); expect(find.text('Item 6'), findsNothing); expect(find.text('Item 7'), findsOneWidget); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 7).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 7) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 7) - .itemTrailingEdge, - 1 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 7).itemTrailingEdge, + 1 / 10, + ); }); - testWidgets('List positioned with 0 at bottom scroll to item 5', - (WidgetTester tester) async { + testWidgets('List positioned with 0 at bottom scroll to item 5', (WidgetTester tester) async { final scrollController = ScrollController(); await setUpWidgetTest(tester, scrollController: scrollController); await tester.pump(); @@ -194,24 +153,16 @@ void main() { expect(find.text('Item 14'), findsOneWidget); expect(find.text('Item 15'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemLeadingEdge, - -1 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemLeadingEdge, + -1 / 10, + ); }); - testWidgets( - 'List positioned with 5 at bottom then scroll up 2 programatically', - (WidgetTester tester) async { + testWidgets('List positioned with 5 at bottom then scroll up 2 programatically', (WidgetTester tester) async { final scrollController = ScrollController(); - await setUpWidgetTest(tester, - topItem: 5, scrollController: scrollController); + await setUpWidgetTest(tester, topItem: 5, scrollController: scrollController); scrollController.jumpTo(itemHeight * 2); await tester.pump(); @@ -222,28 +173,19 @@ void main() { expect(find.text('Item 17'), findsNothing); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 6) - .itemLeadingEdge, - -1 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 7) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 6).itemLeadingEdge, + -1 / 10, + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 7).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 16) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 16).itemTrailingEdge, + 1, + ); }); - testWidgets('List positioned with 5 at bottom and initial scroll offset', - (WidgetTester tester) async { - final scrollController = - ScrollController(initialScrollOffset: itemHeight * 2); - await setUpWidgetTest(tester, - topItem: 5, scrollController: scrollController); + testWidgets('List positioned with 5 at bottom and initial scroll offset', (WidgetTester tester) async { + final scrollController = ScrollController(initialScrollOffset: itemHeight * 2); + await setUpWidgetTest(tester, topItem: 5, scrollController: scrollController); expect(find.text('Item 6'), findsNothing); expect(find.text('Item 7'), findsOneWidget); @@ -251,19 +193,13 @@ void main() { expect(find.text('Item 17'), findsNothing); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 6) - .itemLeadingEdge, - -1 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 7) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 6).itemLeadingEdge, + -1 / 10, + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 7).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 16) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 16).itemTrailingEdge, + 1, + ); }); } diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart index e2b16d8a4d..ddd1971659 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/reversed_scrollable_positioned_list_test.dart @@ -51,122 +51,95 @@ void main() { expect(tester.getTopLeft(find.text('Item 9')).dy, 0); expect(find.text('Item 10'), findsNothing); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); }); - testWidgets('Scroll to 1 then 2 (both already on screen)', - (WidgetTester tester) async { + testWidgets('Scroll to 1 then 2 (both already on screen)', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 1, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 1, duration: scrollDuration)); await tester.pump(); await tester.pump(scrollDuration); expect(find.text('Item 0'), findsNothing); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 1) - .itemLeadingEdge, - 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 1).itemLeadingEdge, 0); expect(tester.getBottomRight(find.text('Item 1')).dy, screenHeight); - unawaited( - itemScrollController.scrollTo(index: 2, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 2, duration: scrollDuration)); await tester.pump(); await tester.pump(scrollDuration); expect(find.text('Item 1'), findsNothing); expect(tester.getBottomRight(find.text('Item 2')).dy, screenHeight); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 2).itemLeadingEdge, 0); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 11) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 11).itemTrailingEdge, + 1, + ); }); - testWidgets('Scroll to 5 (already on screen) and then back to 0', - (WidgetTester tester) async { + testWidgets('Scroll to 5 (already on screen) and then back to 0', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 5, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 5, duration: scrollDuration)); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 0'), findsOneWidget); expect(find.text('Item 9'), findsOneWidget); expect(find.text('Item 10'), findsNothing); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); }); - testWidgets('Scroll to 100 (not already on screen)', - (WidgetTester tester) async { + testWidgets('Scroll to 100 (not already on screen)', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 99'), findsNothing); expect(find.text('Item 100'), findsOneWidget); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 109) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 109).itemTrailingEdge, + 1, + ); }); testWidgets('Jump to 100', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); itemScrollController.jumpTo(index: 100); await tester.pumpAndSettle(); @@ -175,19 +148,16 @@ void main() { expect(tester.getTopLeft(find.text('Item 109')).dy, 0); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 109) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 109).itemTrailingEdge, + 1, + ); }); - testWidgets('padding test - centered sliver at bottom', - (WidgetTester tester) async { + testWidgets('padding test - centered sliver at bottom', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -195,26 +165,23 @@ void main() { padding: const EdgeInsets.all(10), ); - expect(tester.getBottomLeft(find.text('Item 0')), - const Offset(10, screenHeight - 10)); - expect(tester.getBottomLeft(find.text('Item 1')), - const Offset(10, screenHeight - (itemHeight + 10))); - expect(tester.getTopRight(find.text('Item 1')), - const Offset(screenWidth - 10, screenHeight - (10 + itemHeight * 2))); + expect(tester.getBottomLeft(find.text('Item 0')), const Offset(10, screenHeight - 10)); + expect(tester.getBottomLeft(find.text('Item 1')), const Offset(10, screenHeight - (itemHeight + 10))); + expect( + tester.getTopRight(find.text('Item 1')), + const Offset(screenWidth - 10, screenHeight - (10 + itemHeight * 2)), + ); - unawaited( - itemScrollController.scrollTo(index: 490, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 490, duration: scrollDuration)); await tester.pumpAndSettle(); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, 100)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, 100)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 499')), const Offset(10, 10)); }); - testWidgets('padding test - centered sliver not at bottom', - (WidgetTester tester) async { + testWidgets('padding test - centered sliver not at bottom', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -223,15 +190,11 @@ void main() { padding: const EdgeInsets.all(10), ); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, -200)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, -200)); await tester.pumpAndSettle(); - expect(tester.getBottomLeft(find.text('Item 0')), - const Offset(10, screenHeight - 10)); - expect(tester.getBottomLeft(find.text('Item 2')), - const Offset(10, screenHeight - (10 + itemHeight * 2))); - expect(tester.getBottomLeft(find.text('Item 3')), - const Offset(10, screenHeight - (10 + itemHeight * 3))); + expect(tester.getBottomLeft(find.text('Item 0')), const Offset(10, screenHeight - 10)); + expect(tester.getBottomLeft(find.text('Item 2')), const Offset(10, screenHeight - (10 + itemHeight * 2))); + expect(tester.getBottomLeft(find.text('Item 3')), const Offset(10, screenHeight - (10 + itemHeight * 3))); }); } diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart index d3c19b2f0f..baa5196c49 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/scrollable_positioned_list_test.dart @@ -52,8 +52,7 @@ void main() { 'index must be in the range of 0 to itemCount - 1', ); return SizedBox( - height: - variableHeight ? (itemHeight + (index % 13) * 5) : itemHeight, + height: variableHeight ? (itemHeight + (index % 13) * 5) : itemHeight, child: Text('Item $index'), ); }, @@ -85,24 +84,12 @@ void main() { expect(find.text('Item 9'), findsOneWidget); expect(find.text('Item 10'), findsNothing); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); - expect( - itemPositionsListener.itemPositions.value - .where((position) => position.index == 10), - isEmpty); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); + expect(itemPositionsListener.itemPositions.value.where((position) => position.index == 10), isEmpty); }); - testWidgets('List positioned with 0 at top - use default values', - (WidgetTester tester) async { + testWidgets('List positioned with 0 at top - use default values', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); tester.view.devicePixelRatio = 1.0; tester.view.physicalSize = const Size(screenWidth, screenHeight); @@ -124,88 +111,68 @@ void main() { expect(find.text('Item 9'), findsOneWidget); expect(find.text('Item 10'), findsNothing); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); }); testWidgets('List positioned with 5 at top', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemPositionsListener: itemPositionsListener, initialIndex: 5); + await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener, initialIndex: 5); expect(find.text('Item 4'), findsNothing); expect(find.text('Item 5'), findsOneWidget); - expect( - itemPositionsListener.itemPositions.value - .where((position) => position.index == 4), - isEmpty); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - 0); + expect(itemPositionsListener.itemPositions.value.where((position) => position.index == 4), isEmpty); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, 0); }); testWidgets('List positioned with 9 at middle', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemPositionsListener: itemPositionsListener, - initialIndex: 9, - initialAlignment: 0.5); + await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener, initialIndex: 9, initialAlignment: 0.5); expect(tester.getTopLeft(find.text('Item 9')).dy, screenHeight / 2); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemLeadingEdge, - 0.5); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemLeadingEdge, + 0.5, + ); }); - testWidgets('List positioned with 9 half way off top', - (WidgetTester tester) async { + testWidgets('List positioned with 9 half way off top', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemPositionsListener: itemPositionsListener, - initialIndex: 9, - initialAlignment: -(itemHeight / screenHeight) / 2); + await setUpWidgetTest( + tester, + itemPositionsListener: itemPositionsListener, + initialIndex: 9, + initialAlignment: -(itemHeight / screenHeight) / 2, + ); expect(tester.getTopLeft(find.text('Item 9')).dy, -itemHeight / 2); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemLeadingEdge, - -(itemHeight / screenHeight) / 2); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemLeadingEdge, + -(itemHeight / screenHeight) / 2, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - (itemHeight / screenHeight) / 2); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, + (itemHeight / screenHeight) / 2, + ); }); testWidgets('Scroll to 9 half way off top', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); final itemScrollController = ItemScrollController(); expect(itemScrollController.isAttached, false); - await setUpWidgetTest(tester, - itemPositionsListener: itemPositionsListener, - itemScrollController: itemScrollController); + await setUpWidgetTest( + tester, + itemPositionsListener: itemPositionsListener, + itemScrollController: itemScrollController, + ); expect(itemScrollController.isAttached, true); - unawaited(itemScrollController.scrollTo( - index: 9, - duration: scrollDuration, - alignment: -(itemHeight / screenHeight) / 2)); + unawaited( + itemScrollController.scrollTo(index: 9, duration: scrollDuration, alignment: -(itemHeight / screenHeight) / 2), + ); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); @@ -213,54 +180,51 @@ void main() { expect(tester.getTopLeft(find.text('Item 9')).dy, -itemHeight / 2); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemLeadingEdge, - -(itemHeight / screenHeight) / 2); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemLeadingEdge, + -(itemHeight / screenHeight) / 2, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - (itemHeight / screenHeight) / 2); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, + (itemHeight / screenHeight) / 2, + ); }); testWidgets('Jump to 9 half way off top', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); final itemScrollController = ItemScrollController(); - await setUpWidgetTest(tester, - itemPositionsListener: itemPositionsListener, - itemScrollController: itemScrollController); + await setUpWidgetTest( + tester, + itemPositionsListener: itemPositionsListener, + itemScrollController: itemScrollController, + ); - itemScrollController.jumpTo( - index: 9, alignment: -(itemHeight / screenHeight) / 2); + itemScrollController.jumpTo(index: 9, alignment: -(itemHeight / screenHeight) / 2); await tester.pump(); expect(tester.getTopLeft(find.text('Item 9')).dy, -itemHeight / 2); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemLeadingEdge, - -(itemHeight / screenHeight) / 2); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemLeadingEdge, + -(itemHeight / screenHeight) / 2, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - (itemHeight / screenHeight) / 2); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, + (itemHeight / screenHeight) / 2, + ); }); - testWidgets('List positioned with 9 at middle scroll to 15 at bottom', - (WidgetTester tester) async { + testWidgets('List positioned with 9 at middle scroll to 15 at bottom', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener, - initialIndex: 9, - initialAlignment: 0.5); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + initialIndex: 9, + initialAlignment: 0.5, + ); - unawaited(itemScrollController.scrollTo( - index: 16, duration: scrollDuration, alignment: 1)); + unawaited(itemScrollController.scrollTo(index: 16, duration: scrollDuration, alignment: 1)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); @@ -268,21 +232,21 @@ void main() { expect(tester.getBottomRight(find.text('Item 15')).dy, screenHeight); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 15) - .itemTrailingEdge, - 1.0); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 15).itemTrailingEdge, + 1.0, + ); }); testWidgets('Scroll to 1 (already on screen)', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 1, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 1, duration: scrollDuration)); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -295,83 +259,61 @@ void main() { expect(find.text('Item 1'), findsOneWidget); expect(find.text('Item 10'), findsOneWidget); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 1).itemLeadingEdge, 0); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 1) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 10) - .itemTrailingEdge, - 1); - expect( - itemPositionsListener.itemPositions.value - .where((position) => position.index == 11), - isEmpty); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 10).itemTrailingEdge, + 1, + ); + expect(itemPositionsListener.itemPositions.value.where((position) => position.index == 11), isEmpty); }); - testWidgets('Scroll to 1 then 2 (both already on screen)', - (WidgetTester tester) async { + testWidgets('Scroll to 1 then 2 (both already on screen)', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 1, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 1, duration: scrollDuration)); await tester.pump(); await tester.pump(scrollDuration); expect(find.text('Item 0'), findsNothing); expect(find.text('Item 1'), findsOneWidget); - unawaited( - itemScrollController.scrollTo(index: 2, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 2, duration: scrollDuration)); await tester.pump(); await tester.pump(scrollDuration); expect(find.text('Item 1'), findsNothing); expect(find.text('Item 2'), findsOneWidget); + expect(itemPositionsListener.itemPositions.value.where((position) => position.index == 1), isEmpty); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 2).itemLeadingEdge, 0); expect( - itemPositionsListener.itemPositions.value - .where((position) => position.index == 1), - isEmpty); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 11) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 11).itemTrailingEdge, + 1, + ); }); - testWidgets('Scroll to 5 (already on screen) and then back to 0', - (WidgetTester tester) async { + testWidgets('Scroll to 5 (already on screen) and then back to 0', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 5, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 5, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, 0); - unawaited( - itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); @@ -380,30 +322,23 @@ void main() { expect(find.text('Item 9'), findsOneWidget); expect(find.text('Item 10'), findsNothing); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); }); testWidgets('Scroll to 20 without fading', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); var fadeTransition = tester.widget(fadeTransitionFinder); final initialOpacity = fadeTransition.opacity; - unawaited( - itemScrollController.scrollTo(index: 20, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 20, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -417,16 +352,16 @@ void main() { expect(find.text('Item 20'), findsOneWidget); }); - testWidgets('Scroll to 100 (not already on screen)', - (WidgetTester tester) async { + testWidgets('Scroll to 100 (not already on screen)', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); @@ -435,26 +370,22 @@ void main() { expect(find.text('Item 100'), findsOneWidget); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 109) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 109).itemTrailingEdge, + 1, + ); await tester.pumpAndSettle(); }); - testWidgets('Scroll to 100 (not already on screen) front scroll view', - (WidgetTester tester) async { + testWidgets('Scroll to 100 (not already on screen) front scroll view', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); expect(fadeTransitionFinder.evaluate().length, 2); @@ -489,13 +420,11 @@ void main() { ); }); - testWidgets('Scroll to 100 (not already on screen) back scroll view', - (WidgetTester tester) async { + testWidgets('Scroll to 100 (not already on screen) back scroll view', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -506,23 +435,22 @@ void main() { expect(find.text('Item 25', skipOffstage: false), findsNothing); }); - testWidgets('Scroll to 100 (not already on screen) then back to 0', - (WidgetTester tester) async { + testWidgets('Scroll to 100 (not already on screen) then back to 0', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); expect(find.text('Item 0'), findsNothing); - unawaited( - itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pump(); await tester.pump(); expect( @@ -538,29 +466,18 @@ void main() { expect(find.text('Item 0'), findsOneWidget); expect(tester.getTopLeft(find.text('Item 0')).dy, 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); }); - testWidgets('Scroll to 100 then back to 0 back scroll view', - (WidgetTester tester) async { + testWidgets('Scroll to 100 then back to 0 back scroll view', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -571,19 +488,16 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Scroll to 100 then back to 0 front scroll view', - (WidgetTester tester) async { + testWidgets('Scroll to 100 then back to 0 front scroll view', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); - unawaited( - itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -602,20 +516,17 @@ void main() { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); - unawaited( - itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -629,9 +540,11 @@ void main() { testWidgets('Jump to 100', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); itemScrollController.jumpTo(index: 100); await tester.pump(); @@ -642,24 +555,23 @@ void main() { expect(tester.getBottomLeft(find.text('Item 109')).dy, screenHeight); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 109) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 109).itemTrailingEdge, + 1, + ); }); - testWidgets('Jump to 100 and position at bottom', - (WidgetTester tester) async { + testWidgets('Jump to 100 and position at bottom', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); itemScrollController.jumpTo(index: 100, alignment: 1); await tester.pump(); @@ -669,23 +581,20 @@ void main() { expect(tester.getBottomLeft(find.text('Item 99')).dy, screenHeight); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 99) - .itemTrailingEdge, - 1.0); - expect( - itemPositionsListener.itemPositions.value - .where((position) => position.index == 100), - isEmpty); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 99).itemTrailingEdge, + 1.0, + ); + expect(itemPositionsListener.itemPositions.value.where((position) => position.index == 100), isEmpty); }); - testWidgets('Jump to 100 and position at middle', - (WidgetTester tester) async { + testWidgets('Jump to 100 and position at middle', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); itemScrollController.jumpTo(index: 100, alignment: 0.5); await tester.pump(); @@ -695,21 +604,21 @@ void main() { expect(tester.getTopLeft(find.text('Item 100')).dy, screenHeight / 2); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0.5); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0.5, + ); }); - testWidgets('Manually scroll a significant distance, jump to 100', - (WidgetTester tester) async { + testWidgets('Manually scroll a significant distance, jump to 100', (WidgetTester tester) async { // Test for https://github.com/google/flutter.widgets/issues/144. final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener, - variableHeight: true); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + variableHeight: true, + ); final listFinder = find.byType(ScrollablePositionedList); for (var i = 0; i < 5; i += 1) { @@ -723,16 +632,16 @@ void main() { expect(tester.getTopLeft(find.text('Item 100')).dy, 0); }, skip: true); - testWidgets('Scroll to 100 and position at bottom', - (WidgetTester tester) async { + testWidgets('Scroll to 100 and position at bottom', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited(itemScrollController.scrollTo( - index: 100, alignment: 1, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, alignment: 1, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); @@ -740,26 +649,22 @@ void main() { expect(tester.getBottomLeft(find.text('Item 99')).dy, screenHeight); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 99) - .itemTrailingEdge, - 1.0); - expect( - itemPositionsListener.itemPositions.value - .where((position) => position.index == 100), - isEmpty); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 99).itemTrailingEdge, + 1.0, + ); + expect(itemPositionsListener.itemPositions.value.where((position) => position.index == 100), isEmpty); }); - testWidgets('Scroll to 100 and position at middle', - (WidgetTester tester) async { + testWidgets('Scroll to 100 and position at middle', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited(itemScrollController.scrollTo( - index: 100, alignment: 0.5, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, alignment: 0.5, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); @@ -767,22 +672,21 @@ void main() { expect(tester.getTopLeft(find.text('Item 100')).dy, screenHeight / 2); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0.5); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0.5, + ); }); - testWidgets('Scroll to 9 and position at middle', - (WidgetTester tester) async { + testWidgets('Scroll to 9 and position at middle', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited(itemScrollController.scrollTo( - index: 9, alignment: 0.5, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 9, alignment: 0.5, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); @@ -790,22 +694,21 @@ void main() { expect(tester.getTopLeft(find.text('Item 9')).dy, screenHeight / 2); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemLeadingEdge, - 0.5); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemLeadingEdge, + 0.5, + ); }); - testWidgets('Scroll up a little then jump to 100', - (WidgetTester tester) async { + testWidgets('Scroll up a little then jump to 100', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, -10)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, -10)); await tester.pumpAndSettle(); itemScrollController.jumpTo(index: 100); @@ -817,24 +720,20 @@ void main() { expect(tester.getBottomLeft(find.text('Item 109')).dy, screenHeight); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 109) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 109).itemTrailingEdge, + 1, + ); }); - testWidgets('Scroll to 100 Jump to 0 Scroll to 100', - (WidgetTester tester) async { + testWidgets('Scroll to 100 Jump to 0 Scroll to 100', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); @@ -844,8 +743,7 @@ void main() { await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -859,13 +757,11 @@ void main() { expect(tester.getBottomLeft(find.text('Item 109')).dy, screenHeight); }); - testWidgets('Scroll to 100 stop before half way', - (WidgetTester tester) async { + testWidgets('Scroll to 100 stop before half way', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2 - scrollDuration ~/ 20); @@ -884,8 +780,7 @@ void main() { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -904,12 +799,10 @@ void main() { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2 - scrollDuration ~/ 20); @@ -927,21 +820,18 @@ void main() { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2 + scrollDuration ~/ 20); expect(find.text('Item 9', skipOffstage: false), findsOneWidget); - expect(tester.getBottomLeft(find.text('Item 100')).dy, - closeTo(screenHeight, tolerance)); + expect(tester.getBottomLeft(find.text('Item 100')).dy, closeTo(screenHeight, tolerance)); await tester.tap(find.byType(ScrollablePositionedList)); await tester.pump(); - expect(tester.getBottomLeft(find.text('Item 100')).dy, - closeTo(screenHeight, tolerance)); + expect(tester.getBottomLeft(find.text('Item 100')).dy, closeTo(screenHeight, tolerance)); expect(find.text('Item 9', skipOffstage: false), findsNothing); expect(fadeTransitionFinder, findsNWidgets(1)); @@ -952,12 +842,10 @@ void main() { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2 + scrollDuration ~/ 20); @@ -976,12 +864,10 @@ void main() { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -995,13 +881,11 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Scroll to 100 jump to 250 half way', - (WidgetTester tester) async { + testWidgets('Scroll to 100 jump to 250 half way', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -1016,17 +900,14 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Scroll to 250, scroll to 100, jump to 0 half way', - (WidgetTester tester) async { + testWidgets('Scroll to 250, scroll to 100, jump to 0 half way', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 250, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 250, duration: scrollDuration)); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -1040,38 +921,32 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Scroll to 100 scroll to 250 half way', - (WidgetTester tester) async { + testWidgets('Scroll to 100 scroll to 250 half way', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); - unawaited( - itemScrollController.scrollTo(index: 250, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 250, duration: scrollDuration)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 250')).dy, 0); expect(find.text('Item 100'), findsNothing); }); - testWidgets("Second scroll future doesn't complete until scroll is done", - (WidgetTester tester) async { + testWidgets("Second scroll future doesn't complete until scroll is done", (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); - final scrollFuture2 = - itemScrollController.scrollTo(index: 250, duration: scrollDuration); + final scrollFuture2 = itemScrollController.scrollTo(index: 250, duration: scrollDuration); var futureComplete = false; unawaited(scrollFuture2.then((_) => futureComplete = true)); @@ -1087,42 +962,33 @@ void main() { expect(futureComplete, isTrue); }); - testWidgets('Scroll to 250, scroll to 100, scroll to 0 half way', - (WidgetTester tester) async { + testWidgets('Scroll to 250, scroll to 100, scroll to 0 half way', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 250, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 250, duration: scrollDuration)); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); - unawaited( - itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 0')).dy, 0); expect(find.text('Item 100'), findsNothing); }); - testWidgets( - 'Scroll to 100, scroll to 200, then scroll to 300 without waiting', - (WidgetTester tester) async { + testWidgets('Scroll to 100, scroll to 200, then scroll to 300 without waiting', (WidgetTester tester) async { // Possibly https://github.com/google/flutter.widgets/issues/171. final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); - unawaited( - itemScrollController.scrollTo(index: 200, duration: scrollDuration)); - unawaited( - itemScrollController.scrollTo(index: 300, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 200, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 300, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 100'), findsNothing); @@ -1138,9 +1004,11 @@ void main() { (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); itemScrollController.jumpTo(index: 400, alignment: 1); await tester.pumpAndSettle(); @@ -1150,12 +1018,10 @@ void main() { await tester.drag(listFinder, const Offset(0, -screenHeight)); await tester.pumpAndSettle(); - unawaited(itemScrollController.scrollTo( - index: 100, alignment: 1, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, alignment: 1, duration: scrollDuration)); await tester.pumpAndSettle(); - unawaited(itemScrollController.scrollTo( - index: 400, alignment: 1, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 400, alignment: 1, duration: scrollDuration)); await tester.pumpAndSettle(); final itemFinder = find.text('Item 399'); @@ -1166,12 +1032,9 @@ void main() { testWidgets('physics', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - physics: const BouncingScrollPhysics()); + await setUpWidgetTest(tester, itemScrollController: itemScrollController, physics: const BouncingScrollPhysics()); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, 50)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, 50)); await tester.pump(const Duration(milliseconds: 200)); expect(tester.getTopLeft(find.text('Item 0')).dy, greaterThan(0)); @@ -1179,14 +1042,12 @@ void main() { await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 0')).dy, 0); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); itemScrollController.jumpTo(index: 0); await tester.pumpAndSettle(); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, 50)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, 50)); await tester.pump(const Duration(milliseconds: 200)); expect(tester.getTopLeft(find.text('Item 0')).dy, greaterThan(0)); @@ -1197,47 +1058,51 @@ void main() { testWidgets('correct index sematics', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, initialIndex: 5); + await setUpWidgetTest(tester, itemScrollController: itemScrollController, initialIndex: 5); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, itemHeight * 2)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, itemHeight * 2)); await tester.pumpAndSettle(); - final indexSemantics3 = tester.widget(find.ancestor( - of: find.text('Item 3'), matching: find.byType(IndexedSemantics))); + final indexSemantics3 = tester.widget( + find.ancestor(of: find.text('Item 3'), matching: find.byType(IndexedSemantics)), + ); expect(indexSemantics3.index, 3); - final indexSemantics4 = tester.widget(find.ancestor( - of: find.text('Item 4'), matching: find.byType(IndexedSemantics))); + final indexSemantics4 = tester.widget( + find.ancestor(of: find.text('Item 4'), matching: find.byType(IndexedSemantics)), + ); expect(indexSemantics4.index, 4); - final indexSemantics5 = tester.widget(find.ancestor( - of: find.text('Item 5'), matching: find.byType(IndexedSemantics))); + final indexSemantics5 = tester.widget( + find.ancestor(of: find.text('Item 5'), matching: find.byType(IndexedSemantics)), + ); expect(indexSemantics5.index, 5); - final indexSemantics6 = tester.widget(find.ancestor( - of: find.text('Item 6'), matching: find.byType(IndexedSemantics))); + final indexSemantics6 = tester.widget( + find.ancestor(of: find.text('Item 6'), matching: find.byType(IndexedSemantics)), + ); expect(indexSemantics6.index, 6); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); itemScrollController.jumpTo(index: 0); await tester.pumpAndSettle(); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, itemHeight * 2)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, itemHeight * 2)); await tester.pumpAndSettle(); - final indexSemantics3b = tester.widget(find.ancestor( - of: find.text('Item 3'), matching: find.byType(IndexedSemantics))); + final indexSemantics3b = tester.widget( + find.ancestor(of: find.text('Item 3'), matching: find.byType(IndexedSemantics)), + ); expect(indexSemantics3b.index, 3); - final indexSemantics4b = tester.widget(find.ancestor( - of: find.text('Item 4'), matching: find.byType(IndexedSemantics))); + final indexSemantics4b = tester.widget( + find.ancestor(of: find.text('Item 4'), matching: find.byType(IndexedSemantics)), + ); expect(indexSemantics4b.index, 4); - final indexSemantics5b = tester.widget(find.ancestor( - of: find.text('Item 5'), matching: find.byType(IndexedSemantics))); + final indexSemantics5b = tester.widget( + find.ancestor(of: find.text('Item 5'), matching: find.byType(IndexedSemantics)), + ); expect(indexSemantics5b.index, 5); - final indexSemantics6b = tester.widget(find.ancestor( - of: find.text('Item 6'), matching: find.byType(IndexedSemantics))); + final indexSemantics6b = tester.widget( + find.ancestor(of: find.text('Item 6'), matching: find.byType(IndexedSemantics)), + ); expect(indexSemantics6b.index, 6); }); @@ -1252,8 +1117,7 @@ void main() { expect(find.byType(IndexedSemantics), findsNothing); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); itemScrollController.jumpTo(index: 0); await tester.pumpAndSettle(); @@ -1270,16 +1134,13 @@ void main() { itemScrollController: itemScrollController, ); - final customScrollView = - tester.widget(find.byType(UnboundedCustomScrollView)); + final customScrollView = tester.widget(find.byType(UnboundedCustomScrollView)); expect(customScrollView.semanticChildCount, 30); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); - final customScrollView2 = - tester.widget(find.byType(UnboundedCustomScrollView)); + final customScrollView2 = tester.widget(find.byType(UnboundedCustomScrollView)); expect(customScrollView2.semanticChildCount, 30); }); @@ -1290,16 +1151,13 @@ void main() { itemScrollController: itemScrollController, ); - final customScrollView = - tester.widget(find.byType(UnboundedCustomScrollView)); + final customScrollView = tester.widget(find.byType(UnboundedCustomScrollView)); expect(customScrollView.semanticChildCount, defaultItemCount); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); - final customScrollView2 = - tester.widget(find.byType(UnboundedCustomScrollView)); + final customScrollView2 = tester.widget(find.byType(UnboundedCustomScrollView)); expect(customScrollView2.semanticChildCount, defaultItemCount); }); @@ -1329,25 +1187,19 @@ void main() { ); expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); - expect(tester.getTopLeft(find.text('Item 1')), - const Offset(10, itemHeight + 10)); - expect(tester.getTopRight(find.text('Item 1')), - const Offset(screenWidth - 10, itemHeight + 10)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(10, itemHeight + 10)); + expect(tester.getTopRight(find.text('Item 1')), const Offset(screenWidth - 10, itemHeight + 10)); - unawaited( - itemScrollController.scrollTo(index: 490, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 490, duration: scrollDuration)); await tester.pumpAndSettle(); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, -100)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, -100)); await tester.pumpAndSettle(); - expect(tester.getBottomRight(find.text('Item 499')), - const Offset(screenWidth - 10, screenHeight - 10)); + expect(tester.getBottomRight(find.text('Item 499')), const Offset(screenWidth - 10, screenHeight - 10)); }); - testWidgets('padding test - centered not at top', - (WidgetTester tester) async { + testWidgets('padding test - centered not at top', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -1356,19 +1208,15 @@ void main() { padding: const EdgeInsets.all(10), ); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, 200)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, 200)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); - expect(tester.getTopLeft(find.text('Item 2')), - const Offset(10, 10 + itemHeight * 2)); - expect(tester.getTopLeft(find.text('Item 3')), - const Offset(10, 10 + itemHeight * 3)); + expect(tester.getTopLeft(find.text('Item 2')), const Offset(10, 10 + itemHeight * 2)); + expect(tester.getTopLeft(find.text('Item 3')), const Offset(10, 10 + itemHeight * 3)); }); - testWidgets('padding - first element centered - scroll up', - (WidgetTester tester) async { + testWidgets('padding - first element centered - scroll up', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -1376,15 +1224,13 @@ void main() { padding: const EdgeInsets.all(10), ); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, 100)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, 100)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); }); - testWidgets('padding - last element centered - scroll down', - (WidgetTester tester) async { + testWidgets('padding - last element centered - scroll down', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -1392,12 +1238,10 @@ void main() { padding: const EdgeInsets.all(10), ); - unawaited(itemScrollController.scrollTo( - index: defaultItemCount - 1, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: defaultItemCount - 1, duration: scrollDuration)); await tester.pumpAndSettle(); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, -100)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, -100)); await tester.pumpAndSettle(); expect( @@ -1417,12 +1261,13 @@ void main() { ); expect( - tester - .widgetList(find.descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(RepaintBoundary))) - .length, - lessThan(5)); + tester + .widgetList( + find.descendant(of: find.byType(ScrollablePositionedList), matching: find.byType(RepaintBoundary)), + ) + .length, + lessThan(5), + ); }); testWidgets('no automatic keep alives', (WidgetTester tester) async { @@ -1436,10 +1281,9 @@ void main() { ); expect( - find.descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(AutomaticKeepAlive)), - findsNothing); + find.descendant(of: find.byType(ScrollablePositionedList), matching: find.byType(AutomaticKeepAlive)), + findsNothing, + ); }); testWidgets('Jump to end of list', (WidgetTester tester) async { @@ -1449,61 +1293,49 @@ void main() { itemScrollController.jumpTo(index: defaultItemCount - 1); await tester.pumpAndSettle(); - expect(tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')).dy, - screenHeight); + expect(tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')).dy, screenHeight); }); testWidgets('Scroll to end of list', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited(itemScrollController.scrollTo( - index: defaultItemCount - 1, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: defaultItemCount - 1, duration: scrollDuration)); await tester.pumpAndSettle(); - expect(tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')).dy, - screenHeight); + expect(tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')).dy, screenHeight); }); - testWidgets('Scroll to end of list, jump to beginning, jump to end', - (WidgetTester tester) async { + testWidgets('Scroll to end of list, jump to beginning, jump to end', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); - unawaited(itemScrollController.scrollTo( - index: defaultItemCount - 1, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: defaultItemCount - 1, duration: scrollDuration)); await tester.pumpAndSettle(); itemScrollController.jumpTo(index: 0); await tester.pumpAndSettle(); itemScrollController.jumpTo(index: defaultItemCount - 1); await tester.pumpAndSettle(); - expect(tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')).dy, - screenHeight); + expect(tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')).dy, screenHeight); }); - testWidgets('Jump to end of list, scroll to beginning, scroll to end', - (WidgetTester tester) async { + testWidgets('Jump to end of list, scroll to beginning, scroll to end', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); itemScrollController.jumpTo(index: defaultItemCount - 1); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pumpAndSettle(); - unawaited(itemScrollController.scrollTo( - index: defaultItemCount - 1, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: defaultItemCount - 1, duration: scrollDuration)); await tester.pumpAndSettle(); - expect(tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')).dy, - screenHeight); + expect(tester.getBottomLeft(find.text('Item ${defaultItemCount - 1}')).dy, screenHeight); }); - testWidgets( - 'Jump to end of list, jump to beginning with alignment not at top', - (WidgetTester tester) async { + testWidgets('Jump to end of list, jump to beginning with alignment not at top', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest(tester, itemScrollController: itemScrollController); @@ -1518,11 +1350,9 @@ void main() { testWidgets("Short list, can't scroll past end", (WidgetTester tester) async { final itemScrollController = ItemScrollController(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, itemCount: 3); + await setUpWidgetTest(tester, itemScrollController: itemScrollController, itemCount: 3); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, -10)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, -10)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 0')).dy, 0); @@ -1536,9 +1366,7 @@ void main() { expect(find.byKey(key), findsOneWidget); }); - testWidgets( - 'Maintain programmatic position (9 half way off top) in page view', - (WidgetTester tester) async { + testWidgets('Maintain programmatic position (9 half way off top) in page view', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); final itemScrollController = ItemScrollController(); @@ -1563,14 +1391,13 @@ void main() { ), const Center( child: Text('Test'), - ) + ), ], ), ), ); - itemScrollController.jumpTo( - index: 9, alignment: -(itemHeight / screenHeight) / 2); + itemScrollController.jumpTo(index: 9, alignment: -(itemHeight / screenHeight) / 2); await tester.pump(); await tester.drag(find.byType(PageView), const Offset(-500, 0)); @@ -1582,19 +1409,16 @@ void main() { expect(tester.getTopLeft(find.text('Item 9')).dy, -itemHeight / 2); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemLeadingEdge, - -(itemHeight / screenHeight) / 2); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemLeadingEdge, + -(itemHeight / screenHeight) / 2, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - (itemHeight / screenHeight) / 2); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, + (itemHeight / screenHeight) / 2, + ); }); - testWidgets('Maintain user scroll position (1 half way off top) in page view', - (WidgetTester tester) async { + testWidgets('Maintain user scroll position (1 half way off top) in page view', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); final itemScrollController = ItemScrollController(); @@ -1619,14 +1443,13 @@ void main() { ), const Center( child: Text('Test'), - ) + ), ], ), ), ); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, -itemHeight)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, -itemHeight)); await tester.pumpAndSettle(); final item0Bottom = tester.getBottomRight(find.text('Item 0')).dy; @@ -1641,15 +1464,13 @@ void main() { expect(tester.getBottomRight(find.text('Item 0')).dy, item0Bottom); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - -(itemHeight / screenHeight) / 2); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, + -(itemHeight / screenHeight) / 2, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemTrailingEdge, - (itemHeight / screenHeight) / 2); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemTrailingEdge, + (itemHeight / screenHeight) / 2, + ); }); testWidgets( @@ -1679,7 +1500,7 @@ void main() { ), const Center( child: Text('Test'), - ) + ), ], ), ), @@ -1690,8 +1511,7 @@ void main() { expect(tester.getBottomRight(find.text('Item 9')).dy, itemHeight); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, -itemHeight)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, -itemHeight)); await tester.pumpAndSettle(); final item9Bottom = tester.getBottomRight(find.text('Item 9')).dy; @@ -1706,28 +1526,24 @@ void main() { expect(tester.getBottomRight(find.text('Item 9')).dy, item9Bottom); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemLeadingEdge, - -(itemHeight / screenHeight) / 2); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemLeadingEdge, + -(itemHeight / screenHeight) / 2, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - (itemHeight / screenHeight) / 2); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, + (itemHeight / screenHeight) / 2, + ); }, ); testWidgets('List with no items', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, itemCount: 0); + await setUpWidgetTest(tester, itemScrollController: itemScrollController, itemCount: 0); expect(find.text('Item 0'), findsNothing); }); - testWidgets('Jump to 100 then set itemCount to 0', - (WidgetTester tester) async { + testWidgets('Jump to 100 then set itemCount to 0', (WidgetTester tester) async { tester.view.devicePixelRatio = 1.0; tester.view.physicalSize = const Size(screenWidth, screenHeight); @@ -1774,8 +1590,7 @@ void main() { expect(itemPositionsListener.itemPositions.value.isEmpty, isTrue); }); - testWidgets('List positioned with 100 at top then set itemCount to 100', - (WidgetTester tester) async { + testWidgets('List positioned with 100 at top then set itemCount to 100', (WidgetTester tester) async { tester.view.devicePixelRatio = 1.0; tester.view.physicalSize = const Size(screenWidth, screenHeight); @@ -1815,8 +1630,7 @@ void main() { expect(tester.getBottomLeft(find.text('Item 99')).dy, screenHeight); }); - testWidgets('List positioned with 499 at bottom then set itemCount to 100', - (WidgetTester tester) async { + testWidgets('List positioned with 499 at bottom then set itemCount to 100', (WidgetTester tester) async { tester.view.devicePixelRatio = 1.0; tester.view.physicalSize = const Size(screenWidth, screenHeight); @@ -1862,8 +1676,7 @@ void main() { expect(find.text('Item 100', skipOffstage: false), findsOneWidget); }); - testWidgets('Scroll to 20 without fading small minCacheExtent', - (WidgetTester tester) async { + testWidgets('Scroll to 20 without fading small minCacheExtent', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); await setUpWidgetTest( @@ -1876,8 +1689,7 @@ void main() { var fadeTransition = tester.widget(fadeTransitionFinder); final initialOpacity = fadeTransition.opacity; - unawaited( - itemScrollController.scrollTo(index: 20, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 20, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -1891,8 +1703,7 @@ void main() { expect(find.text('Item 20'), findsOneWidget); }); - testWidgets('Scroll to 100 without fading for large minCacheExtent', - (WidgetTester tester) async { + testWidgets('Scroll to 100 without fading for large minCacheExtent', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( @@ -1906,8 +1717,7 @@ void main() { ); final initialOpacity = fadeTransition.opacity; - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration ~/ 2); @@ -1920,8 +1730,7 @@ void main() { expect(find.text('Item 100'), findsOneWidget); }); - testWidgets('Position list when not enough above top item to fill viewport', - (WidgetTester tester) async { + testWidgets('Position list when not enough above top item to fill viewport', (WidgetTester tester) async { const alignment = 0.8; await setUpWidgetTest( @@ -1969,15 +1778,13 @@ void main() { key.value = const ValueKey('newKey'); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 100'), findsOneWidget); }); - testWidgets('Double rebuild with scroll controller', - (WidgetTester tester) async { + testWidgets('Double rebuild with scroll controller', (WidgetTester tester) async { tester.view.devicePixelRatio = 1.0; tester.view.physicalSize = const Size(screenWidth, screenHeight); final outerKey = ValueNotifier(const ValueKey('outerKey')); @@ -2019,8 +1826,7 @@ void main() { listKey.value = const ValueKey('newListKey'); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 100'), findsOneWidget); @@ -2093,15 +1899,13 @@ void main() { key.value = const ValueKey('newKey'); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 70, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 70, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 70'), findsOneWidget); }); - testWidgets('Scroll after rebuild when resusing state', - (WidgetTester tester) async { + testWidgets('Scroll after rebuild when resusing state', (WidgetTester tester) async { tester.view.devicePixelRatio = 1.0; tester.view.physicalSize = const Size(screenWidth, screenHeight); final containerKey = ValueNotifier(const ValueKey('key')); @@ -2137,15 +1941,13 @@ void main() { containerKey.value = const ValueKey('newKey'); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 70, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 70, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 70'), findsOneWidget); }); - testWidgets('Scroll after changing scroll controller', - (WidgetTester tester) async { + testWidgets('Scroll after changing scroll controller', (WidgetTester tester) async { tester.view.devicePixelRatio = 1.0; tester.view.physicalSize = const Size(screenWidth, screenHeight); @@ -2183,65 +1985,63 @@ void main() { expect(itemScrollController0.isAttached, false); expect(itemScrollController1.isAttached, true); - unawaited( - itemScrollController1.scrollTo(index: 70, duration: scrollDuration)); + unawaited(itemScrollController1.scrollTo(index: 70, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 70'), findsOneWidget); }); - testWidgets('Scroll after swapping scroll controllers', - (WidgetTester tester) async { + testWidgets('Scroll after swapping scroll controllers', (WidgetTester tester) async { tester.view.devicePixelRatio = 1.0; tester.view.physicalSize = const Size(screenWidth, screenHeight); final itemScrollController0 = ItemScrollController(); final itemScrollController1 = ItemScrollController(); - final topItemScrollControllerListenable = - ValueNotifier(itemScrollController0); - final bottomItemScrollControllerListenable = - ValueNotifier(itemScrollController1); - - await tester.pumpWidget(MaterialApp( - home: Column( - children: [ - Expanded( - child: ValueListenableBuilder( - valueListenable: topItemScrollControllerListenable, - builder: (context, itemScrollController, child) { - return ScrollablePositionedList.builder( - itemCount: 100, - itemScrollController: itemScrollController, - itemBuilder: (context, index) { - return SizedBox( - height: itemHeight, - child: Text('Item $index'), - ); - }, - ); - }, + final topItemScrollControllerListenable = ValueNotifier(itemScrollController0); + final bottomItemScrollControllerListenable = ValueNotifier(itemScrollController1); + + await tester.pumpWidget( + MaterialApp( + home: Column( + children: [ + Expanded( + child: ValueListenableBuilder( + valueListenable: topItemScrollControllerListenable, + builder: (context, itemScrollController, child) { + return ScrollablePositionedList.builder( + itemCount: 100, + itemScrollController: itemScrollController, + itemBuilder: (context, index) { + return SizedBox( + height: itemHeight, + child: Text('Item $index'), + ); + }, + ); + }, + ), ), - ), - Expanded( - child: ValueListenableBuilder( - valueListenable: bottomItemScrollControllerListenable, - builder: (context, itemScrollController, child) { - return ScrollablePositionedList.builder( - itemCount: 100, - itemScrollController: itemScrollController, - itemBuilder: (context, index) { - return SizedBox( - height: itemHeight, - child: Text('Item $index'), - ); - }, - ); - }, + Expanded( + child: ValueListenableBuilder( + valueListenable: bottomItemScrollControllerListenable, + builder: (context, itemScrollController, child) { + return ScrollablePositionedList.builder( + itemCount: 100, + itemScrollController: itemScrollController, + itemBuilder: (context, index) { + return SizedBox( + height: itemHeight, + child: Text('Item $index'), + ); + }, + ); + }, + ), ), - ), - ], + ], + ), ), - )); + ); await tester.pumpAndSettle(); expect(itemScrollController0.isAttached, true); @@ -2254,10 +2054,8 @@ void main() { expect(itemScrollController0.isAttached, true); expect(itemScrollController1.isAttached, true); - unawaited( - itemScrollController1.scrollTo(index: 70, duration: scrollDuration)); - unawaited( - itemScrollController0.scrollTo(index: 50, duration: scrollDuration)); + unawaited(itemScrollController1.scrollTo(index: 70, duration: scrollDuration)); + unawaited(itemScrollController0.scrollTo(index: 50, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 70'), findsOneWidget); diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_positioned_list_test.dart index f6dc2b5cef..f947d38bf7 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_positioned_list_test.dart @@ -68,24 +68,17 @@ void main() { expect(find.text('Separator 2'), findsNothing); expect(find.text('Item 3'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemTrailingEdge, - _screenProportion(numberOfItems: 3, numberOfSeparators: 2)); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 2).itemTrailingEdge, + _screenProportion(numberOfItems: 3, numberOfSeparators: 2), + ); }); - testWidgets('Short list centered at 1 scrolled up', - (WidgetTester tester) async { + testWidgets('Short list centered at 1 scrolled up', (WidgetTester tester) async { await setUpWidgetTest(tester, itemCount: 3, topItem: 1); - await tester.drag( - find.byType(PositionedList), const Offset(0, itemHeight * 2)); + await tester.drag(find.byType(PositionedList), const Offset(0, itemHeight * 2)); await tester.pumpAndSettle(); expect(find.text('Item 0'), findsOneWidget); @@ -96,16 +89,11 @@ void main() { expect(find.text('Separator 2'), findsNothing); expect(find.text('Item 3'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemTrailingEdge, - _screenProportion(numberOfItems: 3, numberOfSeparators: 2)); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 2).itemTrailingEdge, + _screenProportion(numberOfItems: 3, numberOfSeparators: 2), + ); }); testWidgets('List positioned with 0 at top', (WidgetTester tester) async { @@ -118,22 +106,13 @@ void main() { expect(find.text('Separator 6'), findsNothing); expect(find.text('Item 7'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemTrailingEdge, - 1 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemTrailingEdge, + 1 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1), + ); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 6) - .itemTrailingEdge, - 1); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 6).itemTrailingEdge, 1); }); testWidgets('List positioned with 5 at top', (WidgetTester tester) async { @@ -149,21 +128,15 @@ void main() { expect(find.text('Item 11'), findsOneWidget); expect(find.text('Separator 11'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 6) - .itemLeadingEdge, - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 6).itemLeadingEdge, + _screenProportion(numberOfItems: 1, numberOfSeparators: 1), + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 11) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 11).itemTrailingEdge, + 1, + ); }); testWidgets('List positioned with 20 at bottom', (WidgetTester tester) async { @@ -179,82 +152,60 @@ void main() { expect(find.text('Separator 12'), findsNothing); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 19) - .itemTrailingEdge, - 1 - _screenProportion(numberOfItems: 0, numberOfSeparators: 1)); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemLeadingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 19).itemTrailingEdge, + 1 - _screenProportion(numberOfItems: 0, numberOfSeparators: 1), + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemLeadingEdge, 1); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 13) - .itemLeadingEdge, - _screenProportion(numberOfItems: -0.5, numberOfSeparators: 0)); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 13).itemLeadingEdge, + _screenProportion(numberOfItems: -0.5, numberOfSeparators: 0), + ); }); - testWidgets('List positioned with item 20 at halfway', - (WidgetTester tester) async { + testWidgets('List positioned with item 20 at halfway', (WidgetTester tester) async { await setUpWidgetTest(tester, topItem: 20, anchor: 0.5); await tester.pump(); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemLeadingEdge, - 0.5); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemLeadingEdge, + 0.5, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemTrailingEdge, - 0.5 + itemHeight / screenHeight); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemTrailingEdge, + 0.5 + itemHeight / screenHeight, + ); }); - testWidgets('List positioned with item 20 half off top of screen', - (WidgetTester tester) async { - await setUpWidgetTest(tester, - topItem: 20, anchor: -(itemHeight / screenHeight) / 2); + testWidgets('List positioned with item 20 half off top of screen', (WidgetTester tester) async { + await setUpWidgetTest(tester, topItem: 20, anchor: -(itemHeight / screenHeight) / 2); await tester.pump(); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemLeadingEdge, - _screenProportion(numberOfItems: -0.5, numberOfSeparators: 0)); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemLeadingEdge, + _screenProportion(numberOfItems: -0.5, numberOfSeparators: 0), + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemTrailingEdge, - _screenProportion(numberOfItems: 0.5, numberOfSeparators: 0)); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemTrailingEdge, + _screenProportion(numberOfItems: 0.5, numberOfSeparators: 0), + ); }); - testWidgets('List positioned with 5 at top then scroll up 2 items', - (WidgetTester tester) async { + testWidgets('List positioned with 5 at top then scroll up 2 items', (WidgetTester tester) async { await setUpWidgetTest(tester, topItem: 5); - await tester.drag(find.byType(PositionedList), - const Offset(0, 2 * (itemHeight + separatorHeight))); + await tester.drag(find.byType(PositionedList), const Offset(0, 2 * (itemHeight + separatorHeight))); await tester.pump(); expect(find.text('Separator 2'), findsNothing); expect(find.text('Item 3'), findsOneWidget); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemLeadingEdge, - _screenProportion(numberOfItems: -1, numberOfSeparators: -1)); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 3) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 2).itemLeadingEdge, + _screenProportion(numberOfItems: -1, numberOfSeparators: -1), + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 3).itemLeadingEdge, 0); }); } -double _screenProportion( - {required double numberOfItems, required double numberOfSeparators}) => - (numberOfItems * itemHeight + numberOfSeparators * separatorHeight) / - screenHeight; +double _screenProportion({required double numberOfItems, required double numberOfSeparators}) => + (numberOfItems * itemHeight + numberOfSeparators * separatorHeight) / screenHeight; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart index d4d99595dd..d6ef7bc527 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/separated_scrollable_positioned_list_test.dart @@ -75,30 +75,17 @@ void main() { expect(find.text('Separator 6'), findsNothing); expect(find.text('Item 7'), findsNothing); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemTrailingEdge, - 1 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 5).itemTrailingEdge, + 1 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1), + ); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 6) - .itemTrailingEdge, - 1); - expect( - itemPositionsListener.itemPositions.value - .where((position) => position.index == 7), - isEmpty); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 6).itemTrailingEdge, 1); + expect(itemPositionsListener.itemPositions.value.where((position) => position.index == 7), isEmpty); }); - testWidgets('List positioned with 0 at top - use default values', - (WidgetTester tester) async { + testWidgets('List positioned with 0 at top - use default values', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); tester.view.devicePixelRatio = 1.0; tester.view.physicalSize = const Size(screenWidth, screenHeight); @@ -126,32 +113,19 @@ void main() { expect(find.text('Separator 6'), findsNothing); expect(find.text('Item 7'), findsNothing); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemTrailingEdge, - 1 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 5).itemTrailingEdge, + 1 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1), + ); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 6) - .itemTrailingEdge, - 1); - expect( - itemPositionsListener.itemPositions.value - .where((position) => position.index == 7), - isEmpty); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 6).itemTrailingEdge, 1); + expect(itemPositionsListener.itemPositions.value.where((position) => position.index == 7), isEmpty); }); testWidgets('List positioned with 5 at top', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemPositionsListener: itemPositionsListener, initialIndex: 5); + await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener, initialIndex: 5); expect(find.text('Item 4'), findsNothing); expect(find.text('Separator 4'), findsNothing); @@ -161,58 +135,44 @@ void main() { expect(find.text('Item 11'), findsOneWidget); expect(find.text('Separator 11'), findsNothing); - expect( - itemPositionsListener.itemPositions.value - .where((position) => position.index == 4), - isEmpty); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - 0); + expect(itemPositionsListener.itemPositions.value.where((position) => position.index == 4), isEmpty); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, 0); }); testWidgets('List positioned with 9 at middle', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemPositionsListener: itemPositionsListener, - initialIndex: 9, - initialAlignment: 0.5); + await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener, initialIndex: 9, initialAlignment: 0.5); expect(tester.getTopLeft(find.text('Item 9')).dy, screenHeight / 2); - expect(tester.getTopLeft(find.text('Item 8')).dy, - screenHeight / 2 - itemHeight - separatorHeight); - expect(tester.getTopLeft(find.text('Item 10')).dy, - screenHeight / 2 + itemHeight + separatorHeight); + expect(tester.getTopLeft(find.text('Item 8')).dy, screenHeight / 2 - itemHeight - separatorHeight); + expect(tester.getTopLeft(find.text('Item 10')).dy, screenHeight / 2 + itemHeight + separatorHeight); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemLeadingEdge, - 0.5); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemLeadingEdge, + 0.5, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 8) - .itemLeadingEdge, - 0.5 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 8).itemLeadingEdge, + 0.5 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1), + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 10) - .itemLeadingEdge, - 0.5 + _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 10).itemLeadingEdge, + 0.5 + _screenProportion(numberOfItems: 1, numberOfSeparators: 1), + ); }); testWidgets('Scroll to 9 half way off top', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); final itemScrollController = ItemScrollController(); - await setUpWidgetTest(tester, - itemPositionsListener: itemPositionsListener, - itemScrollController: itemScrollController); - - unawaited(itemScrollController.scrollTo( - index: 9, - duration: scrollDuration, - alignment: -(itemHeight / screenHeight) / 2)); + await setUpWidgetTest( + tester, + itemPositionsListener: itemPositionsListener, + itemScrollController: itemScrollController, + ); + + unawaited( + itemScrollController.scrollTo(index: 9, duration: scrollDuration, alignment: -(itemHeight / screenHeight) / 2), + ); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); @@ -220,76 +180,68 @@ void main() { expect(tester.getTopLeft(find.text('Item 9')).dy, -itemHeight / 2); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemLeadingEdge, - _screenProportion(numberOfItems: -0.5, numberOfSeparators: 0)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemLeadingEdge, + _screenProportion(numberOfItems: -0.5, numberOfSeparators: 0), + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - _screenProportion(numberOfItems: 0.5, numberOfSeparators: 0)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, + _screenProportion(numberOfItems: 0.5, numberOfSeparators: 0), + ); }); testWidgets('Jump to 9 half way off top', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); final itemScrollController = ItemScrollController(); - await setUpWidgetTest(tester, - itemPositionsListener: itemPositionsListener, - itemScrollController: itemScrollController); + await setUpWidgetTest( + tester, + itemPositionsListener: itemPositionsListener, + itemScrollController: itemScrollController, + ); - itemScrollController.jumpTo( - index: 9, alignment: -(itemHeight / screenHeight) / 2); + itemScrollController.jumpTo(index: 9, alignment: -(itemHeight / screenHeight) / 2); await tester.pump(); expect(tester.getTopLeft(find.text('Item 9')).dy, -itemHeight / 2); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemLeadingEdge, - _screenProportion(numberOfItems: -0.5, numberOfSeparators: 0)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemLeadingEdge, + _screenProportion(numberOfItems: -0.5, numberOfSeparators: 0), + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - _screenProportion(numberOfItems: 0.5, numberOfSeparators: 0)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, + _screenProportion(numberOfItems: 0.5, numberOfSeparators: 0), + ); }); - testWidgets('List positioned with 9 at middle scroll to 16 at bottom', - (WidgetTester tester) async { + testWidgets('List positioned with 9 at middle scroll to 16 at bottom', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener, - initialIndex: 9, - initialAlignment: 0.5); - - unawaited(itemScrollController.scrollTo( - index: 16, duration: scrollDuration, alignment: 1)); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + initialIndex: 9, + initialAlignment: 0.5, + ); + + unawaited(itemScrollController.scrollTo(index: 16, duration: scrollDuration, alignment: 1)); await tester.pump(); await tester.pump(); await tester.pump(scrollDuration + scrollDurationTolerance); - expect(tester.getBottomRight(find.text('Item 15')).dy, - screenHeight - separatorHeight); + expect(tester.getBottomRight(find.text('Item 15')).dy, screenHeight - separatorHeight); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 15) - .itemTrailingEdge, - 1 - _screenProportion(numberOfItems: 0, numberOfSeparators: 1)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 15).itemTrailingEdge, + 1 - _screenProportion(numberOfItems: 0, numberOfSeparators: 1), + ); }); testWidgets('physics', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - physics: const BouncingScrollPhysics()); + await setUpWidgetTest(tester, itemScrollController: itemScrollController, physics: const BouncingScrollPhysics()); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, 50)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, 50)); await tester.pump(const Duration(milliseconds: 200)); expect(tester.getTopLeft(find.text('Item 0')).dy, greaterThan(0)); @@ -297,14 +249,12 @@ void main() { await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 0')).dy, 0); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); itemScrollController.jumpTo(index: 0); await tester.pumpAndSettle(); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, 50)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, 50)); await tester.pump(const Duration(milliseconds: 200)); expect(tester.getTopLeft(find.text('Item 0')).dy, greaterThan(0)); @@ -316,15 +266,16 @@ void main() { testWidgets('correct index semantics', (WidgetTester tester) async { await setUpWidgetTest(tester, initialIndex: 5); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, itemHeight * 4)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, itemHeight * 4)); await tester.pumpAndSettle(); - final indexSemantics3 = tester.widget(find.ancestor( - of: find.text('Item 3'), matching: find.byType(IndexedSemantics))); + final indexSemantics3 = tester.widget( + find.ancestor(of: find.text('Item 3'), matching: find.byType(IndexedSemantics)), + ); expect(indexSemantics3.index, 3); - final indexSemantics4 = tester.widget(find.ancestor( - of: find.text('Item 4'), matching: find.byType(IndexedSemantics))); + final indexSemantics4 = tester.widget( + find.ancestor(of: find.text('Item 4'), matching: find.byType(IndexedSemantics)), + ); expect(indexSemantics4.index, 4); }); @@ -339,8 +290,7 @@ void main() { expect(find.byType(IndexedSemantics), findsNothing); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.byType(IndexedSemantics), findsNothing); @@ -355,16 +305,13 @@ void main() { itemScrollController: itemScrollController, ); - final customScrollView = - tester.widget(find.byType(UnboundedCustomScrollView)); + final customScrollView = tester.widget(find.byType(UnboundedCustomScrollView)); expect(customScrollView.semanticChildCount, 30); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); - final customScrollView2 = - tester.widget(find.byType(UnboundedCustomScrollView)); + final customScrollView2 = tester.widget(find.byType(UnboundedCustomScrollView)); expect(customScrollView2.semanticChildCount, 30); }); @@ -375,16 +322,13 @@ void main() { itemScrollController: itemScrollController, ); - final customScrollView = - tester.widget(find.byType(UnboundedCustomScrollView)); + final customScrollView = tester.widget(find.byType(UnboundedCustomScrollView)); expect(customScrollView.semanticChildCount, defaultItemCount); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); - final customScrollView2 = - tester.widget(find.byType(UnboundedCustomScrollView)); + final customScrollView2 = tester.widget(find.byType(UnboundedCustomScrollView)); expect(customScrollView2.semanticChildCount, defaultItemCount); }); @@ -397,25 +341,19 @@ void main() { ); expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); - expect(tester.getTopLeft(find.text('Item 1')), - const Offset(10, itemHeight + 10 + separatorHeight)); - expect(tester.getTopRight(find.text('Item 1')), - const Offset(screenWidth - 10, itemHeight + 10 + separatorHeight)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(10, itemHeight + 10 + separatorHeight)); + expect(tester.getTopRight(find.text('Item 1')), const Offset(screenWidth - 10, itemHeight + 10 + separatorHeight)); - unawaited( - itemScrollController.scrollTo(index: 494, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 494, duration: scrollDuration)); await tester.pumpAndSettle(); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, -500)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, -500)); await tester.pumpAndSettle(); - expect(tester.getBottomRight(find.text('Item 499')), - const Offset(screenWidth - 10, screenHeight - 10)); + expect(tester.getBottomRight(find.text('Item 499')), const Offset(screenWidth - 10, screenHeight - 10)); }); - testWidgets('padding test - centered sliver not at top', - (WidgetTester tester) async { + testWidgets('padding test - centered sliver not at top', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -424,17 +362,15 @@ void main() { padding: const EdgeInsets.all(10), ); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, 200)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, 200)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); - expect(tester.getTopLeft(find.text('Item 2')), - const Offset(10, 10 + 2 * (separatorHeight + itemHeight))); + expect(tester.getTopLeft(find.text('Item 2')), const Offset(10, 10 + 2 * (separatorHeight + itemHeight))); expect( - tester.getTopRight(find.text('Item 3')), - const Offset( - screenWidth - 10, 10 + 3 * (itemHeight + separatorHeight))); + tester.getTopRight(find.text('Item 3')), + const Offset(screenWidth - 10, 10 + 3 * (itemHeight + separatorHeight)), + ); }); testWidgets('no repaint bounderies', (WidgetTester tester) async { @@ -448,12 +384,13 @@ void main() { ); expect( - tester - .widgetList(find.descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(RepaintBoundary))) - .length, - lessThan(5)); + tester + .widgetList( + find.descendant(of: find.byType(ScrollablePositionedList), matching: find.byType(RepaintBoundary)), + ) + .length, + lessThan(5), + ); }); testWidgets('no automatic keep alives', (WidgetTester tester) async { @@ -467,10 +404,9 @@ void main() { ); expect( - find.descendant( - of: find.byType(ScrollablePositionedList), - matching: find.byType(AutomaticKeepAlive)), - findsNothing); + find.descendant(of: find.byType(ScrollablePositionedList), matching: find.byType(AutomaticKeepAlive)), + findsNothing, + ); }); testWidgets('List can be keyed', (WidgetTester tester) async { @@ -481,8 +417,7 @@ void main() { expect(find.byKey(key), findsOneWidget); }); - testWidgets('Empty list then update to single item list', - (WidgetTester tester) async { + testWidgets('Empty list then update to single item list', (WidgetTester tester) async { tester.view.devicePixelRatio = 1.0; tester.view.physicalSize = const Size(screenWidth, screenHeight); @@ -522,8 +457,7 @@ void main() { expect(find.text('Separator 0'), findsNothing); }); - testWidgets('ItemPositions: Empty list then update to 10 items list', - (WidgetTester tester) async { + testWidgets('ItemPositions: Empty list then update to 10 items list', (WidgetTester tester) async { tester.view.devicePixelRatio = 1.0; tester.view.physicalSize = const Size(screenWidth, screenHeight); @@ -570,30 +504,16 @@ void main() { expect(find.text('Item 7'), findsNothing); expect(itemPositionsListener.itemPositions.value, isNotEmpty); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemTrailingEdge, - 1 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 5).itemTrailingEdge, + 1 - _screenProportion(numberOfItems: 1, numberOfSeparators: 1), + ); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 6) - .itemTrailingEdge, - 1); - expect( - itemPositionsListener.itemPositions.value - .where((position) => position.index == 7), - isEmpty); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 6).itemTrailingEdge, 1); + expect(itemPositionsListener.itemPositions.value.where((position) => position.index == 7), isEmpty); }); } -double _screenProportion( - {required double numberOfItems, required double numberOfSeparators}) => - (numberOfItems * itemHeight + numberOfSeparators * separatorHeight) / - screenHeight; +double _screenProportion({required double numberOfItems, required double numberOfSeparators}) => + (numberOfItems * itemHeight + numberOfSeparators * separatorHeight) / screenHeight; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart index 88b0fae0ac..b0c151666e 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/seperated_horizontal_scrollable_positioned_list_test.dart @@ -56,105 +56,90 @@ void main() { await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); expect(tester.getTopLeft(find.text('Item 0')).dx, 0); - expect(tester.getBottomLeft(find.text('Item 1')).dx, - itemWidth + separatorWidth); + expect(tester.getBottomLeft(find.text('Item 1')).dx, itemWidth + separatorWidth); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 1) - .itemLeadingEdge, - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 1).itemLeadingEdge, + _screenProportion(numberOfItems: 1, numberOfSeparators: 1), + ); }); testWidgets('Scroll to 2 (already on screen)', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 2, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 2, duration: scrollDuration)); await tester.pump(); await tester.pump(scrollDuration); expect(find.text('Item 1'), findsNothing); expect(tester.getTopLeft(find.text('Item 2')).dx, 0); - expect( - tester.getTopLeft(find.text('Item 3')).dx, itemWidth + separatorWidth); + expect(tester.getTopLeft(find.text('Item 3')).dx, itemWidth + separatorWidth); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 2).itemLeadingEdge, 0); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 3) - .itemLeadingEdge, - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 3).itemLeadingEdge, + _screenProportion(numberOfItems: 1, numberOfSeparators: 1), + ); }); - testWidgets('Scroll to 100 (not already on screen)', - (WidgetTester tester) async { + testWidgets('Scroll to 100 (not already on screen)', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 99'), findsNothing); expect(find.text('Item 100'), findsOneWidget); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 101) - .itemLeadingEdge, - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 101).itemLeadingEdge, + _screenProportion(numberOfItems: 1, numberOfSeparators: 1), + ); }); testWidgets('Jump to 100', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); itemScrollController.jumpTo(index: 100); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 100')).dx, 0); - expect(tester.getTopLeft(find.text('Item 101')).dx, - itemWidth + separatorWidth); + expect(tester.getTopLeft(find.text('Item 101')).dx, itemWidth + separatorWidth); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 101) - .itemLeadingEdge, - _screenProportion(numberOfItems: 1, numberOfSeparators: 1)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 101).itemLeadingEdge, + _screenProportion(numberOfItems: 1, numberOfSeparators: 1), + ); }); - testWidgets('padding test - centered sliver at left', - (WidgetTester tester) async { + testWidgets('padding test - centered sliver at left', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -163,25 +148,22 @@ void main() { ); expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); - expect(tester.getTopLeft(find.text('Item 1')), - const Offset(itemWidth + 10 + separatorWidth, 10)); - expect(tester.getBottomRight(find.text('Item 1')), - const Offset(10 + itemWidth * 2 + separatorWidth, screenHeight - 10)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(itemWidth + 10 + separatorWidth, 10)); + expect( + tester.getBottomRight(find.text('Item 1')), + const Offset(10 + itemWidth * 2 + separatorWidth, screenHeight - 10), + ); - unawaited( - itemScrollController.scrollTo(index: 494, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 494, duration: scrollDuration)); await tester.pumpAndSettle(); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(-500, 0)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(-500, 0)); await tester.pumpAndSettle(); - expect(tester.getBottomRight(find.text('Item 499')), - const Offset(screenWidth - 10, screenHeight - 10)); + expect(tester.getBottomRight(find.text('Item 499')), const Offset(screenWidth - 10, screenHeight - 10)); }); - testWidgets('padding test - centered sliver not at left', - (WidgetTester tester) async { + testWidgets('padding test - centered sliver not at left', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); await setUpWidgetTest( @@ -192,27 +174,19 @@ void main() { padding: const EdgeInsets.all(10), ); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(300, 0)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(300, 0)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); - expect(tester.getTopLeft(find.text('Item 2')), - const Offset(10 + 2 * (itemWidth + separatorWidth), 10)); - expect(tester.getTopLeft(find.text('Item 3')), - const Offset(10 + 3 * (itemWidth + separatorWidth), 10)); + expect(tester.getTopLeft(find.text('Item 2')), const Offset(10 + 2 * (itemWidth + separatorWidth), 10)); + expect(tester.getTopLeft(find.text('Item 3')), const Offset(10 + 3 * (itemWidth + separatorWidth), 10)); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemLeadingEdge, - closeTo( - 10 / screenWidth + 2 * ((itemWidth + separatorWidth) / screenWidth), - tolerance)); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 2).itemLeadingEdge, + closeTo(10 / screenWidth + 2 * ((itemWidth + separatorWidth) / screenWidth), tolerance), + ); }); } -double _screenProportion( - {required double numberOfItems, required double numberOfSeparators}) => - (numberOfItems * itemWidth + numberOfSeparators * separatorWidth) / - screenHeight; +double _screenProportion({required double numberOfItems, required double numberOfSeparators}) => + (numberOfItems * itemWidth + numberOfSeparators * separatorWidth) / screenHeight; diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_position_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_position_list_test.dart index 8cf4303c4f..2c13c0d601 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_position_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_position_list_test.dart @@ -32,28 +32,28 @@ void main() { MaterialApp( // Use flex layout to ensure that the minimum height is not limited to // screenHeight. - home: Column(children: [ - // Use Constrained to make max height not more than screenHeight - ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: screenHeight, maxWidth: screenWidth), - child: PositionedList( - key: key, - itemCount: itemCount, - positionedIndex: topItem, - alignment: anchor, - controller: scrollController, - itemBuilder: (context, index) => SizedBox( - height: itemHeight, - child: Text('Item $index'), + home: Column( + children: [ + // Use Constrained to make max height not more than screenHeight + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: screenHeight, maxWidth: screenWidth), + child: PositionedList( + key: key, + itemCount: itemCount, + positionedIndex: topItem, + alignment: anchor, + controller: scrollController, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + itemPositionsNotifier: itemPositionsNotifier as ItemPositionsNotifier, + shrinkWrap: true, + reverse: reverse, ), - itemPositionsNotifier: - itemPositionsNotifier as ItemPositionsNotifier, - shrinkWrap: true, - reverse: reverse, ), - ), - ]), + ], + ), ), ); } @@ -64,28 +64,21 @@ void main() { await setUpWidgetTest(tester, itemCount: itemCount, key: key); await tester.pump(); - expect( - tester.getBottomRight(find.text('Item 4')).dy, itemHeight * itemCount); + expect(tester.getBottomRight(find.text('Item 4')).dy, itemHeight * itemCount); expect(find.text('Item 4'), findsOneWidget); expect(find.text('Item 5'), findsNothing); final positionList = find.byKey(key); expect(tester.getBottomRight(positionList).dy, itemHeight * itemCount); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemTrailingEdge, - 1.0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemTrailingEdge, + 1.0, + ); }); - testWidgets('List positioned with 0 at top and shrink wrap', - (WidgetTester tester) async { + testWidgets('List positioned with 0 at top and shrink wrap', (WidgetTester tester) async { await setUpWidgetTest(tester); await tester.pump(); @@ -93,30 +86,16 @@ void main() { expect(find.text('Item 9'), findsOneWidget); expect(find.text('Item 10'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 10).itemLeadingEdge, 1); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 10) - .itemLeadingEdge, - 1); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 10) - .itemTrailingEdge, - 11 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 10).itemTrailingEdge, + 11 / 10, + ); }); - testWidgets('List positioned with 5 at top and shrink wrap', - (WidgetTester tester) async { + testWidgets('List positioned with 5 at top and shrink wrap', (WidgetTester tester) async { await setUpWidgetTest(tester, topItem: 5); await tester.pump(); @@ -126,29 +105,18 @@ void main() { expect(find.text('Item 15'), findsNothing); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemLeadingEdge, - -1 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemTrailingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemLeadingEdge, + -1 / 10, + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemTrailingEdge, 0); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 14) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 14).itemTrailingEdge, + 1, + ); }); - testWidgets('List positioned with 20 at bottom and shrink wrap', - (WidgetTester tester) async { + testWidgets('List positioned with 20 at bottom and shrink wrap', (WidgetTester tester) async { await setUpWidgetTest(tester, topItem: 20, anchor: 1); await tester.pump(); @@ -156,69 +124,50 @@ void main() { expect(find.text('Item 19'), findsOneWidget); expect(find.text('Item 10'), findsOneWidget); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 10).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 10) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 19) - .itemLeadingEdge, - 9 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 19) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 19).itemLeadingEdge, + 9 / 10, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemLeadingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 19).itemTrailingEdge, + 1, + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemLeadingEdge, 1); }); - testWidgets('List positioned with 20 at halfway and shrink wrap', - (WidgetTester tester) async { + testWidgets('List positioned with 20 at halfway and shrink wrap', (WidgetTester tester) async { await setUpWidgetTest(tester, topItem: 20, anchor: 0.5); await tester.pump(); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemLeadingEdge, - 0.5); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemLeadingEdge, + 0.5, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemTrailingEdge, - 0.5 + itemHeight / screenHeight); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemTrailingEdge, + 0.5 + itemHeight / screenHeight, + ); }); - testWidgets('List positioned with 20 half off top of screen and shrink wrap', - (WidgetTester tester) async { - await setUpWidgetTest(tester, - topItem: 20, anchor: -(itemHeight / screenHeight) / 2); + testWidgets('List positioned with 20 half off top of screen and shrink wrap', (WidgetTester tester) async { + await setUpWidgetTest(tester, topItem: 20, anchor: -(itemHeight / screenHeight) / 2); await tester.pump(); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemLeadingEdge, - -(itemHeight / screenHeight) / 2); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemLeadingEdge, + -(itemHeight / screenHeight) / 2, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 20) - .itemTrailingEdge, - (itemHeight / screenHeight) / 2); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 20).itemTrailingEdge, + (itemHeight / screenHeight) / 2, + ); }); - testWidgets('List positioned with 5 at top then scroll up 2 and shrink wrap', - (WidgetTester tester) async { + testWidgets('List positioned with 5 at top then scroll up 2 and shrink wrap', (WidgetTester tester) async { await setUpWidgetTest(tester, topItem: 5); - await tester.drag( - find.byType(PositionedList), const Offset(0, itemHeight * 2)); + await tester.drag(find.byType(PositionedList), const Offset(0, itemHeight * 2)); await tester.pump(); expect(find.text('Item 2'), findsNothing); @@ -227,45 +176,33 @@ void main() { expect(find.text('Item 13'), findsNothing); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemLeadingEdge, - -1 / 10); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 3) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 2).itemLeadingEdge, + -1 / 10, + ); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 3).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 12) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 12).itemTrailingEdge, + 1, + ); }); - testWidgets( - 'List positioned with 5 at top then scroll down 1/2 and shrink wrap', - (WidgetTester tester) async { + testWidgets('List positioned with 5 at top then scroll down 1/2 and shrink wrap', (WidgetTester tester) async { await setUpWidgetTest(tester, topItem: 5); - await tester.drag( - find.byType(PositionedList), const Offset(0, -1 / 2 * itemHeight)); + await tester.drag(find.byType(PositionedList), const Offset(0, -1 / 2 * itemHeight)); await tester.pump(); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemTrailingEdge, - 1 / 20); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemTrailingEdge, + 1 / 20, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 14) - .itemLeadingEdge, - 17 / 20); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 14).itemLeadingEdge, + 17 / 20, + ); }); - testWidgets('List positioned with 0 at top scroll up 5 and shrink wrap', - (WidgetTester tester) async { + testWidgets('List positioned with 0 at top scroll up 5 and shrink wrap', (WidgetTester tester) async { final scrollController = ScrollController(); await setUpWidgetTest(tester, scrollController: scrollController); await tester.pump(); @@ -279,24 +216,18 @@ void main() { expect(find.text('Item 14'), findsOneWidget); expect(find.text('Item 15'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemLeadingEdge, - -1 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemLeadingEdge, + -1 / 10, + ); }); testWidgets( '''List positioned with 5 at top then scroll up 2 programatically and shrink wrap''', (WidgetTester tester) async { final scrollController = ScrollController(); - await setUpWidgetTest(tester, - topItem: 5, scrollController: scrollController); + await setUpWidgetTest(tester, topItem: 5, scrollController: scrollController); scrollController.jumpTo(-2 * itemHeight); await tester.pump(); @@ -307,20 +238,17 @@ void main() { expect(find.text('Item 13'), findsNothing); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemLeadingEdge, - -1 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 2).itemLeadingEdge, + -1 / 10, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 3) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 3).itemLeadingEdge, + 0, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 12) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 12).itemTrailingEdge, + 1, + ); }, ); @@ -328,93 +256,70 @@ void main() { '''List positioned with 5 at top then scroll down 20 programatically and shrink wrap''', (WidgetTester tester) async { final scrollController = ScrollController(); - await setUpWidgetTest(tester, - topItem: 5, scrollController: scrollController); + await setUpWidgetTest(tester, topItem: 5, scrollController: scrollController); scrollController.jumpTo(itemHeight * 20); await tester.pump(); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 23) - .itemLeadingEdge, - -2 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 23).itemLeadingEdge, + -2 / 10, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 24) - .itemLeadingEdge, - -1 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 24).itemLeadingEdge, + -1 / 10, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 25) - .itemLeadingEdge, - 0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 25).itemLeadingEdge, + 0, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemLeadingEdge, - -21 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemLeadingEdge, + -21 / 10, + ); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 5) - .itemLeadingEdge, - -20 / 10); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 5).itemLeadingEdge, + -20 / 10, + ); }, ); - testWidgets( - 'List positioned with 5 at top and initial scroll offset and shrink wrap', - (WidgetTester tester) async { - final scrollController = - ScrollController(initialScrollOffset: -2 * itemHeight); - await setUpWidgetTest(tester, - topItem: 5, scrollController: scrollController); + testWidgets('List positioned with 5 at top and initial scroll offset and shrink wrap', (WidgetTester tester) async { + final scrollController = ScrollController(initialScrollOffset: -2 * itemHeight); + await setUpWidgetTest(tester, topItem: 5, scrollController: scrollController); expect(find.text('Item 2'), findsNothing); expect(find.text('Item 3'), findsOneWidget); expect(find.text('Item 12'), findsOneWidget); expect(find.text('Item 13'), findsNothing); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 3).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 3) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 12) - .itemTrailingEdge, - 1); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 12).itemTrailingEdge, + 1, + ); }); - testWidgets('short List with reverse and shrink wrap', - (WidgetTester tester) async { + testWidgets('short List with reverse and shrink wrap', (WidgetTester tester) async { const itemCount = 5; const key = Key('short_list'); - await setUpWidgetTest(tester, - itemCount: itemCount, key: key, reverse: true); + await setUpWidgetTest(tester, itemCount: itemCount, key: key, reverse: true); await tester.pump(); expect(find.text('Item 4'), findsOneWidget); expect(find.text('Item 5'), findsNothing); - expect( - tester.getBottomRight(find.text('Item 0')).dy, itemHeight * itemCount); + expect(tester.getBottomRight(find.text('Item 0')).dy, itemHeight * itemCount); expect(tester.getTopLeft(find.text('Item 4')).dy, 0); final positionList = find.byKey(key); expect(tester.getBottomRight(positionList).dy, itemHeight * itemCount); expect(tester.getTopLeft(positionList).dy, 0); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 4) - .itemTrailingEdge, - 1.0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 4).itemTrailingEdge, + 1.0, + ); }); testWidgets('test nested positioned list', (WidgetTester tester) async { @@ -432,13 +337,14 @@ void main() { itemBuilder: (context, index) { if (index == 0) { return PositionedList( - key: key, - itemCount: itemCount, - shrinkWrap: true, - itemBuilder: (context, idx) => SizedBox( - height: itemHeight, - child: Text('Item $idx'), - )); + key: key, + itemCount: itemCount, + shrinkWrap: true, + itemBuilder: (context, idx) => SizedBox( + height: itemHeight, + child: Text('Item $idx'), + ), + ); } else { return SizedBox( height: itemHeight, @@ -461,15 +367,10 @@ void main() { expect(tester.getBottomRight(positionList).dy, itemHeight * itemCount); expect(tester.getTopLeft(positionList).dy, 0); + expect(itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsNotifier.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemTrailingEdge, - 5.0); + itemPositionsNotifier.itemPositions.value.firstWhere((position) => position.index == 0).itemTrailingEdge, + 5.0, + ); }); } diff --git a/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_scrollable_position_list_test.dart b/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_scrollable_position_list_test.dart index 37830e00c9..7b0655b150 100644 --- a/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_scrollable_position_list_test.dart +++ b/packages/stream_chat_flutter/test/scrollable_positioned_list/shrink_wrap_scrollable_position_list_test.dart @@ -29,31 +29,31 @@ void main() { MaterialApp( // Use flex layout to ensure that the minimum height is not limited to // screenHeight. - home: Column(children: [ - // Use Constrained to make max height not more than screenHeight - ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: screenHeight, maxWidth: screenWidth), - child: ScrollablePositionedList.builder( - itemCount: itemCount, - initialScrollIndex: initialIndex, - itemScrollController: itemScrollController, - itemBuilder: (context, index) => SizedBox( - height: itemHeight, - child: Text('Item $index'), + home: Column( + children: [ + // Use Constrained to make max height not more than screenHeight + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: screenHeight, maxWidth: screenWidth), + child: ScrollablePositionedList.builder( + itemCount: itemCount, + initialScrollIndex: initialIndex, + itemScrollController: itemScrollController, + itemBuilder: (context, index) => SizedBox( + height: itemHeight, + child: Text('Item $index'), + ), + itemPositionsListener: itemPositionsListener, + shrinkWrap: true, + padding: padding, ), - itemPositionsListener: itemPositionsListener, - shrinkWrap: true, - padding: padding, ), - ), - ]), + ], + ), ), ); } - testWidgets('List positioned with 0 at top and shrink wrap', - (WidgetTester tester) async { + testWidgets('List positioned with 0 at top and shrink wrap', (WidgetTester tester) async { final itemPositionsListener = ItemPositionsListener.create(); await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener); @@ -61,123 +61,95 @@ void main() { expect(tester.getBottomRight(find.text('Item 9')).dy, screenHeight); expect(find.text('Item 10'), findsNothing); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); }); - testWidgets('Scroll to 1 then 2 (both already on screen) with shrink wrap', - (WidgetTester tester) async { + testWidgets('Scroll to 1 then 2 (both already on screen) with shrink wrap', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 1, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 1, duration: scrollDuration)); await tester.pump(); await tester.pump(scrollDuration); expect(find.text('Item 0'), findsNothing); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 1) - .itemLeadingEdge, - 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 1).itemLeadingEdge, 0); expect(tester.getTopLeft(find.text('Item 1')).dy, 0); - unawaited( - itemScrollController.scrollTo(index: 2, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 2, duration: scrollDuration)); await tester.pump(); await tester.pump(scrollDuration); expect(find.text('Item 1'), findsNothing); expect(tester.getTopLeft(find.text('Item 2')).dy, 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 2).itemLeadingEdge, 0); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 2) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 11) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 11).itemTrailingEdge, + 1, + ); }); - testWidgets( - 'Scroll to 5 (already on screen) and then back to 0 with shrink wrap', - (WidgetTester tester) async { + testWidgets('Scroll to 5 (already on screen) and then back to 0 with shrink wrap', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 5, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 5, duration: scrollDuration)); await tester.pumpAndSettle(); - unawaited( - itemScrollController.scrollTo(index: 0, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 0, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 0'), findsOneWidget); expect(find.text('Item 9'), findsOneWidget); expect(find.text('Item 10'), findsNothing); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 0) - .itemLeadingEdge, - 0); - expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 9) - .itemTrailingEdge, - 1); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 0).itemLeadingEdge, 0); + expect(itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 9).itemTrailingEdge, 1); }); - testWidgets('Scroll to 100 (not already on screen) with shrink wrap', - (WidgetTester tester) async { + testWidgets('Scroll to 100 (not already on screen) with shrink wrap', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); - unawaited( - itemScrollController.scrollTo(index: 100, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 100, duration: scrollDuration)); await tester.pumpAndSettle(); expect(find.text('Item 99'), findsNothing); expect(find.text('Item 100'), findsOneWidget); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 109) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 109).itemTrailingEdge, + 1, + ); }); testWidgets('Jump to 100 with shrink wrap', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); final itemPositionsListener = ItemPositionsListener.create(); - await setUpWidgetTest(tester, - itemScrollController: itemScrollController, - itemPositionsListener: itemPositionsListener); + await setUpWidgetTest( + tester, + itemScrollController: itemScrollController, + itemPositionsListener: itemPositionsListener, + ); itemScrollController.jumpTo(index: 100); await tester.pumpAndSettle(); @@ -186,19 +158,16 @@ void main() { expect(tester.getBottomRight(find.text('Item 109')).dy, screenHeight); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 100) - .itemLeadingEdge, - 0); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 100).itemLeadingEdge, + 0, + ); expect( - itemPositionsListener.itemPositions.value - .firstWhere((position) => position.index == 109) - .itemTrailingEdge, - 1); + itemPositionsListener.itemPositions.value.firstWhere((position) => position.index == 109).itemTrailingEdge, + 1, + ); }); - testWidgets('padding test - centered sliver at bottom with shrink wrap', - (WidgetTester tester) async { + testWidgets('padding test - centered sliver at bottom with shrink wrap', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -207,25 +176,19 @@ void main() { ); expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); - expect(tester.getTopLeft(find.text('Item 1')), - const Offset(10, itemHeight + 10)); - expect(tester.getBottomRight(find.text('Item 1')), - const Offset(screenWidth - 10, 10 + itemHeight * 2)); + expect(tester.getTopLeft(find.text('Item 1')), const Offset(10, itemHeight + 10)); + expect(tester.getBottomRight(find.text('Item 1')), const Offset(screenWidth - 10, 10 + itemHeight * 2)); - unawaited( - itemScrollController.scrollTo(index: 490, duration: scrollDuration)); + unawaited(itemScrollController.scrollTo(index: 490, duration: scrollDuration)); await tester.pumpAndSettle(); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, -100)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, -100)); await tester.pumpAndSettle(); - expect(tester.getTopLeft(find.text('Item 499')), - const Offset(10, screenHeight - itemHeight - 10)); + expect(tester.getTopLeft(find.text('Item 499')), const Offset(10, screenHeight - itemHeight - 10)); }); - testWidgets('padding test - centered sliver not at bottom', - (WidgetTester tester) async { + testWidgets('padding test - centered sliver not at bottom', (WidgetTester tester) async { final itemScrollController = ItemScrollController(); await setUpWidgetTest( tester, @@ -234,14 +197,11 @@ void main() { padding: const EdgeInsets.all(10), ); - await tester.drag( - find.byType(ScrollablePositionedList), const Offset(0, 200)); + await tester.drag(find.byType(ScrollablePositionedList), const Offset(0, 200)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10)); - expect(tester.getTopLeft(find.text('Item 2')), - const Offset(10, 10 + itemHeight * 2)); - expect(tester.getTopLeft(find.text('Item 3')), - const Offset(10, 10 + itemHeight * 3)); + expect(tester.getTopLeft(find.text('Item 2')), const Offset(10, 10 + itemHeight * 2)); + expect(tester.getTopLeft(find.text('Item 3')), const Offset(10, 10 + itemHeight * 3)); }); } diff --git a/packages/stream_chat_flutter/test/src/attachment/attachment_handler_test.dart b/packages/stream_chat_flutter/test/src/attachment/attachment_handler_test.dart index 6765434452..b7f449e271 100644 --- a/packages/stream_chat_flutter/test/src/attachment/attachment_handler_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/attachment_handler_test.dart @@ -17,8 +17,7 @@ void main() { final attachmentHandler = MockAttachmentHandler(); - when(() => attachmentHandler.downloadAttachment(attachment)) - .thenAnswer((invocation) async => 'filePath'); + when(() => attachmentHandler.downloadAttachment(attachment)).thenAnswer((invocation) async => 'filePath'); expect( await attachmentHandler.downloadAttachment(attachment), @@ -31,15 +30,13 @@ void main() { title: 'test giphy attachment', type: 'giphy', extraData: const { - 'original': - 'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti', + 'original': 'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti', }, ); final attachmentHandler = MockAttachmentHandler(); - when(() => attachmentHandler.downloadAttachment(attachment)) - .thenAnswer((invocation) async => 'filePath'); + when(() => attachmentHandler.downloadAttachment(attachment)).thenAnswer((invocation) async => 'filePath'); expect( await attachmentHandler.downloadAttachment(attachment), @@ -56,8 +53,7 @@ void main() { final attachmentHandler = MockAttachmentHandler(); - when(() => attachmentHandler.downloadAttachment(attachment)) - .thenAnswer((invocation) async => 'filePath'); + when(() => attachmentHandler.downloadAttachment(attachment)).thenAnswer((invocation) async => 'filePath'); expect( await attachmentHandler.downloadAttachment(attachment), diff --git a/packages/stream_chat_flutter/test/src/attachment/attachment_upload_state_builder_test.dart b/packages/stream_chat_flutter/test/src/attachment/attachment_upload_state_builder_test.dart index f87bcdedf5..104f254be7 100644 --- a/packages/stream_chat_flutter/test/src/attachment/attachment_upload_state_builder_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/attachment_upload_state_builder_test.dart @@ -5,9 +5,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import '../mocks.dart'; void main() { - testWidgets( - 'AttachmentUploadStateBuilder returns Offstage when message is sent', - (tester) async { + testWidgets('AttachmentUploadStateBuilder returns Offstage when message is sent', (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( diff --git a/packages/stream_chat_flutter/test/src/attachment/builder/voice_recording_attachment_playlist_builder.dart b/packages/stream_chat_flutter/test/src/attachment/builder/voice_recording_attachment_playlist_builder.dart index eef7c8279e..28c0ba6c93 100644 --- a/packages/stream_chat_flutter/test/src/attachment/builder/voice_recording_attachment_playlist_builder.dart +++ b/packages/stream_chat_flutter/test/src/attachment/builder/voice_recording_attachment_playlist_builder.dart @@ -48,11 +48,10 @@ void main() { await tester.pumpWidget( _wrapWithStreamChatApp( Builder( - builder: (context) => builder.build( - context, - Message(), - attachments, - ), + builder: (context) { + final attachment = builder.build(context, Message(), attachments); + return attachment ?? const SizedBox.shrink(); + }, ), ), ); @@ -74,13 +73,15 @@ Widget _wrapWithStreamChatApp( return MaterialApp( home: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: Center(child: widget), - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Center(child: widget), + ); + }, + ), ), ); } diff --git a/packages/stream_chat_flutter/test/src/attachment/file_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/file_attachment_test.dart index 578eabe6fa..d3105d2457 100644 --- a/packages/stream_chat_flutter/test/src/attachment/file_attachment_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/file_attachment_test.dart @@ -25,10 +25,12 @@ void main() { channel: channel, child: SizedBox( child: StreamFileAttachment( - constraints: BoxConstraints.tight(const Size( - 300, - 300, - )), + constraints: BoxConstraints.tight( + const Size( + 300, + 300, + ), + ), message: Message(), file: Attachment( type: 'file', diff --git a/packages/stream_chat_flutter/test/src/attachment/gallery_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/gallery_attachment_test.dart index 8b08044a00..dac183ef17 100644 --- a/packages/stream_chat_flutter/test/src/attachment/gallery_attachment_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/gallery_attachment_test.dart @@ -1,8 +1,8 @@ -import 'package:cached_network_image/cached_network_image.dart'; 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 'package:stream_core_flutter/stream_core_flutter.dart'; import '../mocks.dart'; @@ -22,8 +22,7 @@ void main() { Attachment( type: 'image', title: 'example.png', - imageUrl: - 'https://logowik.com/content/uploads/images/flutter5786.jpg', + imageUrl: 'https://logowik.com/content/uploads/images/flutter5786.jpg', extraData: const { 'mime_type': 'png', }, @@ -31,8 +30,7 @@ void main() { Attachment( type: 'image', title: 'example.png', - imageUrl: - 'https://logowik.com/content/uploads/images/flutter5786.jpg', + imageUrl: 'https://logowik.com/content/uploads/images/flutter5786.jpg', extraData: const { 'mime_type': 'png', }, @@ -47,10 +45,12 @@ void main() { channel: channel, child: SizedBox( child: StreamGalleryAttachment( - constraints: BoxConstraints.tight(const Size( - 300, - 300, - )), + constraints: BoxConstraints.tight( + const Size( + 300, + 300, + ), + ), message: Message(), attachments: attachments, itemBuilder: (context, index) { @@ -73,7 +73,7 @@ void main() { // wait for the initial state to be rendered. await tester.pump(Duration.zero); - expect(find.byType(CachedNetworkImage), findsNWidgets(2)); + expect(find.byType(StreamNetworkImage), findsNWidgets(2)); }, ); } diff --git a/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart index 42a604c791..a61b47e080 100644 --- a/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/giphy_attachment_test.dart @@ -25,16 +25,17 @@ void main() { channel: channel, child: SizedBox( child: StreamGiphyAttachment( - constraints: BoxConstraints.tight(const Size( - 300, - 300, - )), + constraints: BoxConstraints.tight( + const Size( + 300, + 300, + ), + ), message: Message(), giphy: Attachment( type: 'giphy', title: 'example.gif', - imageUrl: - 'https://media.giphy.com/media/35H0pwQNaO2iLTnnBf/giphy.gif', + imageUrl: 'https://media.giphy.com/media/35H0pwQNaO2iLTnnBf/giphy.gif', extraData: const { 'mime_type': 'gif', }, 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 fd5b200325..db5b93bea6 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 a73b7e6956..2249f99b30 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 12ad13a8a9..6d969564d5 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 74a2647b67..0055bf5b59 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 bd309b5d25..e21d0c270c 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 a0f1b02e81..1aa2c4ee3c 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/image_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/image_attachment_test.dart index 1bab6234d1..d75da31da1 100644 --- a/packages/stream_chat_flutter/test/src/attachment/image_attachment_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/image_attachment_test.dart @@ -1,8 +1,8 @@ -import 'package:cached_network_image/cached_network_image.dart'; 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 'package:stream_core_flutter/stream_core_flutter.dart'; import '../mocks.dart'; @@ -26,16 +26,17 @@ void main() { channel: channel, child: SizedBox( child: StreamImageAttachment( - constraints: BoxConstraints.tight(const Size( - 300, - 300, - )), + constraints: BoxConstraints.tight( + const Size( + 300, + 300, + ), + ), message: Message(), image: Attachment( type: 'image', title: 'example.png', - imageUrl: - 'https://logowik.com/content/uploads/images/flutter5786.jpg', + imageUrl: 'https://logowik.com/content/uploads/images/flutter5786.jpg', extraData: const { 'mime_type': 'png', }, @@ -50,7 +51,7 @@ void main() { // wait for the initial state to be rendered. await tester.pump(Duration.zero); - expect(find.byType(CachedNetworkImage), findsOneWidget); + expect(find.byType(StreamNetworkImage), findsOneWidget); }, ); } diff --git a/packages/stream_chat_flutter/test/src/attachment/url_attachment_test.dart b/packages/stream_chat_flutter/test/src/attachment/url_attachment_test.dart index 21c990545f..012688ad52 100644 --- a/packages/stream_chat_flutter/test/src/attachment/url_attachment_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment/url_attachment_test.dart @@ -24,10 +24,8 @@ void main() { child: StreamChannel( channel: channel, child: SizedBox( - child: StreamUrlAttachment( - messageTheme: streamTheme.ownMessageTheme, + child: StreamLinkPreviewAttachment( message: Message(), - hostDisplayName: 'Test', urlAttachment: Attachment( title: 'Flutter', titleLink: 'https://flutter.dev', 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..f5b2674d07 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,7 @@ 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/src/audio/audio_playlist_state.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import '../mocks.dart'; @@ -50,33 +50,6 @@ void main() { }, ); - testWidgets( - 'uses custom shape when provided', - (WidgetTester tester) async { - final customShape = RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ); - - await tester.pumpWidget( - _wrapWithStreamChatApp( - StreamVoiceRecordingAttachmentPlaylist( - message: MockMessage(), - voiceRecordings: [fakeAudioRecording1], - shape: customShape, - ), - ), - ); - - expect(find.byType(StreamVoiceRecordingAttachment), findsOneWidget); - - final attachment = tester.widget( - find.byType(StreamVoiceRecordingAttachment), - ); - - expect(attachment.shape, customShape); - }, - ); - testWidgets( 'updates playlist when recordings change', (WidgetTester tester) async { @@ -143,7 +116,7 @@ void main() { find.byType(StreamVoiceRecordingAttachment), ); - expect(attachment.constraints, constraints); + expect(attachment.props.constraints, constraints); }, ); @@ -175,6 +148,63 @@ void main() { }, ); + testWidgets( + 'does not play audio when track seek changes', + (WidgetTester tester) async { + await tester.pumpWidget( + _wrapWithStreamChatApp( + StreamVoiceRecordingAttachmentPlaylist( + message: MockMessage(), + voiceRecordings: [fakeAudioRecording1], + ), + ), + ); + + final attachment = tester.widget( + find.byType(StreamVoiceRecordingAttachment), + ); + + // onTrackSeekEnd must be null so that play() is never called + // when the user lifts their finger after scrubbing. + expect(attachment.props.onTrackSeekEnd, isNull); + }, + ); + + testWidgets( + 'seek callback does not resume playback', + (WidgetTester tester) async { + await tester.pumpWidget( + _wrapWithStreamChatApp( + StreamVoiceRecordingAttachmentPlaylist( + message: MockMessage(), + voiceRecordings: [fakeAudioRecording1], + ), + ), + ); + + final attachmentFinder = find.byType(StreamVoiceRecordingAttachment); + final attachment = tester.widget( + attachmentFinder, + ); + + // Invoking onTrackSeekChanged should not throw and should not change + // the track to a playing state (track is idle, no AudioPlayer action + // is triggered because there is no active audio source). + expect( + () => attachment.props.onTrackSeekChanged?.call(0.5), + returnsNormally, + ); + + await tester.pump(); + + // The track should still not be in a playing state. + final updatedAttachment = tester.widget( + attachmentFinder, + ); + expect(updatedAttachment.props.track.state, isNot(TrackState.playing)); + }, + ); + testWidgets( 'allows custom item', (WidgetTester tester) async { @@ -254,15 +284,26 @@ Widget _wrapWithStreamChatApp( Brightness? brightness, }) { return MaterialApp( + theme: ThemeData( + brightness: .light, + extensions: [StreamTheme.light()], + ), + darkTheme: ThemeData( + brightness: .dark, + extensions: [StreamTheme.dark()], + ), + themeMode: brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark, home: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: widget, - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: widget, + ); + }, + ), ), ); } 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..e34bdec0da 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,13 +2,10 @@ 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'; import '../mocks.dart'; -import '../utils/finders.dart'; void main() { group( @@ -28,7 +25,7 @@ void main() { _wrapWithStreamChatApp( StreamVoiceRecordingAttachment( track: fakePlaylistTrack, - speed: PlaybackSpeed.regular, + speed: StreamPlaybackSpeed.x1, ), ), ); @@ -36,8 +33,6 @@ void main() { // Verify key components are present expect(find.byType(AudioControlButton), findsOneWidget); expect(find.byType(StreamAudioWaveformSlider), findsOneWidget); - expect( - find.bySvgIcon(StreamSvgIcons.filetypeAudioM4a), findsOneWidget); }, ); @@ -49,7 +44,7 @@ void main() { StreamVoiceRecordingAttachment( showTitle: true, track: fakePlaylistTrack, - speed: PlaybackSpeed.regular, + speed: StreamPlaybackSpeed.x1, ), ), ); @@ -68,7 +63,7 @@ void main() { StreamVoiceRecordingAttachment( showTitle: true, track: fakePlaylistTrack, - speed: PlaybackSpeed.regular, + speed: StreamPlaybackSpeed.x1, ), ), ); @@ -86,13 +81,13 @@ void main() { _wrapWithStreamChatApp( StreamVoiceRecordingAttachment( track: fakePlaylistTrack.copyWith(state: TrackState.playing), - speed: PlaybackSpeed.regular, + speed: StreamPlaybackSpeed.x1, ), ), ); - expect(find.text('x1.0'), findsOneWidget); - expect(find.byType(SpeedControlButton), findsOneWidget); + expect(find.text('x1'), findsOneWidget); + expect(find.byType(StreamPlaybackSpeedToggle), findsOneWidget); }, ); @@ -106,7 +101,7 @@ void main() { _wrapWithStreamChatApp( StreamVoiceRecordingAttachment( track: fakePlaylistTrack.copyWith(state: state), - speed: PlaybackSpeed.regular, + speed: StreamPlaybackSpeed.x1, onTrackPlay: onTrackPlay, ), ), @@ -130,7 +125,7 @@ void main() { _wrapWithStreamChatApp( StreamVoiceRecordingAttachment( track: fakePlaylistTrack.copyWith(state: TrackState.playing), - speed: PlaybackSpeed.regular, + speed: StreamPlaybackSpeed.x1, onTrackPause: onTrackPause, ), ), @@ -155,7 +150,7 @@ void main() { _wrapWithStreamChatApp( StreamVoiceRecordingAttachment( track: fakePlaylistTrack.copyWith(state: TrackState.playing), - speed: PlaybackSpeed.regular, + speed: StreamPlaybackSpeed.x1, onTrackSeekStart: onTrackSeekStart, onTrackSeekChanged: onTrackSeekChanged, onTrackSeekEnd: onTrackSeekEnd, @@ -188,8 +183,8 @@ void main() { testWidgets( 'handles speed change callback', (WidgetTester tester) async { - for (final speed in PlaybackSpeed.values) { - final onChangeSpeed = MockValueChanged(); + for (final speed in StreamPlaybackSpeed.values) { + final onChangeSpeed = MockValueChanged(); await tester.pumpWidget( _wrapWithStreamChatApp( @@ -201,40 +196,12 @@ void main() { ), ); - await tester.tap(find.byType(SpeedControlButton)); + await tester.tap(find.byType(StreamPlaybackSpeedToggle)); verify(() => onChangeSpeed(speed.next)).called(1); } }, ); - testWidgets( - 'custom trailing builder works', - (WidgetTester tester) async { - Widget customTrailingBuilder( - BuildContext context, - PlaylistTrack track, - PlaybackSpeed speed, - ValueChanged? onChangeSpeed, - ) { - return const StreamSvgIcon(icon: StreamSvgIcons.closeSmall); - } - - await tester.pumpWidget( - _wrapWithStreamChatApp( - StreamVoiceRecordingAttachment( - track: fakePlaylistTrack, - speed: PlaybackSpeed.regular, - trailingBuilder: customTrailingBuilder, - ), - ), - ); - - // Verify custom trailing widget is rendered - expect(find.bySvgIcon(StreamSvgIcons.closeSmall), findsOneWidget); - expect(find.bySvgIcon(StreamSvgIcons.filetypeAudioM4a), findsNothing); - }, - ); - for (final brightness in Brightness.values) { final theme = brightness.name; goldenTest( @@ -248,7 +215,7 @@ void main() { child: StreamVoiceRecordingAttachment( showTitle: true, track: fakePlaylistTrack, - speed: PlaybackSpeed.regular, + speed: StreamPlaybackSpeed.x1, ), ), ), @@ -268,7 +235,7 @@ void main() { state: TrackState.playing, position: const Duration(seconds: 10), ), - speed: PlaybackSpeed.regular, + speed: StreamPlaybackSpeed.x1, ), ), ), @@ -283,15 +250,18 @@ Widget _wrapWithStreamChatApp( Brightness? brightness, }) { return MaterialApp( + theme: ThemeData(brightness: brightness), home: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: Center(child: widget), - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Center(child: widget), + ); + }, + ), ), ); } diff --git a/packages/stream_chat_flutter/test/src/attachment_actions_modal/attachment_actions_modal_test.dart b/packages/stream_chat_flutter/test/src/attachment_actions_modal/attachment_actions_modal_test.dart index 08213b0981..511089188c 100644 --- a/packages/stream_chat_flutter/test/src/attachment_actions_modal/attachment_actions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment_actions_modal/attachment_actions_modal_test.dart @@ -26,8 +26,7 @@ class MockAttachmentDownloader extends Mock { void main() { setUpAll(() { - registerFallbackValue( - MaterialPageRoute(builder: (context) => const SizedBox())); + registerFallbackValue(MaterialPageRoute(builder: (context) => const SizedBox())); registerFallbackValue(Message()); }); @@ -265,8 +264,7 @@ void main() { final clientState = MockClientState(); final mockChannel = MockChannel(); - when(() => mockChannel.updateMessage(any())) - .thenAnswer((_) async => UpdateMessageResponse()); + when(() => mockChannel.updateMessage(any())).thenAnswer((_) async => UpdateMessageResponse()); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -306,11 +304,15 @@ void main() { ), ); await tester.tap(find.text('Delete')); - verify(() => mockChannel.updateMessage(message.copyWith( + verify( + () => mockChannel.updateMessage( + message.copyWith( attachments: [ message.attachments[1], ], - ))).called(1); + ), + ), + ).called(1); }, ); @@ -321,8 +323,7 @@ void main() { final clientState = MockClientState(); final mockChannel = MockChannel(); - when(() => mockChannel.updateMessage(any())) - .thenAnswer((_) async => UpdateMessageResponse()); + when(() => mockChannel.updateMessage(any())).thenAnswer((_) async => UpdateMessageResponse()); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -357,9 +358,13 @@ void main() { ), ); await tester.tap(find.text('Delete')); - verify(() => mockChannel.updateMessage(message.copyWith( + verify( + () => mockChannel.updateMessage( + message.copyWith( attachments: [], - ))).called(1); + ), + ), + ).called(1); }, ); @@ -371,8 +376,7 @@ void main() { final clientState = MockClientState(); final mockChannel = MockChannel(); - when(() => mockChannel.deleteMessage(any())) - .thenAnswer((_) async => EmptyResponse()); + when(() => mockChannel.deleteMessage(any())).thenAnswer((_) async => EmptyResponse()); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); diff --git a/packages/stream_chat_flutter/test/src/audio/audio_playlist_controller_test.dart b/packages/stream_chat_flutter/test/src/audio/audio_playlist_controller_test.dart index 2a5cb09b5c..f94eecadfd 100644 --- a/packages/stream_chat_flutter/test/src/audio/audio_playlist_controller_test.dart +++ b/packages/stream_chat_flutter/test/src/audio/audio_playlist_controller_test.dart @@ -4,6 +4,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat_flutter/src/audio/audio_playlist_controller.dart'; import 'package:stream_chat_flutter/src/audio/audio_playlist_state.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; class MockAudioPlayer extends Mock implements AudioPlayer {} @@ -52,12 +53,9 @@ void main() { speedController = PublishSubject(); // Default mock behaviors - when(() => mockPlayer.playerStateStream) - .thenAnswer((_) => stateController.stream); - when(() => mockPlayer.positionStream) - .thenAnswer((_) => positionController.stream); - when(() => mockPlayer.speedStream) - .thenAnswer((_) => speedController.stream); + when(() => mockPlayer.playerStateStream).thenAnswer((_) => stateController.stream); + when(() => mockPlayer.positionStream).thenAnswer((_) => positionController.stream); + when(() => mockPlayer.speedStream).thenAnswer((_) => speedController.stream); controller = StreamAudioPlaylistController.raw( player: mockPlayer, @@ -75,7 +73,7 @@ void main() { test('controller initializes with correct default state', () { expect(controller.value.tracks.length, equals(2)); expect(controller.value.currentIndex, isNull); - expect(controller.value.speed, equals(PlaybackSpeed.regular)); + expect(controller.value.speed, equals(StreamPlaybackSpeed.x1)); expect(controller.value.loopMode, equals(PlaylistLoopMode.off)); }); @@ -124,7 +122,7 @@ void main() { PlaylistTrack( title: 'new-track.mp3', uri: Uri.parse('https://example.com/new-track.mp3'), - ) + ), ]; await controller.updatePlaylist(newTracks); @@ -166,7 +164,7 @@ void main() { test('setSpeed changes playback rate', () async { when(() => mockPlayer.setSpeed(any())).thenAnswer((_) async {}); - const playbackSpeed = PlaybackSpeed.faster; + const playbackSpeed = StreamPlaybackSpeed.x2; await controller.setSpeed(playbackSpeed); verify(() => mockPlayer.setSpeed(playbackSpeed.speed)).called(1); @@ -216,17 +214,17 @@ void main() { }); test('speedStream updates playback speed', () async { - speedController.add(1.5); + speedController.add(2); await Future.delayed(Duration.zero); - expect(controller.value.speed, equals(PlaybackSpeed.faster)); + expect(controller.value.speed, equals(StreamPlaybackSpeed.x2)); - speedController.add(2); + speedController.add(0.5); await Future.delayed(Duration.zero); - expect(controller.value.speed, equals(PlaybackSpeed.fastest)); + expect(controller.value.speed, equals(StreamPlaybackSpeed.x0_5)); speedController.add(1); await Future.delayed(Duration.zero); - expect(controller.value.speed, equals(PlaybackSpeed.regular)); + expect(controller.value.speed, equals(StreamPlaybackSpeed.x1)); }); test('track completes and auto-advances', () async { diff --git a/packages/stream_chat_flutter/test/src/audio/audio_sampling_test.dart b/packages/stream_chat_flutter/test/src/audio/audio_sampling_test.dart index a1f2918274..e48ab7a14c 100644 --- a/packages/stream_chat_flutter/test/src/audio/audio_sampling_test.dart +++ b/packages/stream_chat_flutter/test/src/audio/audio_sampling_test.dart @@ -76,8 +76,7 @@ void main() { countMap[value] = (countMap[value] ?? 0) + 1; } // Each value should appear either 2 or 3 times - expect( - countMap.values.every((count) => count == 2 || count == 3), isTrue); + expect(countMap.values.every((count) => count == 2 || count == 3), isTrue); }); test('returns original data when target size is smaller', () { diff --git a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_0.png b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_0.png index 7069fe981d..6e685f7848 100644 Binary files a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_0.png and b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_0.png differ diff --git a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_1.png b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_1.png index 5838bbf505..8779ad28ae 100644 Binary files a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_1.png and b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_1.png differ diff --git a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_2.png b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_2.png index 7069fe981d..6e685f7848 100644 Binary files a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_2.png and b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_2.png differ diff --git a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_3.png b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_3.png index c711450831..88abc34ded 100644 Binary files a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_3.png and b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_3.png differ diff --git a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_issue_2369.png b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_issue_2369.png index 110b053c0b..bff58396b2 100644 Binary files a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_issue_2369.png and b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/gradient_avatar_issue_2369.png differ diff --git a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/group_avatar_0.png b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/group_avatar_0.png index 7a3fe14616..eaa5cea6b3 100644 Binary files a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/group_avatar_0.png and b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/group_avatar_0.png differ diff --git a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/user_avatar_0.png b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/user_avatar_0.png index a511a95448..48e867a140 100644 Binary files a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/user_avatar_0.png and b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/user_avatar_0.png differ diff --git a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/user_avatar_1.png b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/user_avatar_1.png index 531f4ac3ae..87e44f277c 100644 Binary files a/packages/stream_chat_flutter/test/src/avatars/goldens/ci/user_avatar_1.png and b/packages/stream_chat_flutter/test/src/avatars/goldens/ci/user_avatar_1.png differ diff --git a/packages/stream_chat_flutter/test/src/avatars/gradient_avatar_test.dart b/packages/stream_chat_flutter/test/src/avatars/gradient_avatar_test.dart index e63fe05246..478f834361 100644 --- a/packages/stream_chat_flutter/test/src/avatars/gradient_avatar_test.dart +++ b/packages/stream_chat_flutter/test/src/avatars/gradient_avatar_test.dart @@ -28,8 +28,7 @@ void main() { child: SizedBox( width: 100, height: 100, - child: StreamGradientAvatar( - name: 'demo user', userId: 'demo123'), + child: StreamGradientAvatar(name: 'demo user', userId: 'demo123'), ), ), ), @@ -368,16 +367,18 @@ Widget _wrapWithMaterialApp( return MaterialApp( home: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: Container( - padding: const EdgeInsets.all(16), - child: Center(child: widget), - ), - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Container( + padding: const EdgeInsets.all(16), + child: Center(child: widget), + ), + ); + }, + ), ), ); } diff --git a/packages/stream_chat_flutter/test/src/avatars/group_avatar_test.dart b/packages/stream_chat_flutter/test/src/avatars/group_avatar_test.dart index 3cc769213d..9614b24e92 100644 --- a/packages/stream_chat_flutter/test/src/avatars/group_avatar_test.dart +++ b/packages/stream_chat_flutter/test/src/avatars/group_avatar_test.dart @@ -16,8 +16,8 @@ void main() { late MockChannel channel; late MockChannelState channelState; - late final member = Member(user: User(id: 'alice', name: 'Alice')); - late final member2 = Member(user: User(id: 'bob', name: 'Bob')); + late final user1 = User(id: 'alice', name: 'Alice'); + late final user2 = User(id: 'bob', name: 'Bob'); setUpAll(() { client = MockClient(); @@ -26,7 +26,10 @@ void main() { when(() => channel.state!).thenReturn(channelState); when(() => channelState.membersStream).thenAnswer( - (_) => Stream>.value([member, member2]), + (_) => Stream>.value([ + Member(user: user1), + Member(user: user2), + ]), ); }); @@ -46,11 +49,8 @@ void main() { channel: channel, child: Scaffold( body: Center( - child: StreamGroupAvatar( - members: [ - member, - member2, - ], + child: StreamUserAvatarGroup( + users: [user1, user2], ), ), ), @@ -82,11 +82,8 @@ void main() { child: SizedBox( width: 100, height: 100, - child: StreamGroupAvatar( - members: [ - member, - member2, - ], + child: StreamUserAvatarGroup( + users: [user1, user2], ), ), ), diff --git a/packages/stream_chat_flutter/test/src/avatars/user_avatar_test.dart b/packages/stream_chat_flutter/test/src/avatars/user_avatar_test.dart index 6a95d3637e..6ec007a992 100644 --- a/packages/stream_chat_flutter/test/src/avatars/user_avatar_test.dart +++ b/packages/stream_chat_flutter/test/src/avatars/user_avatar_test.dart @@ -28,15 +28,17 @@ void main() { home: StreamChat( client: client, streamChatThemeData: StreamChatThemeData.light(), - child: Builder(builder: (context) { - return Scaffold( - body: Center( - child: StreamUserAvatar( - user: user, + child: Builder( + builder: (context) { + return Scaffold( + body: Center( + child: StreamUserAvatar( + user: user, + ), ), - ), - ); - }), + ); + }, + ), ), ), ); diff --git a/packages/stream_chat_flutter/test/src/bottom_sheets/attachment_modal_sheet_test.dart b/packages/stream_chat_flutter/test/src/bottom_sheets/attachment_modal_sheet_test.dart index 0779c67e87..a1ba65dc58 100644 --- a/packages/stream_chat_flutter/test/src/bottom_sheets/attachment_modal_sheet_test.dart +++ b/packages/stream_chat_flutter/test/src/bottom_sheets/attachment_modal_sheet_test.dart @@ -11,21 +11,23 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: Builder(builder: (context) { - return Center( - child: ElevatedButton( - child: const Text('Show Modal'), - onPressed: () => showModalBottomSheet( - context: context, - builder: (_) => AttachmentModalSheet( - onFileTap: () {}, - onPhotoTap: () {}, - onVideoTap: () {}, + body: Builder( + builder: (context) { + return Center( + child: ElevatedButton( + child: const Text('Show Modal'), + onPressed: () => showModalBottomSheet( + context: context, + builder: (_) => AttachmentModalSheet( + onFileTap: () {}, + onPhotoTap: () {}, + onVideoTap: () {}, + ), ), ), - ), - ); - }), + ); + }, + ), ), ), ); @@ -42,15 +44,17 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: Builder(builder: (context) { - return Center( - child: AttachmentModalSheet( - onPhotoTap: () => called = 1, - onFileTap: () {}, - onVideoTap: () {}, - ), - ); - }), + body: Builder( + builder: (context) { + return Center( + child: AttachmentModalSheet( + onPhotoTap: () => called = 1, + onFileTap: () {}, + onVideoTap: () {}, + ), + ); + }, + ), ), ), ); @@ -68,15 +72,17 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: Builder(builder: (context) { - return Center( - child: AttachmentModalSheet( - onPhotoTap: () {}, - onVideoTap: () => called = 1, - onFileTap: () {}, - ), - ); - }), + body: Builder( + builder: (context) { + return Center( + child: AttachmentModalSheet( + onPhotoTap: () {}, + onVideoTap: () => called = 1, + onFileTap: () {}, + ), + ); + }, + ), ), ), ); @@ -94,15 +100,17 @@ void main() { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: Builder(builder: (context) { - return Center( - child: AttachmentModalSheet( - onPhotoTap: () {}, - onVideoTap: () {}, - onFileTap: () => called = 1, - ), - ); - }), + body: Builder( + builder: (context) { + return Center( + child: AttachmentModalSheet( + onPhotoTap: () {}, + onVideoTap: () {}, + onFileTap: () => called = 1, + ), + ); + }, + ), ), ), ); @@ -121,15 +129,17 @@ void main() { constraints: const BoxConstraints.tightFor(width: 300, height: 300), builder: () => MaterialAppWrapper( home: Scaffold( - body: Builder(builder: (context) { - return Center( - child: AttachmentModalSheet( - onPhotoTap: () {}, - onVideoTap: () {}, - onFileTap: () {}, - ), - ); - }), + body: Builder( + builder: (context) { + return Center( + child: AttachmentModalSheet( + onPhotoTap: () {}, + onVideoTap: () {}, + onFileTap: () {}, + ), + ); + }, + ), ), ), ); diff --git a/packages/stream_chat_flutter/test/src/bottom_sheets/error_alert_sheet_test.dart b/packages/stream_chat_flutter/test/src/bottom_sheets/error_alert_sheet_test.dart index 7dc89683b8..4915e5f6bb 100644 --- a/packages/stream_chat_flutter/test/src/bottom_sheets/error_alert_sheet_test.dart +++ b/packages/stream_chat_flutter/test/src/bottom_sheets/error_alert_sheet_test.dart @@ -9,18 +9,15 @@ import '../mocks.dart'; void main() { group('ErrorAlertSheet tests', () { - const methodChannel = - MethodChannel('dev.fluttercommunity.plus/connectivity_status'); + const methodChannel = MethodChannel('dev.fluttercommunity.plus/connectivity_status'); setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, - (MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(methodChannel, ( + MethodCall methodCall, + ) async { if (methodCall.method == 'listen') { try { - await TestDefaultBinaryMessengerBinding - .instance.defaultBinaryMessenger - .handlePlatformMessage( + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( methodChannel.name, methodChannel.codec.encodeSuccessEnvelope(['wifi']), (_) {}, @@ -93,8 +90,7 @@ void main() { ); tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(methodChannel, null); }); }); } diff --git a/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/attachment_modal_sheet_0.png b/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/attachment_modal_sheet_0.png index d37466f32f..d61f6d8fcc 100644 Binary files a/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/attachment_modal_sheet_0.png and b/packages/stream_chat_flutter/test/src/bottom_sheets/goldens/ci/attachment_modal_sheet_0.png differ 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 19410a8b8b..b73cd7f166 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 30c4d610b7..7b9c75af08 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/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_header_test.dart index 0518fd84fd..3f1653c098 100644 --- a/packages/stream_chat_flutter/test/src/channel/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_header_test.dart @@ -28,8 +28,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(user); - when(() => clientState.currentUserStream) - .thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -37,23 +36,19 @@ void main() { when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); when(() => channel.name).thenReturn('test'); - when(() => channel.imageStream) - .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.imageStream).thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.connected)); - when(() => channelState.unreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.connected)); + when(() => channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => clientState.totalUnreadCountStream).thenAnswer((i) => Stream.value(1)); when(() => channelState.membersStream).thenAnswer( (i) => Stream.value([ Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -99,8 +94,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(user); - when(() => clientState.currentUserStream) - .thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -108,18 +102,16 @@ void main() { when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); when(() => channel.name).thenReturn('test'); - when(() => channel.imageStream) - .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.imageStream).thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); - when(() => channelState.unreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); when(() => channelState.membersStream).thenAnswer( (i) => Stream.value([ Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -128,13 +120,10 @@ void main() { user: User(id: 'user-id'), ), ]); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); - when(() => client.wsConnectionStatus) - .thenReturn(ConnectionStatus.disconnected); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); + when(() => client.wsConnectionStatus).thenReturn(ConnectionStatus.disconnected); when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => clientState.totalUnreadCountStream).thenAnswer((i) => Stream.value(1)); await tester.pumpWidget( MaterialApp( @@ -155,13 +144,8 @@ void main() { // wait for the initial state to be rendered. await tester.pumpAndSettle(); - expect( - tester - .widget(find.byType(StreamInfoTile)) - .showMessage, - true); - expect(tester.widget(find.byType(StreamInfoTile)).message, - 'Disconnected'); + expect(tester.widget(find.byType(StreamInfoTile)).showMessage, true); + expect(tester.widget(find.byType(StreamInfoTile)).message, 'Disconnected'); }, ); @@ -177,8 +161,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(user); - when(() => clientState.currentUserStream) - .thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -186,18 +169,16 @@ void main() { when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); when(() => channel.name).thenReturn('test'); - when(() => channel.imageStream) - .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.imageStream).thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); - when(() => channelState.unreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); when(() => channelState.membersStream).thenAnswer( (i) => Stream.value([ Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -206,11 +187,9 @@ void main() { user: User(id: 'user-id'), ), ]); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => clientState.totalUnreadCountStream).thenAnswer((i) => Stream.value(1)); await tester.pumpWidget( MaterialApp( @@ -231,13 +210,8 @@ void main() { await tester.pump(); - expect( - tester - .widget(find.byType(StreamInfoTile)) - .showMessage, - true); - expect(tester.widget(find.byType(StreamInfoTile)).message, - 'Reconnecting...'); + expect(tester.widget(find.byType(StreamInfoTile)).showMessage, true); + expect(tester.widget(find.byType(StreamInfoTile)).message, 'Reconnecting...'); }, ); @@ -253,28 +227,28 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(user); - when(() => clientState.currentUserStream) - .thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); when(() => channel.isMuted).thenReturn(false); when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); + when(() => channel.extraDataStream).thenAnswer( + (i) => Stream.value({ + 'name': 'test', + }), + ); when(() => channel.extraData).thenReturn({ 'name': 'test', }); when(() => channelState.unreadCount).thenReturn(1); - when(() => channelState.unreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); when(() => channelState.membersStream).thenAnswer( (i) => Stream.value([ Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -283,10 +257,8 @@ void main() { user: User(id: 'user-id'), ), ]); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => clientState.totalUnreadCountStream).thenAnswer((i) => Stream.value(1)); await tester.pumpWidget( MaterialAppWrapper( @@ -296,6 +268,7 @@ void main() { channel: channel, child: const Scaffold( body: StreamChannelHeader( + centerTitle: true, leading: Text('leading'), subtitle: Text('subtitle'), actions: [ @@ -337,8 +310,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(user); - when(() => clientState.currentUserStream) - .thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -346,18 +318,16 @@ void main() { when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); when(() => channel.name).thenReturn('test'); - when(() => channel.imageStream) - .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.imageStream).thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); - when(() => channelState.unreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); when(() => channelState.membersStream).thenAnswer( (i) => Stream.value([ Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -366,8 +336,7 @@ void main() { user: User(id: 'user-id'), ), ]); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); await tester.pumpWidget( MaterialApp( @@ -391,16 +360,10 @@ void main() { expect(find.byType(StreamBackButton), findsNothing); expect( - tester - .widget(find.byType(StreamChannelInfo)) - .showTypingIndicator, + tester.widget(find.byType(StreamChannelInfo)).showTypingIndicator, false, ); - expect( - tester - .widget(find.byType(StreamInfoTile)) - .showMessage, - false); + expect(tester.widget(find.byType(StreamInfoTile)).showMessage, false); }, ); @@ -416,8 +379,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(user); - when(() => clientState.currentUserStream) - .thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -425,18 +387,16 @@ void main() { when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); when(() => channel.name).thenReturn('test'); - when(() => channel.imageStream) - .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.imageStream).thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); - when(() => channelState.unreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); when(() => channelState.membersStream).thenAnswer( (i) => Stream.value([ Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -445,11 +405,9 @@ void main() { user: User(id: 'user-id'), ), ]); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => clientState.totalUnreadCountStream).thenAnswer((i) => Stream.value(1)); var backPressed = false; var imageTapped = false; @@ -500,8 +458,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(user); - when(() => clientState.currentUserStream) - .thenAnswer((_) => Stream.value(user)); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(user)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); @@ -509,23 +466,19 @@ void main() { when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); when(() => channel.name).thenReturn('test'); - when(() => channel.imageStream) - .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.imageStream).thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); when(() => channelState.unreadCount).thenReturn(1); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.connected)); - when(() => channelState.unreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.connected)); + when(() => channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => clientState.totalUnreadCountStream).thenAnswer((i) => Stream.value(1)); when(() => channelState.membersStream).thenAnswer( (i) => Stream.value([ Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ diff --git a/packages/stream_chat_flutter/test/src/channel/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_image_test.dart index 6bcde77973..a66eef0f64 100644 --- a/packages/stream_chat_flutter/test/src/channel/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_image_test.dart @@ -1,8 +1,8 @@ -import 'package:cached_network_image/cached_network_image.dart'; 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 'package:stream_core_flutter/stream_core_flutter.dart'; import '../mocks.dart'; @@ -21,8 +21,7 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); when(() => channel.name).thenReturn('test'); - when(() => channel.imageStream) - .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); + when(() => channel.imageStream).thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); await tester.pumpWidget( @@ -42,9 +41,8 @@ void main() { // wait for the initial state to be rendered. await tester.pumpAndSettle(); - final image = - tester.widget(find.byType(CachedNetworkImage)); - expect(image.imageUrl, 'https://bit.ly/321RmWb'); + final image = tester.widget(find.byType(StreamNetworkImage)); + expect(image.props.url, 'https://bit.ly/321RmWb'); }, ); @@ -62,6 +60,7 @@ void main() { when(() => channel.client).thenReturn(client); when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); when(() => channel.name).thenReturn('test'); + when(() => channel.isDistinct).thenReturn(true); when(() => channel.imageStream).thenAnswer((i) => Stream.value(null)); when(() => channel.image).thenReturn(null); when(() => channelState.membersStream).thenAnswer( @@ -76,7 +75,7 @@ void main() { id: 'user-id2', image: 'testimage', ), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -90,7 +89,7 @@ void main() { Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]); when(() => clientState.usersStream).thenAnswer( (i) => Stream.value({ @@ -121,9 +120,8 @@ void main() { // wait for the initial state to be rendered. await tester.pumpAndSettle(); - final image = - tester.widget(find.byType(CachedNetworkImage)); - expect(image.imageUrl, 'testimage'); + final image = tester.widget(find.byType(StreamNetworkImage)); + expect(image.props.url, 'testimage'); }, ); @@ -140,6 +138,7 @@ void main() { when(() => clientState.currentUser).thenReturn(currentUser); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); + when(() => channel.isDistinct).thenReturn(false); when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); when(() => channel.name).thenReturn('test'); when(() => channel.imageStream).thenAnswer((i) => Stream.value(null)); @@ -167,8 +166,7 @@ void main() { ), ]; when(() => channelState.members).thenReturn(members); - when(() => channelState.membersStream) - .thenAnswer((_) => Stream.value(members)); + when(() => channelState.membersStream).thenAnswer((_) => Stream.value(members)); await tester.pumpWidget( MaterialApp( @@ -187,55 +185,17 @@ void main() { // wait for the initial state to be rendered. await tester.pumpAndSettle(); - final image = - tester.widget(find.byType(StreamGroupAvatar)); - final otherMembers = members.where((it) => it.userId != currentUser.id); - expect( - image.members.map((it) => it.user?.id), - otherMembers.map((it) => it.user?.id), - ); - }, - ); + // The new StreamChannelAvatar uses StreamUserAvatarGroup internally + // for multi-member channels + final avatarGroup = find.byType(StreamUserAvatarGroup); + expect(avatarGroup, findsOneWidget); - testWidgets( - 'using select: true should show a selection border', - (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.client).thenReturn(client); - when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); - when(() => channel.name).thenReturn('test'); - when(() => channel.imageStream) - .thenAnswer((i) => Stream.value('https://bit.ly/321RmWb')); - when(() => channel.image).thenReturn('https://bit.ly/321RmWb'); - - await tester.pumpWidget( - MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamChannelAvatar( - channel: channel, - selected: true, - ), - ), - ), - ), - ), - ); - - // wait for the initial state to be rendered. - await tester.pumpAndSettle(); - - expect(find.byKey(const Key('selectedImage')), findsOneWidget); + // Verify user avatars are shown for all members + expect(find.byType(StreamUserAvatar), findsNWidgets(members.length)); }, ); + + // Note: The 'selected' parameter has been removed in the redesigned + // StreamChannelAvatar component. Selection states should now be handled + // at the parent widget level if needed. } diff --git a/packages/stream_chat_flutter/test/src/channel/channel_list_header_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_list_header_test.dart index 6193d08d97..a41a80200b 100644 --- a/packages/stream_chat_flutter/test/src/channel/channel_list_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_list_header_test.dart @@ -2,6 +2,7 @@ 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 'package:stream_core_flutter/stream_core_flutter.dart'; import '../mocks.dart'; @@ -14,8 +15,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.connected)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.connected)); await tester.pumpWidget( MaterialApp( @@ -29,10 +29,9 @@ void main() { ); await tester.pumpAndSettle(); - final userAvatar = - tester.widget(find.byType(StreamUserAvatar)); + final userAvatar = tester.widget(find.byType(StreamUserAvatar)); expect(userAvatar.user, clientState.currentUser); - expect(find.byType(StreamNeumorphicButton), findsOneWidget); + expect(find.byType(StreamButton), findsOneWidget); expect(find.text('Stream Chat'), findsOneWidget); }, ); @@ -45,8 +44,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.disconnected)); await tester.pumpWidget( MaterialApp( @@ -74,8 +72,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); await tester.pumpWidget( MaterialApp( @@ -103,8 +100,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); await tester.pumpWidget( MaterialApp( @@ -141,8 +137,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); var tapped = false; @@ -175,8 +170,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.connected)); var tapped = 0; await tester.pumpWidget( @@ -199,7 +193,7 @@ void main() { await tester.pump(); await tester.tap(find.byType(StreamUserAvatar)); - await tester.tap(find.byType(StreamNeumorphicButton)); + await tester.tap(find.byIcon(StreamIconData.plus20)); expect(tapped, 2); }, ); diff --git a/packages/stream_chat_flutter/test/src/channel/channel_name_test.dart b/packages/stream_chat_flutter/test/src/channel/channel_name_test.dart index 22cea8a99b..5bd913308c 100644 --- a/packages/stream_chat_flutter/test/src/channel/channel_name_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/channel_name_test.dart @@ -25,14 +25,13 @@ void main() { when(() => channel.nameStream).thenAnswer((_) => Stream.value('test')); when(() => channel.name).thenReturn('test'); when(() => channelState.unreadCount).thenReturn(1); - when(() => channelState.unreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); when(() => channelState.membersStream).thenAnswer( (_) => Stream.value([ Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -45,14 +44,14 @@ void main() { Message( text: 'hello', user: User(id: 'other-user'), - ) + ), ]); when(() => channelState.messagesStream).thenAnswer( (i) => Stream.value([ Message( text: 'hello', user: User(id: 'other-user'), - ) + ), ]), ); 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 756f0f9c68..a6b856682d 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/channel/stream_message_preview_text_test.dart b/packages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dart index 4585e577c2..6eca2e4cb3 100644 --- a/packages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dart +++ b/packages/stream_chat_flutter/test/src/channel/stream_message_preview_text_test.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -19,7 +21,6 @@ void main() { when(() => clientState.currentUser).thenReturn(currentUser); }); - // Helper to pump the message preview widget Future pumpMessagePreview( WidgetTester tester, Message message, { @@ -57,7 +58,7 @@ void main() { await tester.pump(); } - group('StreamMessagePreviewText', () { + group('Message types', () { testWidgets('renders regular text message', (tester) async { final message = Message( text: 'Hello, world!', @@ -67,9 +68,10 @@ void main() { await pumpMessagePreview(tester, message); expect(find.text('Hello, world!'), findsOneWidget); + expect(_findIcons(tester), isEmpty); }); - testWidgets('renders deleted message', (tester) async { + testWidgets('renders deleted message with ban icon', (tester) async { final message = Message( text: 'Original message', type: MessageType.deleted, @@ -79,10 +81,19 @@ void main() { await pumpMessagePreview(tester, message); - expect(find.text('Message deleted'), findsOneWidget); + final icons = _findIcons(tester); + expect(icons, hasLength(1)); + expect(icons.first.size, 16); + + expect(_extractText(tester), 'Message deleted'); + + final span = _getPreviewSpan(tester); + final styledSpans = _findTextSpans(span); + final deletedSpan = styledSpans.firstWhere((s) => s.text == 'Message deleted'); + expect(deletedSpan.style?.color, isNotNull); }); - testWidgets('renders system message', (tester) async { + testWidgets('renders system message with text', (tester) async { final message = Message( text: 'User joined the channel', type: MessageType.system, @@ -91,9 +102,10 @@ void main() { await pumpMessagePreview(tester, message); expect(find.text('User joined the channel'), findsOneWidget); + expect(_findIcons(tester), isEmpty); }); - testWidgets('renders empty system message', (tester) async { + testWidgets('renders system message without text as fallback label', (tester) async { final message = Message(type: MessageType.system); await pumpMessagePreview(tester, message); @@ -101,7 +113,7 @@ void main() { expect(find.text('System Message'), findsOneWidget); }); - testWidgets('renders empty message with no attachments', (tester) async { + testWidgets('renders empty message with no text or attachments', (tester) async { final message = Message( text: '', user: User(id: 'other-user-id', name: 'Other User'), @@ -109,10 +121,35 @@ void main() { await pumpMessagePreview(tester, message); - expect(find.text(''), findsOneWidget); + expect(find.byType(StreamMessagePreviewText), findsOneWidget); + expect(_findIcons(tester), isEmpty); }); - testWidgets('renders message with mentioned users in bold', (tester) async { + testWidgets('renders null text message as empty', (tester) async { + final message = Message( + user: User(id: 'other-user-id', name: 'Other User'), + ); + + await pumpMessagePreview(tester, message); + + expect(find.byType(StreamMessagePreviewText), findsOneWidget); + }); + + testWidgets('trims whitespace-only text as empty', (tester) async { + final message = Message( + text: ' ', + user: User(id: 'other-user-id', name: 'Other User'), + ); + + await pumpMessagePreview(tester, message); + + expect(find.byType(StreamMessagePreviewText), findsOneWidget); + expect(_findIcons(tester), isEmpty); + }); + }); + + group('Mentions', () { + testWidgets('renders mentioned users with bold styling', (tester) async { final mentionedUser = User(id: 'mentioned-id', name: 'Mentioned User'); final message = Message( text: 'Hello @Mentioned User, how are you?', @@ -124,428 +161,649 @@ void main() { expect(find.text('Hello @Mentioned User, how are you?'), findsOneWidget); - // Find the rich text and verify that it contains a valid TextSpan - final textWidget = tester.widget(find.byType(Text).last); - expect(textWidget.textSpan, isNotNull); + final span = _getPreviewSpan(tester); + final mentionSpan = _findTextSpans(span).firstWhere( + (s) => s.text == '@Mentioned User', + ); + expect(mentionSpan.style?.fontWeight, FontWeight.bold); }); - group('Attachments', () { - testWidgets('renders image attachment', (tester) async { - final message = Message( - text: '', - user: User(id: 'other-user-id', name: 'Other User'), - attachments: [ - Attachment( - type: AttachmentType.image, - ), - ], - ); + testWidgets('renders multiple mentions with bold styling', (tester) async { + final user1 = User(id: 'user-1', name: 'Alice'); + final user2 = User(id: 'user-2', name: 'Bob'); + final message = Message( + text: 'Hey @Alice and @Bob!', + user: User(id: 'other-user-id', name: 'Other User'), + mentionedUsers: [user1, user2], + ); - await pumpMessagePreview(tester, message); + await pumpMessagePreview(tester, message, textStyle: const TextStyle()); - expect(find.text('📷 Image'), findsOneWidget); - }); + expect(find.text('Hey @Alice and @Bob!'), findsOneWidget); - testWidgets('renders image attachment with text', (tester) async { - final message = Message( - text: 'Check this out', - user: User(id: 'other-user-id', name: 'Other User'), - attachments: [ - Attachment( - type: AttachmentType.image, - ), - ], - ); + final span = _getPreviewSpan(tester); + final textSpans = _findTextSpans(span); + final aliceSpan = textSpans.firstWhere((s) => s.text == '@Alice'); + final bobSpan = textSpans.firstWhere((s) => s.text == '@Bob'); + expect(aliceSpan.style?.fontWeight, FontWeight.bold); + expect(bobSpan.style?.fontWeight, FontWeight.bold); + }); - await pumpMessagePreview(tester, message); + testWidgets('renders message without matching mention as plain text', (tester) async { + final mentionedUser = User(id: 'mentioned-id', name: 'NoMatch'); + final message = Message( + text: 'Hello @SomeoneElse', + user: User(id: 'other-user-id', name: 'Other User'), + mentionedUsers: [mentionedUser], + ); - expect(find.text('📷 Check this out'), findsOneWidget); - }); + await pumpMessagePreview(tester, message); - testWidgets('renders video attachment', (tester) async { - final message = Message( - text: '', - user: User(id: 'other-user-id', name: 'Other User'), - attachments: [ - Attachment( - type: AttachmentType.video, - ), - ], - ); + expect(find.text('Hello @SomeoneElse'), findsOneWidget); + }); + }); + + group('Single attachments', () { + testWidgets('image attachment shows camera icon and "Photo" label', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [Attachment(type: AttachmentType.image)], + ); - await pumpMessagePreview(tester, message); + await pumpMessagePreview(tester, message); - expect(find.text('📹 Video'), findsOneWidget); - }); + final icons = _findIcons(tester); + expect(icons, hasLength(1)); + expect(icons.first.size, 16); + expect(_extractText(tester), 'Photo'); + }); - testWidgets('renders video attachment with text', (tester) async { - final message = Message( - text: 'Check this out', - user: User(id: 'other-user-id', name: 'Other User'), - attachments: [ - Attachment( - type: AttachmentType.video, - ), - ], - ); + testWidgets('image attachment with caption shows icon and caption', (tester) async { + final message = Message( + text: 'Check this out', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [Attachment(type: AttachmentType.image)], + ); - await pumpMessagePreview(tester, message); + await pumpMessagePreview(tester, message); - expect(find.text('📹 Check this out'), findsOneWidget); - }); + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'Check this out'); + }); - testWidgets('renders file attachment', (tester) async { - final message = Message( - text: '', - user: User(id: 'other-user-id', name: 'Other User'), - attachments: [ - Attachment( - type: AttachmentType.file, - title: 'document.pdf', - ), - ], - ); + testWidgets('video attachment shows video icon and "Video" label', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [Attachment(type: AttachmentType.video)], + ); - await pumpMessagePreview(tester, message); + await pumpMessagePreview(tester, message); - expect(find.text('📄 document.pdf'), findsOneWidget); - }); + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'Video'); + }); - testWidgets('renders audio attachment', (tester) async { - final message = Message( - text: '', - user: User(id: 'other-user-id', name: 'Other User'), - attachments: [ - Attachment( - type: AttachmentType.audio, - ), - ], - ); + testWidgets('video attachment with caption shows icon and caption', (tester) async { + final message = Message( + text: 'Watch this', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [Attachment(type: AttachmentType.video)], + ); - await pumpMessagePreview(tester, message); + await pumpMessagePreview(tester, message); - expect(find.text('🎧 Audio'), findsOneWidget); - }); + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'Watch this'); + }); - testWidgets('renders audio attachment with text', (tester) async { - final message = Message( - text: 'Check this out', - user: User(id: 'other-user-id', name: 'Other User'), - attachments: [ - Attachment( - type: AttachmentType.audio, - ), - ], - ); + testWidgets('file attachment shows file icon and "File" fallback label', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [Attachment(type: AttachmentType.file)], + ); - await pumpMessagePreview(tester, message); + await pumpMessagePreview(tester, message); - expect(find.text('🎧 Check this out'), findsOneWidget); - }); + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'File'); + }); - testWidgets('renders giphy attachment with text', (tester) async { - final message = Message( - text: 'Check this out', - user: User(id: 'other-user-id', name: 'Other User'), - attachments: [ - Attachment( - type: AttachmentType.giphy, - ), - ], - ); + testWidgets('file attachment with file name shows the file name', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [ + Attachment( + type: AttachmentType.file, + file: AttachmentFile(size: 100, bytes: Uint8List(100), name: 'report.pdf'), + ), + ], + ); - await pumpMessagePreview(tester, message); + await pumpMessagePreview(tester, message); - expect(find.text('/giphy Check this out'), findsOneWidget); - }); + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'report.pdf'); + }); - testWidgets('renders voice recording attachment', (tester) async { - final message = Message( - text: '', - user: User(id: 'other-user-id', name: 'Other User'), - attachments: [ - Attachment( - type: AttachmentType.voiceRecording, - ), - ], - ); + testWidgets('audio attachment shows microphone icon and "Audio" label', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [Attachment(type: AttachmentType.audio)], + ); - await pumpMessagePreview(tester, message); + await pumpMessagePreview(tester, message); - expect(find.text('🎤 Voice Recording'), findsOneWidget); - }); + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'Audio'); + }); - testWidgets('renders unknown attachment type with text', (tester) async { - final message = Message( - text: 'Some text', - user: User(id: 'other-user-id', name: 'Other User'), - attachments: [ - Attachment( - type: 'unknown', - ), - ], - ); + testWidgets('audio attachment with caption shows icon and caption', (tester) async { + final message = Message( + text: 'New podcast episode', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [Attachment(type: AttachmentType.audio)], + ); + + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'New podcast episode'); + }); + + testWidgets('voice recording shows microphone icon with duration', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [Attachment(type: AttachmentType.voiceRecording)], + ); - await pumpMessagePreview(tester, message); + await pumpMessagePreview(tester, message); - expect(find.text('Some text'), findsOneWidget); - }); + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), contains('Voice Recording')); + expect(_extractText(tester), contains('00:00')); }); - group('Poll Tests', () { - testWidgets('renders poll with latest voter (current user)', - (tester) async { - final voterPoll = Poll( + testWidgets('voice recording with duration shows formatted time', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [ + Attachment( + type: AttachmentType.voiceRecording, + extraData: const {'duration': 125}, + ), + ], + ); + + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), contains('Voice Recording')); + expect(_extractText(tester), contains('02:05')); + }); + + testWidgets('giphy attachment shows /giphy text prefix (no icon)', (tester) async { + final message = Message( + text: 'funny cat', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [Attachment(type: AttachmentType.giphy)], + ); + + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), isEmpty); + expect(_extractText(tester), contains('/giphy')); + expect(_extractText(tester), contains('funny cat')); + }); + + testWidgets('unknown attachment type shows file icon with text', (tester) async { + final message = Message( + text: 'Some text', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [Attachment(type: 'custom_type')], + ); + + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'Some text'); + }); + + testWidgets('unknown attachment type without text shows file icon only', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [Attachment(type: 'custom_type')], + ); + + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), isEmpty); + }); + }); + + group('Multiple same-type attachments', () { + testWidgets('multiple images show camera icon and photo count', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [ + Attachment(type: AttachmentType.image), + Attachment(type: AttachmentType.image), + Attachment(type: AttachmentType.image), + ], + ); + + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), '3 photos'); + }); + + testWidgets('multiple videos show video icon and video count', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [ + Attachment(type: AttachmentType.video), + Attachment(type: AttachmentType.video), + ], + ); + + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), '2 videos'); + }); + + testWidgets('multiple files show file icon and file count', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [ + Attachment(type: AttachmentType.file), + Attachment(type: AttachmentType.file), + Attachment(type: AttachmentType.file), + Attachment(type: AttachmentType.file), + ], + ); + + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), '4 files'); + }); + + testWidgets('multiple images with caption show icon and caption text', (tester) async { + final message = Message( + text: 'Vacation photos', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [ + Attachment(type: AttachmentType.image), + Attachment(type: AttachmentType.image), + ], + ); + + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'Vacation photos'); + }); + }); + + group('Mixed-type attachments', () { + testWidgets('mixed types show generic file icon and total count', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [ + Attachment(type: AttachmentType.image), + Attachment(type: AttachmentType.video), + ], + ); + + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), '2 files'); + }); + + testWidgets('three mixed types show count of all', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [ + Attachment(type: AttachmentType.image), + Attachment(type: AttachmentType.video), + Attachment(type: AttachmentType.file), + ], + ); + + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), '3 files'); + }); + + testWidgets('mixed types with caption show generic icon and caption', (tester) async { + final message = Message( + text: 'Mixed media', + user: User(id: 'other-user-id', name: 'Other User'), + attachments: [ + Attachment(type: AttachmentType.image), + Attachment(type: AttachmentType.file), + ], + ); + + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'Mixed media'); + }); + }); + + group('Polls', () { + testWidgets('poll shows chart icon and poll name', (tester) async { + final message = Message( + user: User(id: 'other-user-id', name: 'Poll Creator'), + poll: Poll( name: 'Favorite Color?', options: const [ PollOption(id: 'option-1', text: 'Red'), PollOption(id: 'option-2', text: 'Blue'), ], - latestVotesByOption: { - 'option-1': [ - PollVote( - user: currentUser, - optionId: 'option-1', - ), - ], - }, - ); - - final message = Message( - user: User(id: 'other-user-id', name: 'Poll Creator'), - poll: voterPoll, - ); + ), + ); - await pumpMessagePreview(tester, message); + await pumpMessagePreview(tester, message); - expect(find.text('📊 You voted: "Favorite Color?"'), findsOneWidget); - }); + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'Favorite Color?'); + }); - testWidgets('renders poll with latest voter (another user)', - (tester) async { - final voter = User(id: 'voter-id', name: 'Voter'); - final voterPoll = Poll( - name: 'Favorite Color?', + testWidgets('poll with empty name shows chart icon only', (tester) async { + final message = Message( + user: User(id: 'other-user-id', name: 'Message Sender'), + poll: Poll( + name: ' ', options: const [ PollOption(id: 'option-1', text: 'Red'), PollOption(id: 'option-2', text: 'Blue'), ], - latestVotesByOption: { - 'option-1': [ - PollVote( - user: voter, - optionId: 'option-1', - ), - ], - }, - ); - - final message = Message( - user: User(id: 'other-user-id', name: 'Poll Creator'), - poll: voterPoll, - ); - - await pumpMessagePreview(tester, message); - - expect(find.text('📊 Voter voted: "Favorite Color?"'), findsOneWidget); - }); - - testWidgets('renders poll with creator (current user)', (tester) async { - final message = Message( - user: User(id: 'other-user-id', name: 'Message Sender'), - poll: Poll( - name: 'Favorite Color?', - options: const [ - PollOption(id: 'option-1', text: 'Red'), - PollOption(id: 'option-2', text: 'Blue'), - ], - createdBy: currentUser, - ), - ); - - await pumpMessagePreview(tester, message); - - expect(find.text('📊 You created: "Favorite Color?"'), findsOneWidget); - }); - - testWidgets('renders poll with creator (another user)', (tester) async { - final creator = User(id: 'creator-id', name: 'Alex'); - final message = Message( - user: User(id: 'other-user-id', name: 'Message Sender'), - poll: Poll( - name: 'Favorite Color?', - options: const [ - PollOption(id: 'option-1', text: 'Red'), - PollOption(id: 'option-2', text: 'Blue'), - ], - createdBy: creator, - ), - ); - - await pumpMessagePreview(tester, message); - - expect(find.text('📊 Alex created: "Favorite Color?"'), findsOneWidget); - }); - - testWidgets('renders poll with only name', (tester) async { - final message = Message( - user: User(id: 'other-user-id', name: 'Message Sender'), - poll: Poll( - name: 'Favorite Color?', - options: const [ - PollOption(id: 'option-1', text: 'Red'), - PollOption(id: 'option-2', text: 'Blue'), - ], - ), - ); - - await pumpMessagePreview(tester, message); - - expect(find.text('📊 Favorite Color?'), findsOneWidget); - }); - - testWidgets('renders poll with empty name', (tester) async { - final message = Message( - user: User(id: 'other-user-id', name: 'Message Sender'), - poll: Poll( - name: ' ', - options: const [ - PollOption(id: 'option-1', text: 'Red'), - PollOption(id: 'option-2', text: 'Blue'), - ], - ), - ); + ), + ); - await pumpMessagePreview(tester, message); + await pumpMessagePreview(tester, message); - expect(find.text('📊'), findsOneWidget); - }); + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), isEmpty); }); - testWidgets('supports different language for translation', (tester) async { + testWidgets('poll in group channel doesnt includes sender prefix', (tester) async { + final channel = ChannelModel( + id: 'test-channel', + type: 'messaging', + memberCount: 3, + ); + final message = Message( - text: 'Hello, world!', + user: User(id: 'test-user-id', name: 'Test User'), + poll: Poll( + name: 'Lunch spot?', + options: const [ + PollOption(id: 'option-1', text: 'Pizza'), + PollOption(id: 'option-2', text: 'Sushi'), + ], + ), + ); + + await pumpMessagePreview(tester, message, channel: channel); + + expect(_findIcons(tester), hasLength(1)); + + final text = _extractText(tester); + expect(text, isNot(contains('You: '))); + expect(text, contains('Lunch spot?')); + }); + }); + + group('Locations', () { + testWidgets('static location shows map pin icon and location label', (tester) async { + final message = Message( + text: '', user: User(id: 'other-user-id', name: 'Other User'), - i18n: const { - 'fr_text': 'Bonjour, monde!', - }, + sharedLocation: Location( + latitude: 37.7749, + longitude: -122.4194, + ), ); - await pumpMessagePreview(tester, message, language: 'fr'); + await pumpMessagePreview(tester, message); - expect(find.text('Bonjour, monde!'), findsOneWidget); + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), contains('Location')); + expect(_extractText(tester), isNot(contains('Live'))); }); - group('Channel-specific behaviors', () { - testWidgets( - 'prepends "You:" for current user\'s messages in group channels', - (tester) async { - final channel = ChannelModel( - id: 'test-channel', - type: 'messaging', - memberCount: 3, - ); + testWidgets('live location shows map pin icon and live location label', (tester) async { + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Other User'), + sharedLocation: Location( + latitude: 37.7749, + longitude: -122.4194, + endAt: DateTime.now().add(const Duration(minutes: 15)), + ), + ); - final message = Message( - text: 'Hello everyone', - user: User(id: 'test-user-id', name: 'Test User'), // Current user - ); + await pumpMessagePreview(tester, message); - await pumpMessagePreview(tester, message, channel: channel); + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), contains('Live Location')); + }); - expect(find.text('You: Hello everyone'), findsOneWidget); - }, + testWidgets('location with caption shows map pin icon and caption text', (tester) async { + final message = Message( + text: 'Meet me here', + user: User(id: 'other-user-id', name: 'Other User'), + sharedLocation: Location( + latitude: 37.7749, + longitude: -122.4194, + ), ); - testWidgets( - 'prepends author name for other messages in group channels', - (tester) async { - final channel = ChannelModel( - id: 'test-channel', - type: 'messaging', - memberCount: 3, - ); + await pumpMessagePreview(tester, message); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'Meet me here'); + }); + }); + + group('Channel context', () { + testWidgets('group channel prepends bold "You:" for current user', (tester) async { + final channel = ChannelModel( + id: 'test-channel', + type: 'messaging', + memberCount: 3, + ); + + final message = Message( + text: 'Hello everyone', + user: User(id: 'test-user-id', name: 'Test User'), + ); - final message = Message( - text: 'Hello everyone', - user: User(id: 'other-user-id', name: 'Jane Doe'), - ); + await pumpMessagePreview(tester, message, channel: channel); + + expect(find.text('You: Hello everyone'), findsOneWidget); + + final span = _getPreviewSpan(tester); + final youSpan = _findTextSpans(span).firstWhere((s) => s.text == 'You: '); + expect(youSpan.style?.fontWeight, FontWeight.bold); + }); + + testWidgets('group channel prepends bold first name for other users', (tester) async { + final channel = ChannelModel( + id: 'test-channel', + type: 'messaging', + memberCount: 3, + ); + + final message = Message( + text: 'Hello everyone', + user: User(id: 'other-user-id', name: 'Jane Doe'), + ); - await pumpMessagePreview(tester, message, channel: channel); + await pumpMessagePreview(tester, message, channel: channel); - expect(find.text('Jane Doe: Hello everyone'), findsOneWidget); + expect(find.text('Jane: Hello everyone'), findsOneWidget); + + final span = _getPreviewSpan(tester); + final nameSpan = _findTextSpans(span).firstWhere((s) => s.text == 'Jane: '); + expect(nameSpan.style?.fontWeight, FontWeight.bold); + }); + + testWidgets('group channel skips prefix when message has no user', (tester) async { + final channel = ChannelModel( + id: 'test-channel', + type: 'messaging', + memberCount: 3, + ); + + final message = Message(text: 'Hello'); + + await pumpMessagePreview(tester, message, channel: channel); + + expect(find.text('Hello'), findsOneWidget); + }); + + testWidgets('1:1 channel does not prepend author name', (tester) async { + final channel = ChannelModel( + id: 'test-channel', + type: 'messaging', + memberCount: 2, + ); + + final message = Message( + text: 'Hello there', + user: User(id: 'other-user-id', name: 'Jane Doe'), + ); + + await pumpMessagePreview(tester, message, channel: channel); + + expect(find.text('Hello there'), findsOneWidget); + }); + + testWidgets('no channel does not prepend author name', (tester) async { + final message = Message( + text: 'Hello there', + user: User(id: 'other-user-id', name: 'Jane Doe'), + ); + + await pumpMessagePreview(tester, message); + + expect(find.text('Hello there'), findsOneWidget); + }); + + testWidgets('group channel with attachment includes sender prefix', (tester) async { + final channel = ChannelModel( + id: 'test-channel', + type: 'messaging', + memberCount: 3, + ); + + final message = Message( + text: '', + user: User(id: 'other-user-id', name: 'Jane Doe'), + attachments: [Attachment(type: AttachmentType.image)], + ); + + await pumpMessagePreview(tester, message, channel: channel); + + final text = _extractText(tester); + expect(text, contains('Jane: ')); + expect(text, contains('Photo')); + }); + }); + + group('Translations', () { + testWidgets('uses explicit language parameter for translation', (tester) async { + final message = Message( + text: 'Hello, world!', + user: User(id: 'other-user-id', name: 'Other User'), + i18n: const { + 'fr_text': 'Bonjour, monde!', }, ); - testWidgets( - 'does not prepend author name in 1:1 channels', - (tester) async { - final channel = ChannelModel( - id: 'test-channel', - type: 'messaging', - memberCount: 2, - ); + await pumpMessagePreview(tester, message, language: 'fr'); - final message = Message( - text: 'Hello there', - user: User(id: 'other-user-id', name: 'Jane Doe'), - ); + expect(find.text('Bonjour, monde!'), findsOneWidget); + }); - await pumpMessagePreview(tester, message, channel: channel); + testWidgets('falls back to user language when no explicit language', (tester) async { + final client = MockClient(); + final clientState = MockClientState(); + final currentUser = OwnUser( + id: 'test-user-id', + name: 'Test User', + language: 'es', + ); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(currentUser); - expect(find.text('Hello there'), findsOneWidget); + final message = Message( + text: 'Hello, world!', + user: User(id: 'other-user-id', name: 'Other User'), + i18n: const { + 'es_text': 'Hola, mundo!', }, ); - testWidgets( - 'falls back to user language for translation when available', - (tester) async { - final client = MockClient(); - final clientState = MockClientState(); - final currentUser = OwnUser( - id: 'test-user-id', - name: 'Test User', - language: 'es', // Spanish language - ); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(currentUser); - - final message = Message( - text: 'Hello, world!', - user: User(id: 'other-user-id', name: 'Other User'), - i18n: const { - 'es_text': 'Hola, mundo!', - }, - ); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: StreamChat( - client: client, - streamChatThemeData: StreamChatThemeData.light(), - child: Center( - child: StreamMessagePreviewText( - message: message, - ), - ), - ), + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StreamChat( + client: client, + streamChatThemeData: StreamChatThemeData.light(), + child: Center( + child: StreamMessagePreviewText(message: message), ), ), - ); - await tester.pump(); + ), + ), + ); + await tester.pump(); + + expect(find.text('Hola, mundo!'), findsOneWidget); + }); - expect(find.text('Hola, mundo!'), findsOneWidget); + testWidgets('falls back to original text when translation missing', (tester) async { + final message = Message( + text: 'Hello, world!', + user: User(id: 'other-user-id', name: 'Other User'), + i18n: const { + 'fr_text': 'Bonjour, monde!', }, ); + + await pumpMessagePreview(tester, message, language: 'de'); + + expect(find.text('Hello, world!'), findsOneWidget); }); }); group('Custom MessagePreviewFormatter', () { const customFormatter = _CustomMessagePreviewFormatter(); - testWidgets('can override formatCurrentUserMessage', (tester) async { + testWidgets('can remove current user prefix via formatGroupMessage', (tester) async { final channel = ChannelModel( id: 'test-channel', type: 'messaging', @@ -554,7 +812,7 @@ void main() { final message = Message( text: 'Hello everyone', - user: User(id: 'test-user-id', name: 'Test User'), // Current user + user: User(id: 'test-user-id', name: 'Test User'), ); await pumpMessagePreview( @@ -566,12 +824,11 @@ void main() { ), ); - // Custom formatter removes "You:" prefix expect(find.text('Hello everyone'), findsOneWidget); expect(find.text('You: Hello everyone'), findsNothing); }); - testWidgets('can override formatGroupMessage', (tester) async { + testWidgets('can customize group message prefix via formatGroupMessage', (tester) async { final channel = ChannelModel( id: 'test-channel', type: 'messaging', @@ -592,11 +849,10 @@ void main() { ), ); - // Custom formatter uses "says:" instead of ":" expect(find.text('John Doe says: Hello'), findsOneWidget); }); - testWidgets('can override formatPollMessage', (tester) async { + testWidgets('can customize poll formatting via formatPollMessage', (tester) async { final message = Message( user: User(id: 'other-user-id', name: 'Message Sender'), poll: Poll( @@ -616,11 +872,32 @@ void main() { ), ); - // Custom formatter uses different format expect(find.text('📊 Poll: Favorite Color?'), findsOneWidget); + expect(_findIcons(tester), isEmpty); }); - testWidgets('can override formatMessageAttachments', (tester) async { + testWidgets('can customize location formatting via formatLocationMessage', (tester) async { + final message = Message( + user: User(id: 'other-user-id', name: 'Message Sender'), + sharedLocation: Location( + latitude: 37.7749, + longitude: -122.4194, + ), + ); + + await pumpMessagePreview( + tester, + message, + configData: StreamChatConfigurationData( + messagePreviewFormatter: customFormatter, + ), + ); + + expect(find.text('🗺️ -> Location Shared'), findsOneWidget); + expect(_findIcons(tester), isEmpty); + }); + + testWidgets('can handle custom attachment types via formatMessageAttachments', (tester) async { final message = Message( text: '', user: User(id: 'user-id'), @@ -640,11 +917,29 @@ void main() { ), ); - // Custom formatter handles custom attachment type expect(find.text('🛍️ iPhone'), findsOneWidget); }); - testWidgets('can override formatDirectMessage', (tester) async { + testWidgets('custom formatter falls through to default for known types', (tester) async { + final message = Message( + text: '', + user: User(id: 'user-id'), + attachments: [Attachment(type: AttachmentType.image)], + ); + + await pumpMessagePreview( + tester, + message, + configData: StreamChatConfigurationData( + messagePreviewFormatter: customFormatter, + ), + ); + + expect(_findIcons(tester), hasLength(1)); + expect(_extractText(tester), 'Photo'); + }); + + testWidgets('can customize direct message via formatMessage override', (tester) async { final channel = ChannelModel( id: 'test-channel', type: 'messaging', @@ -665,66 +960,158 @@ void main() { ), ); - // Custom formatter adds emoji prefix expect(find.text('💬 Hey there'), findsOneWidget); }); }); } -// Custom formatter for testing overrides +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Extracts concatenated text from all [TextSpan]s in the preview, ignoring +/// [WidgetSpan]s (icons, spacers). +String _extractText(WidgetTester tester) { + final span = _getPreviewSpan(tester); + return _spanText(span); +} + +String _spanText(InlineSpan span) { + if (span is TextSpan) { + final buffer = StringBuffer(span.text ?? ''); + for (final child in span.children ?? []) { + buffer.write(_spanText(child)); + } + return buffer.toString(); + } + return ''; +} + +/// Returns the root [TextSpan] rendered by the [StreamMessagePreviewText]. +TextSpan _getPreviewSpan(WidgetTester tester) { + final text = tester.widget( + find.descendant( + of: find.byType(StreamMessagePreviewText), + matching: find.byType(Text), + ), + ); + return text.textSpan! as TextSpan; +} + +/// Recursively collects all leaf [TextSpan]s that have non-null [text]. +List _findTextSpans(InlineSpan span) { + final result = []; + if (span is TextSpan) { + if (span.text != null) result.add(span); + for (final child in span.children ?? []) { + result.addAll(_findTextSpans(child)); + } + } + return result; +} + +/// Finds all [Icon] widgets rendered inside the [StreamMessagePreviewText]. +List _findIcons(WidgetTester tester) { + return tester + .widgetList( + find.descendant( + of: find.byType(StreamMessagePreviewText), + matching: find.byType(Icon), + ), + ) + .toList(); +} + +// --------------------------------------------------------------------------- +// Custom formatter for override tests +// --------------------------------------------------------------------------- + class _CustomMessagePreviewFormatter extends StreamMessagePreviewFormatter { const _CustomMessagePreviewFormatter(); @override - String formatCurrentUserMessage(BuildContext context, String messageText) { - // Remove "You:" prefix - return messageText; + TextSpan formatMessage( + BuildContext context, + Message message, { + bool showCaption = true, + ChannelModel? channel, + User? currentUser, + TextStyle? textStyle, + }) { + if (channel != null && channel.memberCount <= 2) { + final text = message.text ?? ''; + return TextSpan(text: '💬 $text', style: textStyle); + } + return super.formatMessage( + context, + message, + showCaption: showCaption, + channel: channel, + currentUser: currentUser, + textStyle: textStyle, + ); } @override - String formatGroupMessage( + TextSpan? formatGroupMessage( BuildContext context, User? messageAuthor, - String messageText, + User? currentUser, ) { + if (messageAuthor?.id == currentUser?.id) return null; + final authorName = messageAuthor?.name; - if (authorName == null || authorName.isEmpty) return messageText; + if (authorName == null || authorName.isEmpty) return null; - // Use "says:" instead of ":" - return '$authorName says: $messageText'; + return TextSpan(text: '$authorName says: '); } @override - String formatPollMessage( + TextSpan formatPollMessage( BuildContext context, Poll poll, - User? currentUser, - ) { - // Simple format with "Poll:" prefix - return poll.name.isEmpty ? '📊 Poll' : '📊 Poll: ${poll.name}'; + User? currentUser, { + TextStyle? textStyle, + }) { + return TextSpan( + text: poll.name.trim().isEmpty ? '📊 Poll' : '📊 Poll: ${poll.name}', + ); } @override - String formatDirectMessage(BuildContext context, String messageText) { - // Add emoji prefix - return '💬 $messageText'; + TextSpan formatLocationMessage( + BuildContext context, + Message message, + Location location, { + bool showCaption = true, + TextStyle? textStyle, + }) { + return const TextSpan(text: '🗺️ -> Location Shared'); } @override - String? formatMessageAttachments( + TextSpan? formatMessageAttachments( BuildContext context, String? messageText, - Iterable attachments, - ) { + Iterable attachments, { + List mentionedUsers = const [], + bool showCaption = true, + TextStyle? textStyle, + }) { final attachment = attachments.firstOrNull; - // Handle custom product attachment type if (attachment?.type == 'product') { final title = attachment?.extraData['title'] as String?; - return '🛍️ ${title ?? "Product"}'; + return TextSpan(text: '🛍️ ${title ?? "Product"}'); } - // Fallback to default implementation - return super.formatMessageAttachments(context, messageText, attachments); + return super.formatMessageAttachments( + context, + messageText, + attachments, + mentionedUsers: mentionedUsers, + showCaption: showCaption, + textStyle: textStyle, + ); } } 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/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/confirmation_dialog_test.dart b/packages/stream_chat_flutter/test/src/dialogs/confirmation_dialog_test.dart index 27e74c5892..671837c4d7 100644 --- a/packages/stream_chat_flutter/test/src/dialogs/confirmation_dialog_test.dart +++ b/packages/stream_chat_flutter/test/src/dialogs/confirmation_dialog_test.dart @@ -18,12 +18,9 @@ void main() { child: StreamChatTheme( data: StreamChatThemeData.light(), child: ConfirmationDialog( - titleText: context.translations - .toggleMuteUnmuteUserText(isMuted: false), - promptText: context.translations - .toggleMuteUnmuteUserQuestion(isMuted: false), - affirmativeText: context.translations - .toggleMuteUnmuteAction(isMuted: false), + titleText: context.translations.toggleMuteUnmuteUserText(isMuted: false), + promptText: context.translations.toggleMuteUnmuteUserQuestion(isMuted: false), + affirmativeText: context.translations.toggleMuteUnmuteAction(isMuted: false), onConfirmation: () {}, ), ), @@ -36,8 +33,7 @@ void main() { expect(find.byType(AlertDialog), findsOneWidget); expect(find.text('Mute User'), findsOneWidget); - expect(find.text('Are you sure you want to mute this user?'), - findsOneWidget); + expect(find.text('Are you sure you want to mute this user?'), findsOneWidget); expect(find.text('MUTE'), findsOneWidget); }); @@ -53,12 +49,9 @@ void main() { child: StreamChatTheme( data: StreamChatThemeData.light(), child: ConfirmationDialog( - titleText: context.translations - .toggleMuteUnmuteUserText(isMuted: false), - promptText: context.translations - .toggleMuteUnmuteUserQuestion(isMuted: false), - affirmativeText: context.translations - .toggleMuteUnmuteAction(isMuted: false), + titleText: context.translations.toggleMuteUnmuteUserText(isMuted: false), + promptText: context.translations.toggleMuteUnmuteUserQuestion(isMuted: false), + affirmativeText: context.translations.toggleMuteUnmuteAction(isMuted: false), onConfirmation: () {}, ), ), 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 e84c749682..c426684cc6 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 aa8b12cb51..99f0dc354b 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 d8fc7a77cf..ef6313474e 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 2ad3e344c8..2dd9ed245d 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 d8fc7a77cf..ef6313474e 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/fakes.dart b/packages/stream_chat_flutter/test/src/fakes.dart index 3c54a85aa6..d488a3f365 100644 --- a/packages/stream_chat_flutter/test/src/fakes.dart +++ b/packages/stream_chat_flutter/test/src/fakes.dart @@ -12,9 +12,7 @@ const String kApplicationDocumentsPath = 'applicationDocumentsPath'; const String kExternalCachePath = 'externalCachePath'; const String kExternalStoragePath = 'externalStoragePath'; -class FakePathProviderPlatform extends Fake - with MockPlatformInterfaceMixin - implements PathProviderPlatform { +class FakePathProviderPlatform extends Fake with MockPlatformInterfaceMixin implements PathProviderPlatform { @override Future getTemporaryPath() async { return kTemporaryPath; @@ -58,9 +56,7 @@ class FakePathProviderPlatform extends Fake } } -class AllNullFakePathProviderPlatform extends Fake - with MockPlatformInterfaceMixin - implements PathProviderPlatform { +class AllNullFakePathProviderPlatform extends Fake with MockPlatformInterfaceMixin implements PathProviderPlatform { @override Future getTemporaryPath() async { return null; @@ -104,9 +100,7 @@ class AllNullFakePathProviderPlatform extends Fake } } -class FakeRecordPlatform extends Fake - with MockPlatformInterfaceMixin - implements RecordPlatform { +class FakeRecordPlatform extends Fake with MockPlatformInterfaceMixin implements RecordPlatform { @override Future create(String recorderId) async {} @@ -143,9 +137,7 @@ class FakeRecordPlatform extends Fake Future dispose(String recorderId) async {} } -class FakeConnectivityPlatform extends Fake - with MockPlatformInterfaceMixin - implements ConnectivityPlatform { +class FakeConnectivityPlatform extends Fake with MockPlatformInterfaceMixin implements ConnectivityPlatform { @override Future> checkConnectivity() { return Future.value([ConnectivityResult.wifi]); diff --git a/packages/stream_chat_flutter/test/src/full_screen_media/full_screen_media_test.dart b/packages/stream_chat_flutter/test/src/full_screen_media/full_screen_media_test.dart index 43cb1bb35a..a9f0b6b5f0 100644 --- a/packages/stream_chat_flutter/test/src/full_screen_media/full_screen_media_test.dart +++ b/packages/stream_chat_flutter/test/src/full_screen_media/full_screen_media_test.dart @@ -36,7 +36,7 @@ void main() { Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -49,24 +49,24 @@ void main() { Message( text: 'hello', user: User(id: 'other-user'), - ) + ), ]); when(() => channelState.messagesStream).thenAnswer( (i) => Stream.value([ Message( text: 'hello', user: User(id: 'other-user'), - ) + ), ]), ); - when(() => channelState.typingEvents).thenAnswer((i) => { - User(id: 'other-user', extraData: const {'name': 'demo'}): - Event(type: EventType.typingStart), - }); + when(() => channelState.typingEvents).thenAnswer( + (i) => { + User(id: 'other-user', extraData: const {'name': 'demo'}): Event(type: EventType.typingStart), + }, + ); when(() => channelState.typingEventsStream).thenAnswer( (i) => Stream.value({ - User(id: 'other-user', extraData: const {'name': 'demo'}): - Event(type: EventType.typingStart), + User(id: 'other-user', extraData: const {'name': 'demo'}): Event(type: EventType.typingStart), }), ); @@ -81,28 +81,30 @@ void main() { attachment, ], ); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: StreamFullScreenMedia( - mediaAttachmentPackages: [ - StreamAttachmentPackage( - attachment: attachment, - message: message, - ), - ], + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: StreamFullScreenMedia( + mediaAttachmentPackages: [ + StreamAttachmentPackage( + attachment: attachment, + message: message, + ), + ], + ), ), ), ), - )); + ); // wait for the initial state to be rendered. await tester.pump(Duration.zero); expect(find.byType(PhotoView), findsOneWidget); - expect(find.byType(StreamSvgIcon), findsNWidgets(4)); + expect(find.byType(Icon), findsNWidgets(4)); }, ); } diff --git a/packages/stream_chat_flutter/test/src/gallery/gallery_footer_test.dart b/packages/stream_chat_flutter/test/src/gallery/gallery_footer_test.dart index 7480968063..cfb4cd9efa 100644 --- a/packages/stream_chat_flutter/test/src/gallery/gallery_footer_test.dart +++ b/packages/stream_chat_flutter/test/src/gallery/gallery_footer_test.dart @@ -13,8 +13,7 @@ void main() { late MockClientState clientState; late MockChannel channel; late MockChannelState channelState; - const methodChannel = - MethodChannel('dev.fluttercommunity.plus/connectivity_status'); + const methodChannel = MethodChannel('dev.fluttercommunity.plus/connectivity_status'); setUpAll(() { client = MockClient(); @@ -41,13 +40,12 @@ void main() { }); setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(methodChannel, ( + MethodCall methodCall, + ) async { if (methodCall.method == 'listen') { try { - await TestDefaultBinaryMessengerBinding - .instance.defaultBinaryMessenger - .handlePlatformMessage( + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( methodChannel.name, methodChannel.codec.encodeSuccessEnvelope(['wifi']), (_) {}, @@ -85,7 +83,7 @@ void main() { // wait for the initial state to be rendered. await tester.pumpAndSettle(); - expect(find.byType(StreamSvgIcon), findsNWidgets(2)); + expect(find.byType(Icon), findsNWidgets(2)); }, ); @@ -112,7 +110,6 @@ void main() { ); tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(methodChannel, null); }); } diff --git a/packages/stream_chat_flutter/test/src/gallery/gallery_header_test.dart b/packages/stream_chat_flutter/test/src/gallery/gallery_header_test.dart index f29f9e596d..d467151397 100644 --- a/packages/stream_chat_flutter/test/src/gallery/gallery_header_test.dart +++ b/packages/stream_chat_flutter/test/src/gallery/gallery_header_test.dart @@ -13,8 +13,7 @@ void main() { late MockClientState clientState; late MockChannel channel; late MockChannelState channelState; - const methodChannel = - MethodChannel('dev.fluttercommunity.plus/connectivity_status'); + const methodChannel = MethodChannel('dev.fluttercommunity.plus/connectivity_status'); setUpAll(() { client = MockClient(); @@ -41,13 +40,12 @@ void main() { }); setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, (MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(methodChannel, ( + MethodCall methodCall, + ) async { if (methodCall.method == 'listen') { try { - await TestDefaultBinaryMessengerBinding - .instance.defaultBinaryMessenger - .handlePlatformMessage( + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( methodChannel.name, methodChannel.codec.encodeSuccessEnvelope(['wifi']), (_) {}, @@ -86,7 +84,7 @@ void main() { // wait for the initial state to be rendered. await tester.pumpAndSettle(); - expect(find.byType(StreamSvgIcon), findsNWidgets(2)); + expect(find.byType(Icon), findsNWidgets(2)); }, ); @@ -118,7 +116,6 @@ void main() { ); tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(methodChannel, null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(methodChannel, null); }); } diff --git a/packages/stream_chat_flutter/test/src/gallery/goldens/ci/gallery_footer_0.png b/packages/stream_chat_flutter/test/src/gallery/goldens/ci/gallery_footer_0.png index 7217acb047..1de647f17b 100644 Binary files a/packages/stream_chat_flutter/test/src/gallery/goldens/ci/gallery_footer_0.png and b/packages/stream_chat_flutter/test/src/gallery/goldens/ci/gallery_footer_0.png differ diff --git a/packages/stream_chat_flutter/test/src/gallery/goldens/ci/gallery_header_0.png b/packages/stream_chat_flutter/test/src/gallery/goldens/ci/gallery_header_0.png index 9a3f6097fe..82703f3ce5 100644 Binary files a/packages/stream_chat_flutter/test/src/gallery/goldens/ci/gallery_header_0.png and b/packages/stream_chat_flutter/test/src/gallery/goldens/ci/gallery_header_0.png differ diff --git a/packages/stream_chat_flutter/test/src/icons/goldens/ci/stream_svg_icon_dark.png b/packages/stream_chat_flutter/test/src/icons/goldens/ci/stream_svg_icon_dark.png index e498b63bf8..a181028960 100644 Binary files a/packages/stream_chat_flutter/test/src/icons/goldens/ci/stream_svg_icon_dark.png and b/packages/stream_chat_flutter/test/src/icons/goldens/ci/stream_svg_icon_dark.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..8098dacd46 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/icons/stream_svg_icon_test.dart b/packages/stream_chat_flutter/test/src/icons/stream_svg_icon_test.dart index 5c4fc86d9c..47f28610a5 100644 --- a/packages/stream_chat_flutter/test/src/icons/stream_svg_icon_test.dart +++ b/packages/stream_chat_flutter/test/src/icons/stream_svg_icon_test.dart @@ -181,13 +181,15 @@ Widget _wrapWithMaterialApp( data: ThemeData(brightness: brightness), child: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: Center(child: widget), - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Center(child: widget), + ); + }, + ), ), ), ); diff --git a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_0.png b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_0.png index 838561febd..be8989f375 100644 Binary files a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_0.png and b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_0.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 efb6048218..311529e247 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 be1ab4d6f5..768e415472 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/goldens/ci/sending_indicator_3.png b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_3.png index 0c31a3c88b..768e415472 100644 Binary files a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_3.png and b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/sending_indicator_3.png differ diff --git a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/upload_progress_indicator_0.png b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/upload_progress_indicator_0.png index 4724897fe5..834961f86a 100644 Binary files a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/upload_progress_indicator_0.png and b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/upload_progress_indicator_0.png differ diff --git a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/upload_progress_indicator_1.png b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/upload_progress_indicator_1.png index 3ee1a56391..419582b6f3 100644 Binary files a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/upload_progress_indicator_1.png and b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/upload_progress_indicator_1.png differ diff --git a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/upload_progress_indicator_2.png b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/upload_progress_indicator_2.png index 8d3d9a3c78..e53612c8e7 100644 Binary files a/packages/stream_chat_flutter/test/src/indicators/goldens/ci/upload_progress_indicator_2.png and b/packages/stream_chat_flutter/test/src/indicators/goldens/ci/upload_progress_indicator_2.png differ diff --git a/packages/stream_chat_flutter/test/src/indicators/sending_indicator_test.dart b/packages/stream_chat_flutter/test/src/indicators/sending_indicator_test.dart index a686a966e3..1c172d3454 100644 --- a/packages/stream_chat_flutter/test/src/indicators/sending_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/indicators/sending_indicator_test.dart @@ -2,6 +2,7 @@ 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'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; import '../material_app_wrapper.dart'; void main() { @@ -28,7 +29,7 @@ void main() { ); goldenTest( - 'golden test for StreamSendingIndicator with StreamSvgIcon.checkAll', + 'golden test for StreamSendingIndicator with Icon checkAll', fileName: 'sending_indicator_0', constraints: const BoxConstraints.tightFor(width: 50, height: 50), builder: () => MaterialAppWrapper( @@ -47,7 +48,7 @@ void main() { ); goldenTest( - 'golden test for StreamSendingIndicator with StreamSvgIcon.checkAll ' + 'golden test for StreamSendingIndicator with Icon checkAll ' '(delivered)', fileName: 'sending_indicator_1', constraints: const BoxConstraints.tightFor(width: 50, height: 50), @@ -69,7 +70,7 @@ void main() { ); goldenTest( - 'golden test for StreamSendingIndicator with StreamSvgIcon.check', + 'golden test for StreamSendingIndicator with Icon check', fileName: 'sending_indicator_2', constraints: const BoxConstraints.tightFor(width: 50, height: 50), builder: () => MaterialAppWrapper( @@ -89,7 +90,7 @@ void main() { ); goldenTest( - 'golden test for StreamSendingIndicator with StreamSvgIcons.time', + 'golden test for StreamSendingIndicator with clock icon', fileName: 'sending_indicator_3', constraints: const BoxConstraints.tightFor(width: 50, height: 50), builder: () => MaterialAppWrapper( @@ -129,13 +130,13 @@ void main() { ), ); - final streamSvgIcon = tester.widget( - find.byType(StreamSvgIcon), + final icon = tester.widget( + find.byType(Icon), ); - expect(streamSvgIcon.icon, StreamSvgIcons.checkAll); + expect(icon.icon, StreamIconData.checks16); expect( - streamSvgIcon.color, + icon.color, StreamChatThemeData.light().colorTheme.textLowEmphasis, ); }, @@ -162,13 +163,13 @@ void main() { ), ); - final streamSvgIcon = tester.widget( - find.byType(StreamSvgIcon), + final icon = tester.widget( + find.byType(Icon), ); - expect(streamSvgIcon.icon, StreamSvgIcons.checkAll); + expect(icon.icon, StreamIconData.checks16); expect( - streamSvgIcon.color, + icon.color, StreamChatThemeData.light().colorTheme.accentPrimary, ); }, @@ -196,14 +197,14 @@ void main() { ), ); - final streamSvgIcon = tester.widget( - find.byType(StreamSvgIcon), + final icon = tester.widget( + find.byType(Icon), ); - expect(streamSvgIcon.icon, StreamSvgIcons.checkAll); + expect(icon.icon, StreamIconData.checks16); // Should use accentPrimary (read) not textLowEmphasis (delivered) expect( - streamSvgIcon.color, + icon.color, StreamChatThemeData.light().colorTheme.accentPrimary, ); }, diff --git a/packages/stream_chat_flutter/test/src/indicators/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/indicators/typing_indicator_test.dart index bcc2f13701..0a55b81f2c 100644 --- a/packages/stream_chat_flutter/test/src/indicators/typing_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/indicators/typing_indicator_test.dart @@ -35,7 +35,7 @@ void main() { Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -48,43 +48,45 @@ void main() { Message( text: 'hello', user: User(id: 'other-user'), - ) + ), ]); when(() => channelState.messagesStream).thenAnswer( (i) => Stream.value([ Message( text: 'hello', user: User(id: 'other-user'), - ) + ), ]), ); - when(() => channelState.typingEvents).thenAnswer((i) => { - User(id: 'other-user', extraData: const {'name': 'demo'}): - Event(type: EventType.typingStart), - }); + when(() => channelState.typingEvents).thenAnswer( + (i) => { + User(id: 'other-user', extraData: const {'name': 'demo'}): Event(type: EventType.typingStart), + }, + ); when(() => channelState.typingEventsStream).thenAnswer( (i) => Stream.value({ - User(id: 'other-user', extraData: const {'name': 'demo'}): - Event(type: EventType.typingStart), + User(id: 'other-user', extraData: const {'name': 'demo'}): Event(type: EventType.typingStart), }), ); const typingKey = Key('typing'); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: const Scaffold( - body: StreamTypingIndicator( - key: typingKey, + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: const Scaffold( + body: StreamTypingIndicator( + key: typingKey, + ), ), ), ), ), - )); + ); // wait for the initial state to be rendered. await tester.pump(Duration.zero); 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..c6fe1a058a 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 @@ -30,20 +30,21 @@ void main() { }); when(() => clientState.totalUnreadCount).thenReturn(10); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(10)); - - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamUnreadIndicator(), + when(() => clientState.totalUnreadCountStream).thenAnswer((i) => Stream.value(10)); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: const Scaffold( + body: StreamUnreadIndicator(), + ), ), ), ), - )); + ); // wait for the initial state to be rendered. await tester.pumpAndSettle(); @@ -70,22 +71,23 @@ void main() { when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); when(() => channelState.unreadCount).thenReturn(0); - when(() => channelState.unreadCountStream) - .thenAnswer((i) => Stream.value(0)); - - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamUnreadIndicator.channels( - cid: channel.cid, + when(() => channelState.unreadCountStream).thenAnswer((i) => Stream.value(0)); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: StreamUnreadIndicator.channels( + cid: channel.cid, + ), ), ), ), ), - )); + ); // wait for the initial state to be rendered. await tester.pumpAndSettle(); @@ -112,22 +114,23 @@ void main() { when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); when(() => channelState.unreadCount).thenReturn(100); - when(() => channelState.unreadCountStream) - .thenAnswer((i) => Stream.value(100)); - - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamUnreadIndicator.channels( - cid: channel.cid, + when(() => channelState.unreadCountStream).thenAnswer((i) => Stream.value(100)); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: StreamUnreadIndicator.channels( + cid: channel.cid, + ), ), ), ), ), - )); + ); // wait for the initial state to be rendered. await tester.pumpAndSettle(); diff --git a/packages/stream_chat_flutter/test/src/indicators/upload_progress_indicator_test.dart b/packages/stream_chat_flutter/test/src/indicators/upload_progress_indicator_test.dart index 09a9237d81..d7250346cd 100644 --- a/packages/stream_chat_flutter/test/src/indicators/upload_progress_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/indicators/upload_progress_indicator_test.dart @@ -6,8 +6,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import '../material_app_wrapper.dart'; void main() { - testWidgets('StreamUploadProgressIndicator at 0% with no background', - (tester) async { + testWidgets('StreamUploadProgressIndicator at 0% with no background', (tester) async { await tester.pumpWidget( MaterialApp( home: StreamChatTheme( @@ -28,8 +27,7 @@ void main() { expect(find.text('0%'), findsOneWidget); }); - testWidgets('StreamUploadProgressIndicator at 50% with no background', - (tester) async { + testWidgets('StreamUploadProgressIndicator at 50% with no background', (tester) async { await tester.pumpWidget( MaterialApp( home: StreamChatTheme( @@ -50,8 +48,7 @@ void main() { expect(find.text('50%'), findsOneWidget); }); - testWidgets('StreamUploadProgressIndicator at 100% with no background', - (tester) async { + testWidgets('StreamUploadProgressIndicator at 100% with no background', (tester) async { await tester.pumpWidget( MaterialApp( home: StreamChatTheme( @@ -72,8 +69,7 @@ void main() { expect(find.text('100%'), findsOneWidget); }); - testWidgets('StreamUploadProgressIndicator at 50% with background', - (tester) async { + testWidgets('StreamUploadProgressIndicator at 50% with background', (tester) async { await tester.pumpWidget( MaterialApp( home: StreamChatTheme( @@ -91,9 +87,7 @@ void main() { ); final backgroundColor = - ((find.byType(DecoratedBox).evaluate().first.widget as DecoratedBox) - .decoration as BoxDecoration) - .color; + ((find.byType(DecoratedBox).evaluate().first.widget as DecoratedBox).decoration as BoxDecoration).color; expect(const Color(0x99000000), backgroundColor); }); diff --git a/packages/stream_chat_flutter/test/src/material_app_wrapper.dart b/packages/stream_chat_flutter/test/src/material_app_wrapper.dart index 904b9a080d..b0e354997e 100644 --- a/packages/stream_chat_flutter/test/src/material_app_wrapper.dart +++ b/packages/stream_chat_flutter/test/src/material_app_wrapper.dart @@ -13,16 +13,15 @@ class MaterialAppWrapper extends MaterialApp { TransitionBuilder? builder, Widget? home, }) : super( - key: key, - builder: builder, - localizationsDelegates: localizations, - supportedLocales: localeOverrides ?? const [Locale('en')], - theme: theme?.copyWith(platform: platform) ?? - ThemeData(platform: platform, useMaterial3: false), - debugShowCheckedModeBanner: false, - home: home, - navigatorObservers: [ - if (navigatorObserver != null) navigatorObserver, - ], - ); + key: key, + builder: builder, + localizationsDelegates: localizations, + supportedLocales: localeOverrides ?? const [Locale('en')], + theme: theme?.copyWith(platform: platform) ?? ThemeData(platform: platform, useMaterial3: false), + debugShowCheckedModeBanner: false, + home: home, + navigatorObservers: [ + if (navigatorObserver != null) navigatorObserver, + ], + ); } 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..2305901cff 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..3f2d369e17 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..5ff006f9db 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..b681b7092b 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..be0eb556f4 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..22133451fa 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_actions_builder_test.dart b/packages/stream_chat_flutter/test/src/message_action/message_actions_builder_test.dart new file mode 100644 index 0000000000..00b288f2b5 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_action/message_actions_builder_test.dart @@ -0,0 +1,577 @@ +// 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); + }); + + 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', + ); + }); + + testWidgets( + 'excludes mark unread action for own messages even if parent message', + (tester) async { + final context = await _getContext(tester); + + final channelWithReadEvents = _getChannelWithCapabilities([ + ChannelCapability.sendMessage, + ChannelCapability.sendReply, + ChannelCapability.readEvents, + ]); + + final ownParentMessage = createTestMessage( + userId: currentUser.id, + replyCount: 5, + ); + + final actions = StreamMessageActionsBuilder.buildActions( + context: context, + message: ownParentMessage, + channel: channelWithReadEvents, + currentUser: currentUser, + ); + + actions.notExpects( + reason: + 'Mark unread should not be available for own messages, ' + 'even if it is a parent message', + ); + }, + ); + }); + + 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 action lists to simplify message action type checks. +extension StreamMessageActionSetExtension on List { + void expects({String? reason}) { + final containsActionType = this.any((it) => it.props.value is T); + return expect(containsActionType, isTrue, reason: reason); + } + + void notExpects({String? reason}) { + final containsActionType = this.any((it) => it.props.value 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 b23c6854db..0000000000 --- a/packages/stream_chat_flutter/test/src/message_actions_modal/message_actions_modal_test.dart +++ /dev/null @@ -1,926 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:record/record.dart'; -import 'package:stream_chat_flutter/src/message_actions_modal/message_actions_modal.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../fakes.dart'; -import '../mocks.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - setUpAll(() { - registerFallbackValue( - MaterialPageRoute(builder: (context) => const SizedBox()), - ); - - registerFallbackValue(Message()); - }); - - final originalRecordPlatform = RecordPlatform.instance; - setUp(() => RecordPlatform.instance = FakeRecordPlatform()); - tearDown(() => RecordPlatform.instance = originalRecordPlatform); - - 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/attachment_button_test.dart b/packages/stream_chat_flutter/test/src/message_input/attachment_button_test.dart index 7ac928d201..e5c54d2ed7 100644 --- a/packages/stream_chat_flutter/test/src/message_input/attachment_button_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/attachment_button_test.dart @@ -26,7 +26,7 @@ void main() { final button = find.byType(IconButton); expect(button, findsOneWidget); - expect(find.byType(StreamSvgIcon), findsOneWidget); + expect(find.byType(Icon), findsOneWidget); await tester.tap(button); expect(count, 1); }); @@ -78,9 +78,7 @@ void main() { home: Scaffold( body: Center( child: AttachmentButton( - color: StreamChatThemeData.light() - .messageInputTheme - .actionButtonIdleColor, + color: StreamChatThemeData.light().messageInputTheme.actionButtonIdleColor, onPressed: () {}, ), ), diff --git a/packages/stream_chat_flutter/test/src/message_input/attachment_picker/stream_attachment_picker_test.dart b/packages/stream_chat_flutter/test/src/message_input/attachment_picker/stream_attachment_picker_test.dart new file mode 100644 index 0000000000..ca2ff3baf9 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_input/attachment_picker/stream_attachment_picker_test.dart @@ -0,0 +1,333 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + group('tabbedAttachmentPickerBuilder', () { + group('optionsBuilder', () { + testWidgets( + 'should call optionsBuilder with default options', + (tester) async { + var builderCalled = false; + int? defaultOptionsCount; + + final controller = StreamAttachmentPickerController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + _wrapWithStreamChatApp( + Builder( + builder: (context) { + return SizedBox( + height: 400, + child: tabbedAttachmentPickerBuilder( + context: context, + controller: controller, + optionsBuilder: (context, defaultOptions) { + builderCalled = true; + defaultOptionsCount = defaultOptions.length; + return defaultOptions; + }, + ), + ); + }, + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(builderCalled, isTrue); + expect(defaultOptionsCount, isNotNull); + expect(defaultOptionsCount, greaterThan(0)); + }, + ); + + testWidgets( + 'should allow filtering default options', + (tester) async { + int? defaultOptionsCount; + + final controller = StreamAttachmentPickerController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + _wrapWithStreamChatApp( + Builder( + builder: (context) { + return SizedBox( + height: 400, + child: tabbedAttachmentPickerBuilder( + context: context, + controller: controller, + optionsBuilder: (context, defaultOptions) { + defaultOptionsCount = defaultOptions.length; + return [defaultOptions.first]; + }, + ), + ); + }, + ), + ), + ); + + await tester.pumpAndSettle(); + + final picker = tester.widget( + find.byType(StreamTabbedAttachmentPicker), + ); + + expect(picker.options.length, equals(1)); + expect(picker.options.length, lessThan(defaultOptionsCount!)); + }, + ); + + testWidgets( + 'should throw ArgumentError when wrong option types are provided', + (tester) async { + final controller = StreamAttachmentPickerController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + _wrapWithStreamChatApp( + Builder( + builder: (context) { + return SizedBox( + height: 400, + child: tabbedAttachmentPickerBuilder( + context: context, + controller: controller, + optionsBuilder: (context, defaultOptions) { + return [ + SystemAttachmentPickerOption( + key: 'wrong', + icon: Icons.error, + title: 'Wrong', + supportedTypes: [AttachmentPickerType.images], + onTap: (context, controller) async {}, + ), + ]; + }, + ), + ); + }, + ), + ), + ); + + expect(tester.takeException(), isA()); + }, + ); + }); + + group('allowedTypes', () { + testWidgets( + 'should filter options based on allowedTypes', + (tester) async { + final controller = StreamAttachmentPickerController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + _wrapWithStreamChatApp( + Builder( + builder: (context) { + return SizedBox( + height: 400, + child: tabbedAttachmentPickerBuilder( + context: context, + controller: controller, + allowedTypes: [AttachmentPickerType.images], + ), + ); + }, + ), + ), + ); + + await tester.pumpAndSettle(); + + final picker = tester.widget( + find.byType(StreamTabbedAttachmentPicker), + ); + + expect( + picker.options.every( + (option) => option.supportedTypes.contains(AttachmentPickerType.images), + ), + isTrue, + ); + }, + ); + }); + }); + + group('systemAttachmentPickerBuilder', () { + group('optionsBuilder', () { + testWidgets( + 'should call optionsBuilder with default options', + (tester) async { + var builderCalled = false; + + final controller = StreamAttachmentPickerController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + _wrapWithStreamChatApp( + Builder( + builder: (context) { + return systemAttachmentPickerBuilder( + context: context, + controller: controller, + optionsBuilder: (context, defaultOptions) { + builderCalled = true; + return defaultOptions; + }, + ); + }, + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(builderCalled, isTrue); + expect( + find.byType(StreamSystemAttachmentPicker), + findsOneWidget, + ); + }, + ); + + testWidgets( + 'should allow adding custom system picker options', + (tester) async { + final controller = StreamAttachmentPickerController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + _wrapWithStreamChatApp( + Builder( + builder: (context) { + return systemAttachmentPickerBuilder( + context: context, + controller: controller, + optionsBuilder: (context, defaultOptions) { + return [ + ...defaultOptions, + SystemAttachmentPickerOption( + key: 'custom-upload', + icon: Icons.cloud_upload, + title: 'Custom Upload', + supportedTypes: [AttachmentPickerType.files], + onTap: (context, controller) async {}, + ), + ]; + }, + ); + }, + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text('Custom Upload'), findsOneWidget); + }, + ); + + testWidgets( + 'should throw ArgumentError when wrong option types are provided', + (tester) async { + final controller = StreamAttachmentPickerController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + _wrapWithStreamChatApp( + Builder( + builder: (context) { + return systemAttachmentPickerBuilder( + context: context, + controller: controller, + optionsBuilder: (context, defaultOptions) { + return [ + TabbedAttachmentPickerOption( + key: 'wrong', + icon: Icons.error, + title: 'Wrong', + supportedTypes: [AttachmentPickerType.images], + optionViewBuilder: (context, controller) { + return const Text('Wrong'); + }, + ), + ]; + }, + ); + }, + ), + ), + ); + + expect(tester.takeException(), isA()); + }, + ); + }); + + group('allowedTypes', () { + testWidgets( + 'should filter options based on allowedTypes', + (tester) async { + final controller = StreamAttachmentPickerController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + _wrapWithStreamChatApp( + Builder( + builder: (context) { + return systemAttachmentPickerBuilder( + context: context, + controller: controller, + allowedTypes: [AttachmentPickerType.images], + ); + }, + ), + ), + ); + + await tester.pumpAndSettle(); + + final picker = tester.widget( + find.byType(StreamSystemAttachmentPicker), + ); + + expect( + picker.options.every( + (option) => option.supportedTypes.contains(AttachmentPickerType.images), + ), + isTrue, + ); + }, + ); + }); + }); +} + +Widget _wrapWithStreamChatApp( + Widget widget, { + 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: widget, + ); + }, + ), + ), + ); +} diff --git a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/audio_recorder_controller_test.dart b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/audio_recorder_controller_test.dart index 86ad93a548..82fb045413 100644 --- a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/audio_recorder_controller_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/audio_recorder_controller_test.dart @@ -33,8 +33,7 @@ void main() { when(() => mockRecorder.dispose()).thenAnswer((_) async {}); amplitudeController = PublishSubject(); - when(() => mockRecorder.onAmplitudeChanged(any())) - .thenAnswer((_) => amplitudeController.stream); + when(() => mockRecorder.onAmplitudeChanged(any())).thenAnswer((_) => amplitudeController.stream); controller = StreamAudioRecorderController.raw( config: config, @@ -53,30 +52,43 @@ void main() { group('startRecord', () { setUp(() { - when(() => mockRecorder.start(config, path: any(named: 'path'))) - .thenAnswer((_) async {}); + when(() => mockRecorder.start(config, path: any(named: 'path'))).thenAnswer((_) async {}); }); test( - 'starts recording when permission is granted', + 'starts recording when permission is already granted', () async { - when(() => mockRecorder.hasPermission()).thenAnswer((_) async => true); + when(() => mockRecorder.hasPermission(request: false)).thenAnswer((_) async => true); await controller.startRecord(); expect(controller.value, isA()); verify(() => mockRecorder.start(config, path: any(named: 'path'))); + // Should not prompt for permission when already granted. + verifyNever(() => mockRecorder.hasPermission(request: true)); }, ); - test('does not start recording when permission is denied', () async { - when(() => mockRecorder.hasPermission()).thenAnswer((_) async => false); + test('does not start recording when permission is not pre-granted', () async { + when(() => mockRecorder.hasPermission(request: false)).thenAnswer((_) async => false); + when(() => mockRecorder.hasPermission(request: true)).thenAnswer((_) async => false); await controller.startRecord(); expect(controller.value, isA()); verifyNever(() => mockRecorder.start(config, path: any(named: 'path'))); }); + + test('requests permission when not pre-granted', () async { + when(() => mockRecorder.hasPermission(request: false)).thenAnswer((_) async => false); + when(() => mockRecorder.hasPermission(request: true)).thenAnswer((_) async => false); + + await controller.startRecord(); + + // Should first check without prompting, then prompt the user. + verify(() => mockRecorder.hasPermission(request: false)).called(1); + verify(() => mockRecorder.hasPermission(request: true)).called(1); + }); }); group('stopRecord', () { @@ -84,10 +96,9 @@ void main() { const testPath = '$pathPrefix/audio.m4a'; setUp(() async { - when(() => mockRecorder.hasPermission()).thenAnswer((_) async => true); + when(() => mockRecorder.hasPermission(request: false)).thenAnswer((_) async => true); when(() => mockRecorder.stop()).thenAnswer((_) async => testPath); - when(() => mockRecorder.start(config, path: any(named: 'path'))) - .thenAnswer((_) async {}); + when(() => mockRecorder.start(config, path: any(named: 'path'))).thenAnswer((_) async {}); }); test('stops recording and updates state to stopped', () async { @@ -120,10 +131,9 @@ void main() { group('cancelRecord', () { setUp(() { - when(() => mockRecorder.hasPermission()).thenAnswer((_) async => true); + when(() => mockRecorder.hasPermission(request: false)).thenAnswer((_) async => true); when(() => mockRecorder.cancel()).thenAnswer((_) async {}); - when(() => mockRecorder.start(config, path: any(named: 'path'))) - .thenAnswer((_) async {}); + when(() => mockRecorder.start(config, path: any(named: 'path'))).thenAnswer((_) async {}); }); test('cancels recording and returns to idle state', () async { @@ -207,9 +217,8 @@ void main() { group('amplitude changes', () { setUp(() { - when(() => mockRecorder.hasPermission()).thenAnswer((_) async => true); - when(() => mockRecorder.start(config, path: any(named: 'path'))) - .thenAnswer((_) async {}); + when(() => mockRecorder.hasPermission(request: false)).thenAnswer((_) async => true); + when(() => mockRecorder.start(config, path: any(named: 'path'))).thenAnswer((_) async {}); }); test('updates waveform data when amplitude changes', () async { 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..f57b34bd21 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..40294f79ef 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 2eafbc2070..348ec3ed08 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 2865138e0c..312746df27 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 b00d4f798e..b76c96abc1 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 5669b3a7b7..079a2a1529 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 f89f3cffd1..3a3f1b0a59 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 350827d356..e423ca2dcb 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/audio_recorder/stream_audio_recorder_test.dart b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/stream_audio_recorder_test.dart index 736649f03a..8ed2c8abf8 100644 --- a/packages/stream_chat_flutter/test/src/message_input/audio_recorder/stream_audio_recorder_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/audio_recorder/stream_audio_recorder_test.dart @@ -3,9 +3,8 @@ import 'package:flutter/gestures.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/src/misc/audio_waveform.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import '../../utils/finders.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; void main() { group('StreamAudioRecorderButton', () { @@ -32,7 +31,7 @@ void main() { ), ); - expect(find.bySvgIcon(StreamSvgIcons.mic), findsOneWidget); + expect(find.byIcon(StreamIconData.voice20), findsOneWidget); }, ); @@ -176,9 +175,9 @@ void main() { ); expect(find.byType(StreamAudioWaveform), findsOneWidget); - expect(find.bySvgIcon(StreamSvgIcons.delete), findsOneWidget); - expect(find.bySvgIcon(StreamSvgIcons.stop), findsOneWidget); - expect(find.bySvgIcon(StreamSvgIcons.checkSend), findsOneWidget); + expect(find.byIcon(StreamIconData.delete20), findsOneWidget); + expect(find.byIcon(StreamIconData.stopFill20), findsOneWidget); + expect(find.byIcon(StreamIconData.checkmark20), findsOneWidget); }, ); @@ -200,7 +199,7 @@ void main() { ), ); - await tester.tap(find.bySvgIcon(StreamSvgIcons.delete)); + await tester.tap(find.byIcon(StreamIconData.delete20)); expect(onRecordCancelCalled, true); }, @@ -224,7 +223,7 @@ void main() { ), ); - await tester.tap(find.bySvgIcon(StreamSvgIcons.stop)); + await tester.tap(find.byIcon(StreamIconData.stopFill20)); expect(onRecordStopCalled, true); }, @@ -248,7 +247,7 @@ void main() { ), ); - await tester.tap(find.bySvgIcon(StreamSvgIcons.checkSend)); + await tester.tap(find.byIcon(StreamIconData.checkmark20)); expect(onRecordFinishCalled, true); }, @@ -270,8 +269,8 @@ void main() { expect(find.byType(PlaybackControlButton), findsOneWidget); expect(find.byType(PlaybackTimerText), findsOneWidget); expect(find.byType(StreamAudioWaveformSlider), findsOneWidget); - expect(find.bySvgIcon(StreamSvgIcons.delete), findsOneWidget); - expect(find.bySvgIcon(StreamSvgIcons.checkSend), findsOneWidget); + expect(find.byIcon(StreamIconData.delete20), findsOneWidget); + expect(find.byIcon(StreamIconData.checkmark20), findsOneWidget); }, ); @@ -292,7 +291,7 @@ void main() { ), ); - await tester.tap(find.bySvgIcon(StreamSvgIcons.checkSend)); + await tester.tap(find.byIcon(StreamIconData.checkmark20)); expect(onRecordFinishCalled, true); }, @@ -315,7 +314,7 @@ void main() { ), ); - await tester.tap(find.bySvgIcon(StreamSvgIcons.delete)); + await tester.tap(find.byIcon(StreamIconData.delete20)); expect(onRecordCancelCalled, true); }, @@ -455,7 +454,7 @@ void main() { ), ); - await tester.tap(find.bySvgIcon(StreamSvgIcons.stop)); + await tester.tap(find.byIcon(StreamIconData.stopFill20)); expect(feedbackCalled, isTrue); }, @@ -482,7 +481,7 @@ void main() { ), ); - await tester.tap(find.bySvgIcon(StreamSvgIcons.delete)); + await tester.tap(find.byIcon(StreamIconData.delete20)); expect(feedbackCalled, isTrue); }, @@ -509,7 +508,7 @@ void main() { ), ); - await tester.tap(find.bySvgIcon(StreamSvgIcons.checkSend)); + await tester.tap(find.byIcon(StreamIconData.checkmark20)); expect(feedbackCalled, isTrue); }, @@ -535,7 +534,7 @@ void main() { ), ); - await tester.tap(find.bySvgIcon(StreamSvgIcons.delete)); + await tester.tap(find.byIcon(StreamIconData.delete20)); expect(feedbackCalled, isTrue); }, @@ -561,7 +560,7 @@ void main() { ), ); - await tester.tap(find.bySvgIcon(StreamSvgIcons.checkSend)); + await tester.tap(find.byIcon(StreamIconData.checkmark20)); expect(feedbackCalled, isTrue); }, @@ -637,8 +636,7 @@ void main() { goldenTest( '[${brightness.name}] -> should look fine in recording hold state', - fileName: - 'stream_audio_recorder_button_recording_hold_${brightness.name}', + fileName: 'stream_audio_recorder_button_recording_hold_${brightness.name}', constraints: const BoxConstraints.tightFor(width: 400, height: 160), builder: () => _wrapWithStreamChatApp( brightness: brightness, @@ -653,8 +651,7 @@ void main() { goldenTest( '[${brightness.name}] -> should look fine in recording locked state', - fileName: - 'stream_audio_recorder_button_recording_locked_${brightness.name}', + fileName: 'stream_audio_recorder_button_recording_locked_${brightness.name}', constraints: const BoxConstraints.tightFor(width: 400, height: 160), builder: () => _wrapWithStreamChatApp( brightness: brightness, @@ -672,8 +669,7 @@ void main() { goldenTest( '[${brightness.name}] -> should look fine in recording stopped state', - fileName: - 'stream_audio_recorder_button_recording_stopped_${brightness.name}', + fileName: 'stream_audio_recorder_button_recording_stopped_${brightness.name}', constraints: const BoxConstraints.tightFor(width: 400, height: 160), builder: () => _wrapWithStreamChatApp( brightness: brightness, @@ -695,24 +691,27 @@ Widget _wrapWithStreamChatApp( Brightness? brightness, }) { return MaterialApp( + theme: ThemeData(brightness: brightness), debugShowCheckedModeBanner: false, home: Portal( child: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - bottomNavigationBar: Material( - elevation: 10, - color: theme.colorTheme.barsBg, - child: Padding( - padding: const EdgeInsets.all(8), - child: widget, + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + bottomNavigationBar: Material( + elevation: 10, + color: theme.colorTheme.barsBg, + child: Padding( + padding: const EdgeInsets.all(8), + child: widget, + ), ), - ), - ); - }), + ); + }, + ), ), ), ); diff --git a/packages/stream_chat_flutter/test/src/message_input/clear_input_item_test.dart b/packages/stream_chat_flutter/test/src/message_input/clear_input_item_test.dart index 68d1f430fd..8f896a164d 100644 --- a/packages/stream_chat_flutter/test/src/message_input/clear_input_item_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/clear_input_item_test.dart @@ -28,7 +28,7 @@ void main() { final button = find.byType(RawMaterialButton); expect(button, findsOneWidget); - expect(find.byType(StreamSvgIcon), findsOneWidget); + expect(find.byType(Icon), findsOneWidget); await tester.tap(button); expect(count, 1); }); 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..1292f30fe9 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/clear_input_item_0.png b/packages/stream_chat_flutter/test/src/message_input/goldens/ci/clear_input_item_0.png index 989e925b4e..8122de5b8f 100644 Binary files a/packages/stream_chat_flutter/test/src/message_input/goldens/ci/clear_input_item_0.png and b/packages/stream_chat_flutter/test/src/message_input/goldens/ci/clear_input_item_0.png differ diff --git a/packages/stream_chat_flutter/test/src/message_input/goldens/ci/command_button_0.png b/packages/stream_chat_flutter/test/src/message_input/goldens/ci/command_button_0.png index 588d580602..f81ea758fe 100644 Binary files a/packages/stream_chat_flutter/test/src/message_input/goldens/ci/command_button_0.png and b/packages/stream_chat_flutter/test/src/message_input/goldens/ci/command_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 4425571a1c..8f5524d281 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_input/message_input_attachment_list_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_input_attachment_list_test.dart index 15a655d9c1..0d0c2c3c02 100644 --- a/packages/stream_chat_flutter/test/src/message_input/message_input_attachment_list_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_input_attachment_list_test.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; import '../mocks.dart'; @@ -17,9 +18,9 @@ Widget wrapWithStreamChat( } void main() { - group('StreamMessageInputAttachmentList tests', () { + group('StreamMessageComposerAttachmentList tests', () { testWidgets( - 'StreamMessageInputAttachmentList should render attachments', + 'StreamMessageComposerAttachmentList should render attachments', (WidgetTester tester) async { final attachments = [ Attachment(type: 'file', id: 'file1'), @@ -29,22 +30,21 @@ void main() { await tester.pumpWidget( wrapWithStreamChat( - StreamMessageInputAttachmentList( + StreamMessageComposerAttachmentList( attachments: attachments, ), ), ); // Expect 2 file attachments and 1 media attachment - expect(find.byType(MessageInputFileAttachments), findsOneWidget); - expect(find.byType(StreamFileAttachment), findsNWidgets(2)); + expect(find.byType(MessageComposerFileAttachment), findsNWidgets(2)); expect(find.byType(MessageInputMediaAttachments), findsOneWidget); expect(find.byType(StreamMediaAttachmentThumbnail), findsOneWidget); }, ); testWidgets( - 'StreamMessageInputAttachmentList should call onRemovePressed callback', + 'StreamMessageComposerAttachmentList should call onRemovePressed callback', (WidgetTester tester) async { Attachment? removedAttachment; @@ -55,7 +55,7 @@ void main() { await tester.pumpWidget( wrapWithStreamChat( - StreamMessageInputAttachmentList( + StreamMessageComposerAttachmentList( attachments: attachments, onRemovePressed: (attachment) { removedAttachment = attachment; @@ -64,7 +64,7 @@ void main() { ), ); - final removeButtons = find.byType(RemoveAttachmentButton); + final removeButtons = find.byType(StreamRemoveControl); // Tap the first remove button await tester.tap(removeButtons.first); @@ -72,18 +72,18 @@ void main() { // Expect the onRemovePressed callback to be called with the second // attachment as they are reversed in the UI. - expect(removedAttachment, attachments[1]); + expect(removedAttachment, attachments[0]); }, ); testWidgets( - '''StreamMessageInputAttachmentList should display empty box if no attachments''', + '''StreamMessageComposerAttachmentList should display empty box if no attachments''', (WidgetTester tester) async { final attachments = []; await tester.pumpWidget( wrapWithStreamChat( - StreamMessageInputAttachmentList( + StreamMessageComposerAttachmentList( attachments: attachments, ), ), @@ -106,14 +106,14 @@ void main() { await tester.pumpWidget( wrapWithStreamChat( - MessageInputFileAttachments( + StreamMessageComposerAttachmentList( attachments: attachments, ), ), ); // Expect 2 file attachments - expect(find.byType(StreamFileAttachment), findsNWidgets(2)); + expect(find.byType(MessageComposerFileAttachment), findsNWidgets(2)); }, ); @@ -128,7 +128,7 @@ void main() { await tester.pumpWidget( wrapWithStreamChat( - MessageInputFileAttachments( + StreamMessageComposerAttachmentList( attachments: attachments, onRemovePressed: (attachment) { removedAttachment = attachment; @@ -137,7 +137,7 @@ void main() { ), ); - final removeButton = find.byType(RemoveAttachmentButton); + final removeButton = find.byType(StreamRemoveControl); // Tap the remove button await tester.tap(removeButton); @@ -167,7 +167,7 @@ void main() { ); // Expect 2 media attachments - expect(find.byType(Stack), findsNWidgets(2)); + expect(find.byType(StreamMediaAttachmentBuilder), findsNWidgets(2)); }, ); @@ -228,7 +228,7 @@ void main() { ), ); - final removeButton = find.byType(RemoveAttachmentButton); + final removeButton = find.byType(StreamRemoveControl); // Tap the remove button await tester.tap(removeButton); diff --git a/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart index cfb48916e2..e196bea51b 100644 --- a/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/message_input_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: lines_longer_than_80_chars -import 'package:desktop_drop/desktop_drop.dart'; +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,6 +13,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import '../fakes.dart'; import '../mocks.dart'; +/// TODO: remove skip once we have a proper message input test. void main() { final originalRecordPlatform = RecordPlatform.instance; setUp(() => RecordPlatform.instance = FakeRecordPlatform()); @@ -19,10 +21,13 @@ void main() { testWidgets( 'checks message input features', + skip: true, (WidgetTester tester) async { - await tester.pumpWidget(buildWidget( - const StreamMessageInput(), - )); + await tester.pumpWidget( + buildWidget( + const StreamMessageInput(), + ), + ); // wait for the initial state to be rendered. await tester.pumpAndSettle(); @@ -34,6 +39,7 @@ void main() { testWidgets( 'checks message input slow mode', + skip: true, (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); @@ -62,7 +68,7 @@ void main() { Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -75,14 +81,14 @@ void main() { Message( text: 'hello', user: User(id: 'other-user'), - ) + ), ]); when(() => channelState.messagesStream).thenAnswer( (i) => Stream.value([ Message( text: 'hello', user: User(id: 'other-user'), - ) + ), ]), ); @@ -107,53 +113,6 @@ void main() { }, ); - testWidgets( - 'allows setting padding on message input', - (WidgetTester tester) async { - await tester.pumpWidget( - buildWidget( - const StreamMessageInput( - padding: EdgeInsets.only(left: 50), - ), - ), - ); - - // wait for the initial state to be rendered. - await tester.pumpAndSettle(); - - expect( - find.descendant( - of: find.byType(StreamMessageValueListenableBuilder), - matching: find.byWidgetPredicate((w) => - w is Padding && - w.padding == const EdgeInsets.only(left: 50))), - findsOneWidget); - }, - ); - - testWidgets( - 'allows setting explicit margin on text field', - (WidgetTester tester) async { - await tester.pumpWidget( - buildWidget( - const StreamMessageInput( - textInputMargin: EdgeInsets.only(left: 50), - ), - ), - ); - // wait for the initial state to be rendered. - await tester.pumpAndSettle(); - - expect( - find.descendant( - of: find.byType(DropTarget), - matching: find.byWidgetPredicate((w) => - w is Container && - w.margin == const EdgeInsets.only(left: 50))), - findsOneWidget); - }, - ); - group('MessageInput keyboard interactions', () { final client = MockClient(); final clientState = MockClientState(); @@ -177,6 +136,7 @@ void main() { testWidgets( 'should send message when Enter key is pressed on desktop', + skip: true, (tester) async { when(() => channel.sendMessage(any())).thenAnswer( (i) async => SendMessageResponse() @@ -218,6 +178,7 @@ void main() { testWidgets( 'should not send message when Shift+Enter key is pressed on desktop', + skip: true, (tester) async { when(() => channel.sendMessage(any())).thenAnswer( (_) async => SendMessageResponse() @@ -263,6 +224,7 @@ void main() { testWidgets( 'should clear quoted message when Esc key is pressed on desktop', + skip: true, (tester) async { final quotedMessage = Message(text: 'I am a quoted message'); final initialMessage = Message(quotedMessage: quotedMessage); @@ -311,6 +273,7 @@ void main() { testWidgets( 'should not clear quoted message contains text and Esc key is pressed on desktop', + skip: true, (tester) async { final quotedMessage = Message(text: 'I am a quoted message'); final initialMessage = Message(quotedMessage: quotedMessage); @@ -358,6 +321,131 @@ void main() { ); }); + group('Edit message routing', () { + final client = MockClient(); + final clientState = MockClientState(); + final channel = MockChannel(); + final channelState = MockChannelState(); + + setUp(() { + registerFallbackValue(Message()); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUserStream).thenAnswer( + (_) => Stream.value(OwnUser(id: 'user-id')), + ); + + when(() => channel.state).thenReturn(channelState); + when(() => channel.client).thenReturn(client); + when(channel.getRemainingCooldown).thenReturn(0); + when(() => channel.isMuted).thenReturn(false); + when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false)); + when(() => channel.extraData).thenReturn({'name': 'test'}); + when(() => channel.extraDataStream).thenAnswer((_) => Stream.value({'name': 'test'})); + when(() => channelState.isUpToDate).thenReturn(true); + when(() => channelState.members).thenReturn([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]); + when(() => channelState.membersStream).thenAnswer( + (_) => Stream.value([ + Member( + userId: 'user-id', + user: User(id: 'user-id'), + ), + ]), + ); + when(() => channelState.messages).thenReturn([]); + when(() => channelState.messagesStream).thenAnswer((_) => Stream.value([])); + }); + + testWidgets( + 'calls updateMessage when controller is in edit state', + (tester) async { + when(() => channel.updateMessage(any())).thenAnswer( + (_) async => UpdateMessageResponse()..message = Message(id: 'msg-1', text: 'Edited text'), + ); + + final existingMessage = Message(id: 'msg-1', text: 'Original text'); + final messageInputController = StreamMessageInputController()..editMessage(existingMessage); + addTearDown(messageInputController.dispose); + + final key = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + bottomNavigationBar: StreamMessageInput( + key: key, + messageInputController: messageInputController, + ), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + await key.currentState!.sendMessage(); + // Pump past the debounce/throttle timers (350ms) + await tester.pump(const Duration(seconds: 1)); + + verify(() => channel.updateMessage(any())).called(1); + verifyNever(() => channel.sendMessage(any())); + }, + ); + + testWidgets( + 'calls sendMessage when controller is in normal (non-edit) state', + (tester) async { + when(() => channel.sendMessage(any())).thenAnswer( + (_) async => SendMessageResponse()..message = Message(text: 'Hello'), + ); + + final messageInputController = StreamMessageInputController( + message: Message(text: 'Hello'), + ); + addTearDown(messageInputController.dispose); + + final key = GlobalKey(); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + bottomNavigationBar: StreamMessageInput( + key: key, + messageInputController: messageInputController, + ), + ), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + await key.currentState!.sendMessage(); + // Pump past the debounce/throttle timers (350ms) + await tester.pump(const Duration(seconds: 1)); + + verify(() => channel.sendMessage(any())).called(1); + verifyNever(() => channel.updateMessage(any())); + }, + ); + }); + group('DmCheckboxListTile integration in MessageInput', () { final client = MockClient(); final clientState = MockClientState(); @@ -381,20 +469,21 @@ void main() { Message( text: 'hello', user: User(id: 'other-user'), - ) + ), ]); when(() => channelState.messagesStream).thenAnswer( (i) => Stream.value([ Message( text: 'hello', user: User(id: 'other-user'), - ) + ), ]), ); }); testWidgets( 'should not show DmCheckboxListTile when hideSendAsDm is true', + skip: true, (tester) async { await tester.pumpWidget( MaterialApp( @@ -404,7 +493,7 @@ void main() { channel: channel, child: const Scaffold( bottomNavigationBar: StreamMessageInput( - hideSendAsDm: true, + canAlsoSendToChannelFromThread: false, ), ), ), @@ -420,6 +509,7 @@ void main() { testWidgets( 'should not show DmCheckboxListTile when not in a thread', + skip: true, (tester) async { await tester.pumpWidget( MaterialApp( @@ -443,6 +533,7 @@ void main() { testWidgets( 'should show DmCheckboxListTile when in a thread and hideSendAsDm is false', + skip: true, (tester) async { // Set up a message controller with a parent message ID (thread) final messageInputController = StreamMessageInputController( @@ -473,6 +564,7 @@ void main() { testWidgets( 'should toggle showInChannel value when DmCheckboxListTile is tapped', + skip: true, (tester) async { // Set up a message controller with a parent message ID (thread) final messageInputController = StreamMessageInputController( @@ -518,6 +610,428 @@ void main() { }, ); }); + + group('Composer sync with remote events', () { + late MockClient client; + late MockClientState clientState; + late MockChannel channel; + late MockChannelState channelState; + late StreamController eventController; + + setUp(() { + registerFallbackValue(Message()); + + eventController = StreamController.broadcast(); + + client = MockClient(); + clientState = MockClientState(); + channel = MockChannel(eventStream: eventController.stream); + channelState = MockChannelState(); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + when(() => clientState.currentUserStream).thenAnswer( + (_) => Stream.value(OwnUser(id: 'user-id')), + ); + + when(() => channel.state).thenReturn(channelState); + when(() => channel.client).thenReturn(client); + when(channel.getRemainingCooldown).thenReturn(0); + when(() => channelState.isUpToDate).thenReturn(true); + when(() => channelState.members).thenReturn([]); + when(() => channelState.membersStream).thenAnswer((_) => Stream.value([])); + when(() => channelState.messages).thenReturn([]); + when(() => channelState.messagesStream).thenAnswer((_) => Stream.value([])); + }); + + tearDown(() => eventController.close()); + + group('quoted message', () { + testWidgets( + 'clears quoted message on message.deleted event', + (tester) async { + final quotedMessage = Message( + id: 'quoted-msg-id', + text: 'Original message', + user: User(id: 'other-user'), + ); + final controller = StreamMessageInputController( + message: Message( + quotedMessage: quotedMessage, + quotedMessageId: quotedMessage.id, + ), + ); + addTearDown(controller.dispose); + + var onQuotedMessageClearedCalled = false; + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + bottomNavigationBar: StreamMessageInput( + messageInputController: controller, + onQuotedMessageCleared: () { + onQuotedMessageClearedCalled = true; + }, + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(controller.message.quotedMessageId, 'quoted-msg-id'); + + eventController.add( + Event( + type: EventType.messageDeleted, + message: Message(id: 'quoted-msg-id'), + ), + ); + await tester.pump(); + + expect(onQuotedMessageClearedCalled, isTrue); + }, + ); + + testWidgets( + 'does not clear quoted message when a different message is deleted', + (tester) async { + final quotedMessage = Message( + id: 'quoted-msg-id', + text: 'Original message', + user: User(id: 'other-user'), + ); + final controller = StreamMessageInputController( + message: Message( + quotedMessage: quotedMessage, + quotedMessageId: quotedMessage.id, + ), + ); + addTearDown(controller.dispose); + + var onQuotedMessageClearedCalled = false; + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + bottomNavigationBar: StreamMessageInput( + messageInputController: controller, + onQuotedMessageCleared: () { + onQuotedMessageClearedCalled = true; + }, + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + eventController.add( + Event( + type: EventType.messageDeleted, + message: Message(id: 'some-other-msg-id'), + ), + ); + await tester.pump(); + + expect(controller.message.quotedMessageId, 'quoted-msg-id'); + expect(controller.message.quotedMessage, isNotNull); + expect(onQuotedMessageClearedCalled, isFalse); + }, + ); + + testWidgets( + 'updates quoted message on message.updated event', + (tester) async { + final quotedMessage = Message( + id: 'quoted-msg-id', + text: 'Original text', + user: User(id: 'other-user'), + ); + final controller = StreamMessageInputController( + message: Message( + quotedMessage: quotedMessage, + quotedMessageId: quotedMessage.id, + ), + ); + addTearDown(controller.dispose); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + bottomNavigationBar: StreamMessageInput( + messageInputController: controller, + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(controller.message.quotedMessage?.text, 'Original text'); + + eventController.add( + Event( + type: EventType.messageUpdated, + message: Message( + id: 'quoted-msg-id', + text: 'Edited text', + user: User(id: 'other-user'), + ), + ), + ); + await tester.pump(); + + expect(controller.message.quotedMessageId, 'quoted-msg-id'); + expect(controller.message.quotedMessage?.text, 'Edited text'); + }, + ); + + testWidgets( + 'does not update quoted message when a different message is updated', + (tester) async { + final quotedMessage = Message( + id: 'quoted-msg-id', + text: 'Original text', + user: User(id: 'other-user'), + ); + final controller = StreamMessageInputController( + message: Message( + quotedMessage: quotedMessage, + quotedMessageId: quotedMessage.id, + ), + ); + addTearDown(controller.dispose); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + bottomNavigationBar: StreamMessageInput( + messageInputController: controller, + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + eventController.add( + Event( + type: EventType.messageUpdated, + message: Message( + id: 'some-other-msg-id', + text: 'Edited text', + user: User(id: 'other-user'), + ), + ), + ); + await tester.pump(); + + expect(controller.message.quotedMessage?.text, 'Original text'); + }, + ); + }); + + group('editing message', () { + testWidgets( + 'refreshes editing message on message.updated event', + (tester) async { + final existingMessage = Message( + id: 'editing-msg-id', + text: 'Original text', + user: User(id: 'user-id'), + ); + final controller = StreamMessageInputController()..editMessage(existingMessage); + addTearDown(controller.dispose); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + bottomNavigationBar: StreamMessageInput( + messageInputController: controller, + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(controller.message.id, 'editing-msg-id'); + expect(controller.message.text, 'Original text'); + + eventController.add( + Event( + type: EventType.messageUpdated, + message: Message( + id: 'editing-msg-id', + text: 'Updated by another device', + user: User(id: 'user-id'), + ), + ), + ); + await tester.pump(); + + expect(controller.message.id, 'editing-msg-id'); + expect(controller.message.text, 'Updated by another device'); + }, + ); + + testWidgets( + 'does not refresh editing message when a different message is updated', + (tester) async { + final existingMessage = Message( + id: 'editing-msg-id', + text: 'Original text', + user: User(id: 'user-id'), + ); + final controller = StreamMessageInputController()..editMessage(existingMessage); + addTearDown(controller.dispose); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + bottomNavigationBar: StreamMessageInput( + messageInputController: controller, + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + eventController.add( + Event( + type: EventType.messageUpdated, + message: Message( + id: 'some-other-msg-id', + text: 'Edited text', + user: User(id: 'other-user'), + ), + ), + ); + await tester.pump(); + + expect(controller.message.text, 'Original text'); + }, + ); + + testWidgets( + 'cancels edit on message.deleted event', + (tester) async { + final existingMessage = Message( + id: 'editing-msg-id', + text: 'Being edited', + user: User(id: 'user-id'), + ); + final controller = StreamMessageInputController()..editMessage(existingMessage); + addTearDown(controller.dispose); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + bottomNavigationBar: StreamMessageInput( + messageInputController: controller, + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(controller.message.id, 'editing-msg-id'); + expect(controller.message.state.isUpdating, isTrue); + + eventController.add( + Event( + type: EventType.messageDeleted, + message: Message(id: 'editing-msg-id'), + ), + ); + await tester.pump(); + + expect(controller.message.id, isNot('editing-msg-id')); + expect(controller.message.state.isInitial, isTrue); + }, + ); + + testWidgets( + 'does not cancel edit when a different message is deleted', + (tester) async { + final existingMessage = Message( + id: 'editing-msg-id', + text: 'Being edited', + user: User(id: 'user-id'), + ); + final controller = StreamMessageInputController()..editMessage(existingMessage); + addTearDown(controller.dispose); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + bottomNavigationBar: StreamMessageInput( + messageInputController: controller, + ), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + eventController.add( + Event( + type: EventType.messageDeleted, + message: Message(id: 'some-other-msg-id'), + ), + ); + await tester.pump(); + + expect(controller.message.id, 'editing-msg-id'); + expect(controller.message.state.isUpdating, isTrue); + }, + ); + }); + }); } MaterialApp buildWidget(StreamMessageInput input) { @@ -548,7 +1062,7 @@ MaterialApp buildWidget(StreamMessageInput input) { Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -561,14 +1075,14 @@ MaterialApp buildWidget(StreamMessageInput input) { Message( text: 'hello', user: User(id: 'other-user'), - ) + ), ]); when(() => channelState.messagesStream).thenAnswer( (i) => Stream.value([ Message( text: 'hello', user: User(id: 'other-user'), - ) + ), ]), ); diff --git a/packages/stream_chat_flutter/test/src/message_input/stream_message_send_button_test.dart b/packages/stream_chat_flutter/test/src/message_input/stream_message_send_button_test.dart index a9e4d95052..db9444fd9d 100644 --- a/packages/stream_chat_flutter/test/src/message_input/stream_message_send_button_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input/stream_message_send_button_test.dart @@ -2,13 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/src/icons/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/message_input/stream_message_input_icon_button.dart'; import 'package:stream_chat_flutter/src/message_input/stream_message_send_button.dart'; import 'package:stream_chat_flutter/src/theme/message_input_theme.dart'; import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; - -import '../utils/finders.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; void main() { group('StreamMessageSendButton', () { @@ -49,7 +47,7 @@ void main() { expect(iconButton.onPressed, isNull); // Verify default idle icon is shown - expect(find.bySvgIcon(StreamSvgIcons.sendMessage), findsOneWidget); + expect(find.byIcon(StreamIconData.send20), findsOneWidget); }, ); @@ -73,7 +71,7 @@ void main() { expect(iconButton.onPressed, isNotNull); // Verify default active icon is shown - expect(find.bySvgIcon(StreamSvgIcons.circleUp), findsOneWidget); + expect(find.byIcon(StreamIconData.arrowUp20), findsOneWidget); }, ); @@ -93,7 +91,7 @@ void main() { ); expect(find.byKey(const Key('custom_idle')), findsOneWidget); - expect(find.byType(StreamSvgIcon), findsNothing); + expect(find.byType(Icon), findsNothing); }, ); @@ -113,7 +111,7 @@ void main() { ); expect(find.byKey(const Key('custom_active')), findsOneWidget); - expect(find.byType(StreamSvgIcon), findsNothing); + expect(find.byType(Icon), findsNothing); }, ); @@ -176,20 +174,22 @@ Widget _wrapWithStreamChatApp( debugShowCheckedModeBanner: false, home: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - bottomNavigationBar: Material( - elevation: 10, - color: theme.colorTheme.barsBg, - child: Padding( - padding: const EdgeInsets.all(8), - child: widget, + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + bottomNavigationBar: Material( + elevation: 10, + color: theme.colorTheme.barsBg, + child: Padding( + padding: const EdgeInsets.all(8), + child: widget, + ), ), - ), - ); - }), + ); + }, + ), ), ); } diff --git a/packages/stream_chat_flutter/test/src/message_list_view/bottom_row_test.dart b/packages/stream_chat_flutter/test/src/message_list_view/bottom_row_test.dart deleted file mode 100644 index 4f3059b99c..0000000000 --- a/packages/stream_chat_flutter/test/src/message_list_view/bottom_row_test.dart +++ /dev/null @@ -1,74 +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() { - late Channel channel; - late ChannelClientState channelClientState; - - setUp(() { - channel = MockChannel(); - when(() => channel.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); - channelClientState = MockChannelState(); - when(() => channel.state).thenReturn(channelClientState); - when(() => channelClientState.messages).thenReturn([ - Message( - id: 'parentId', - ) - ]); - }); - - setUpAll(() { - registerFallbackValue(Message()); - }); - - testWidgets('BottomRow', (tester) async { - final theme = StreamChatThemeData.light(); - final onThreadTap = MockValueChanged(); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Center( - child: StreamChannel( - channel: channel, - child: BottomRow( - message: Message( - parentId: 'parentId', - ), - isDeleted: false, - showThreadReplyIndicator: false, - showUsername: false, - showInChannel: true, - showTimeStamp: false, - showEditedLabel: false, - reverse: false, - showSendingIndicator: false, - hasUrlAttachments: false, - isGiphy: false, - isOnlyEmoji: false, - messageTheme: theme.otherMessageTheme, - streamChatTheme: theme, - hasNonUrlAttachments: false, - streamChat: StreamChatState(), - onThreadTap: onThreadTap, - ), - ), - ), - ), - ), - ); - - // wait for the initial state to be rendered. - await tester.pump(Duration.zero); - - await tester.tap(find.byType(GestureDetector)); - await tester.pumpAndSettle(); - - verify(() => onThreadTap.call(any())); - }); -} diff --git a/packages/stream_chat_flutter/test/src/message_list_view/floating_date_divider_test.dart b/packages/stream_chat_flutter/test/src/message_list_view/floating_date_divider_test.dart index 9af5114ba1..9535a19643 100644 --- a/packages/stream_chat_flutter/test/src/message_list_view/floating_date_divider_test.dart +++ b/packages/stream_chat_flutter/test/src/message_list_view/floating_date_divider_test.dart @@ -311,6 +311,7 @@ void main() { FloatingDateDivider( itemPositionListener: itemPositionListener, reverse: false, + fadeNearInlineDivider: false, messages: messages, itemCount: itemCount, ), @@ -381,6 +382,7 @@ void main() { FloatingDateDivider( itemPositionListener: itemPositionListener, reverse: true, // Use getBottomElementIndex + fadeNearInlineDivider: false, messages: messages, itemCount: itemCount, ), diff --git a/packages/stream_chat_flutter/test/src/message_list_view/message_list_view_test.dart b/packages/stream_chat_flutter/test/src/message_list_view/message_list_view_test.dart index 0fd7ae3eeb..f1d8b650e8 100644 --- a/packages/stream_chat_flutter/test/src/message_list_view/message_list_view_test.dart +++ b/packages/stream_chat_flutter/test/src/message_list_view/message_list_view_test.dart @@ -18,34 +18,24 @@ void main() { clientState = MockClientState(); when(() => client.state).thenAnswer((_) => clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'testid')); - when(() => clientState.currentUserStream) - .thenAnswer((_) => Stream.value(OwnUser(id: 'testid'))); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(OwnUser(id: 'testid'))); channel = MockChannel(); - when(() => channel.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); channelClientState = MockChannelState(); when(() => channel.client).thenReturn(client); when(() => channel.state).thenReturn(channelClientState); - when(() => channelClientState.threadsStream) - .thenAnswer((_) => const Stream.empty()); - when(() => channelClientState.messagesStream) - .thenAnswer((_) => const Stream.empty()); + when(() => channelClientState.threadsStream).thenAnswer((_) => const Stream.empty()); + when(() => channelClientState.messagesStream).thenAnswer((_) => const Stream.empty()); when(() => channelClientState.messages).thenReturn([]); when(() => channelClientState.isUpToDate).thenReturn(true); - when(() => channelClientState.isUpToDateStream) - .thenAnswer((_) => Stream.value(true)); - when(() => channelClientState.unreadCountStream) - .thenAnswer((_) => Stream.value(0)); + when(() => channelClientState.isUpToDateStream).thenAnswer((_) => Stream.value(true)); + when(() => channelClientState.unreadCountStream).thenAnswer((_) => Stream.value(0)); when(() => channelClientState.unreadCount).thenReturn(0); - when(() => channelClientState.readStream) - .thenAnswer((_) => const Stream.empty()); + when(() => channelClientState.readStream).thenAnswer((_) => const Stream.empty()); when(() => channelClientState.read).thenReturn([]); - when(() => channelClientState.membersStream) - .thenAnswer((_) => const Stream.empty()); + when(() => channelClientState.membersStream).thenAnswer((_) => const Stream.empty()); when(() => channelClientState.members).thenReturn([]); when(() => channelClientState.currentUserRead).thenReturn(null); - when(() => channelClientState.currentUserReadStream) - .thenAnswer((_) => const Stream.empty()); + when(() => channelClientState.currentUserReadStream).thenAnswer((_) => const Stream.empty()); }); // https://github.com/GetStream/stream-chat-flutter/issues/674 @@ -71,8 +61,7 @@ void main() { expect(find.byKey(emptyWidgetKey), findsOneWidget); }); - testWidgets('renders a non empty message list view with custom background', - (tester) async { + testWidgets('renders a non empty message list view with custom background', (tester) async { final message = Message( id: 'message1', text: 'Hello world!', @@ -136,8 +125,7 @@ void main() { ); }); - testWidgets('renders a non empty message list view with unread messages', - (tester) async { + testWidgets('renders a non empty message list view with unread messages', (tester) async { final user = OwnUser(id: 'testid'); final message = Message( id: 'message1', @@ -148,8 +136,7 @@ void main() { ), ); - when(() => channelClientState.read) - .thenReturn([Read(lastRead: DateTime.now(), user: user)]); + when(() => channelClientState.read).thenReturn([Read(lastRead: DateTime.now(), user: user)]); when(() => channelClientState.messagesStream).thenAnswer( (_) => Stream.value([message]), 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..add8396f09 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..c7638d3945 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..ba72ea1654 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..cba5644a08 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..e2772f5c06 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..37f33dc53c 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..3493dabe51 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..b760b7d0b9 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..855bc03e2b 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..16414bb424 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/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..c7ab21e839 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_modal/message_actions_modal_test.dart @@ -0,0 +1,299 @@ +// 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'; +import 'package:stream_core_flutter/stream_core_flutter.dart' show StreamIconData; + +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 = >[ + StreamContextMenuAction( + label: const Text('Reply'), + leading: const Icon(StreamIconData.reply20), + value: QuotedReply(message: message), + ), + StreamContextMenuAction( + label: const Text('Thread Reply'), + leading: const Icon(StreamIconData.thread20), + value: ThreadReply(message: message), + ), + StreamContextMenuAction( + label: const Text('Copy Message'), + leading: const Icon(StreamIconData.copy20), + value: CopyMessage(message: message), + ), + StreamContextMenuAction.destructive( + label: const Text('Delete Message'), + leading: const Icon(StreamIconData.delete20), + value: 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'), + alignment: AlignmentDirectional.centerStart, + ), + ), + ); + + // 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'), + alignment: AlignmentDirectional.centerStart, + showReactionPicker: true, + ), + ), + ); + + // Use a longer timeout to ensure everything is rendered + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(find.byType(StreamMessageReactionPicker), findsOneWidget); + }); + + testWidgets( + 'pops with SelectReaction when reaction is selected', + (tester) async { + MessageAction? messageAction; + + // Define custom reaction icons via resolver for testing. + const testReactionResolver = _TestReactionIconResolver( + defaultReactionTypes: {'like', 'love'}, + ); + + await tester.pumpWidget( + _wrapWithMaterialApp( + reactionIconResolver: testReactionResolver, + Builder( + builder: (context) => TextButton( + onPressed: () async { + messageAction = await showStreamDialog( + context: context, + builder: (_) => StreamMessageActionsModal( + message: message, + messageActions: messageActions, + messageWidget: const Text('Message Widget'), + alignment: AlignmentDirectional.centerStart, + showReactionPicker: true, + ), + ); + }, + child: const Text('Open Dialog'), + ), + ), + ), + ); + await tester.tap(find.text('Open Dialog')); + + // Use a longer timeout to ensure everything is rendered + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Verify reaction picker is shown + expect(find.byType(StreamMessageReactionPicker), findsOneWidget); + + // Reactions are rendered as StreamEmojiButton widgets. The resolver + // maps 'like' → 👍 and 'love' → ❤️. Find and tap the 'like' emoji. + final likeEmoji = find.text('👍'); + expect(likeEmoji, findsOneWidget); + await tester.tap(likeEmoji); + await tester.pumpAndSettle(); + + expect(messageAction, isA()); + // Verify the popped value has correct reaction type + expect((messageAction! as SelectReaction).reaction.type, 'like'); + + // Open dialog again and tap the second reaction (love) + await tester.tap(find.text('Open Dialog')); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + final loveEmoji = find.text('❤️'); + expect(loveEmoji, findsOneWidget); + await tester.tap(loveEmoji); + await tester.pumpAndSettle(); + + expect(messageAction, isA()); + // Verify the popped value has 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(), + alignment: AlignmentDirectional.centerStart, + ), + ), + ); + + 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(), + alignment: AlignmentDirectional.centerStart, + 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), + alignment: AlignmentDirectional.centerEnd, + ), + ), + ); + + 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), + alignment: AlignmentDirectional.centerEnd, + showReactionPicker: true, + ), + ), + ); + } + }); +} + +Widget _wrapWithMaterialApp( + Widget child, { + Brightness? brightness, + ReactionIconResolver? reactionIconResolver, +}) { + return Portal( + child: MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData(brightness: brightness), + builder: (context, child) => StreamChatConfiguration( + data: StreamChatConfigurationData( + reactionIconResolver: reactionIconResolver ?? const _TestReactionIconResolver(), + ), + child: StreamChatTheme( + data: StreamChatThemeData(brightness: brightness), + child: child ?? const SizedBox.shrink(), + ), + ), + home: 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, + ), + ), + ); + }, + ), + ), + ); +} + +class _TestReactionIconResolver extends ReactionIconResolver { + const _TestReactionIconResolver({ + this.defaultReactionTypes = const {'like', 'love', 'haha', 'wow', 'sad'}, + }); + + final Set defaultReactionTypes; + + @override + Set get defaultReactions => defaultReactionTypes; + + @override + Set get supportedReactions => defaultReactionTypes; + + @override + String? emojiCode(String type) => streamSupportedEmojis[type]?.emoji; + + @override + StreamEmojiContent resolve(String type) { + if (emojiCode(type) case final emoji?) return StreamUnicodeEmoji(emoji); + return const StreamUnicodeEmoji('❓'); + } +} 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..be1ff829ca --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_modal/moderated_message_actions_modal_test.dart @@ -0,0 +1,161 @@ +// 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'; +import 'package:stream_core_flutter/stream_core_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 = >[ + StreamContextMenuAction( + label: const Text('Send Anyway'), + leading: const Icon(StreamIconData.send20), + value: ResendMessage(message: message), + ), + StreamContextMenuAction( + label: const Text('Edit Message'), + leading: const Icon(StreamIconData.edit20), + value: EditMessage(message: message), + ), + StreamContextMenuAction.destructive( + label: const Text('Delete Message'), + leading: const Icon(StreamIconData.delete20), + value: 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(Icon), 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 pop with the correct value', (tester) async { + MessageAction? messageAction; + + await tester.pumpWidget( + _wrapWithMaterialApp( + Builder( + builder: (context) => TextButton( + onPressed: () async { + messageAction = await showStreamDialog( + context: context, + builder: (_) => ModeratedMessageActionsModal( + message: message, + messageActions: messageActions, + ), + ); + }, + child: const Text('Open Dialog'), + ), + ), + ), + ); + + // Open dialog and tap Send Anyway + await tester.tap(find.text('Open Dialog')); + await tester.pumpAndSettle(); + + // Tap on Send Anyway button + await tester.tap(find.text('Send Anyway')); + await tester.pumpAndSettle(); + expect(messageAction, isA()); + + // Open dialog and tap Edit Message + await tester.tap(find.text('Open Dialog')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Edit Message')); + await tester.pumpAndSettle(); + expect(messageAction, isA()); + + // Open dialog and tap Delete Message + await tester.tap(find.text('Open Dialog')); + await tester.pumpAndSettle(); + 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, + theme: ThemeData(brightness: brightness), + builder: (context, child) => StreamChatConfiguration( + data: StreamChatConfigurationData(), + child: StreamChatTheme( + data: StreamChatThemeData(brightness: brightness), + child: child ?? const SizedBox.shrink(), + ), + ), + home: 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/deleted_message_test.dart b/packages/stream_chat_flutter/test/src/message_widget/deleted_message_test.dart deleted file mode 100644 index 6fc7bfe6eb..0000000000 --- a/packages/stream_chat_flutter/test/src/message_widget/deleted_message_test.dart +++ /dev/null @@ -1,217 +0,0 @@ -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/stream_chat_flutter.dart'; - -import '../material_app_wrapper.dart'; -import '../mocks.dart'; - -void main() { - testWidgets('control test', (tester) async { - final client = MockClient(); - final clientState = MockClientState(); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - await tester.pumpWidget( - MaterialApp( - home: StreamChat( - client: client, - child: const Scaffold( - body: StreamDeletedMessage( - messageTheme: StreamMessageThemeData( - createdAtStyle: TextStyle( - color: Colors.black, - ), - messageTextStyle: TextStyle(), - ), - ), - ), - ), - ), - ); - - expect(find.text('Message deleted'), findsOneWidget); - }); - - goldenTest( - 'control golden light', - fileName: 'deleted_message_light', - constraints: const BoxConstraints.tightFor(width: 200, height: 200), - builder: () { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - final channelState = MockChannelState(); - final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => channel.lastMessageAt).thenReturn(lastMessageAt); - when(() => channel.state).thenReturn(channelState); - when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer( - (i) => Stream.value({ - 'name': 'test', - }), - ); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); - - when(() => clientState.totalUnreadCount).thenReturn(10); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(10)); - - final materialTheme = ThemeData.light( - useMaterial3: false, - ); - final theme = StreamChatThemeData.fromTheme(materialTheme); - return MaterialAppWrapper( - theme: materialTheme, - home: StreamChat( - streamChatThemeData: theme, - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold( - body: Center( - child: StreamDeletedMessage( - messageTheme: theme.ownMessageTheme, - ), - ), - ), - ), - ), - ); - }, - ); - - goldenTest( - 'control golden dark', - fileName: 'deleted_message_dark', - constraints: const BoxConstraints.tightFor(width: 200, height: 200), - builder: () { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - final channelState = MockChannelState(); - final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => channel.lastMessageAt).thenReturn(lastMessageAt); - when(() => channel.state).thenReturn(channelState); - when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer( - (i) => Stream.value({ - 'name': 'test', - }), - ); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); - - when(() => clientState.totalUnreadCount).thenReturn(10); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(10)); - - final materialTheme = ThemeData.dark( - useMaterial3: false, - ); - final theme = StreamChatThemeData.fromTheme(materialTheme); - return MaterialAppWrapper( - theme: materialTheme, - home: StreamChat( - streamChatThemeData: theme, - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold( - body: Center( - child: StreamDeletedMessage( - messageTheme: theme.ownMessageTheme, - ), - ), - ), - ), - ), - ); - }, - ); - - goldenTest( - 'golden customization test', - fileName: 'deleted_message_custom', - constraints: const BoxConstraints.tightFor(width: 200, height: 200), - builder: () { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - final channelState = MockChannelState(); - final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - when(() => channel.lastMessageAt).thenReturn(lastMessageAt); - when(() => channel.state).thenReturn(channelState); - when(() => channel.client).thenReturn(client); - when(() => channel.extraDataStream).thenAnswer( - (i) => Stream.value({ - 'name': 'test', - }), - ); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); - - when(() => clientState.totalUnreadCount).thenReturn(10); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(10)); - - final materialTheme = ThemeData.light( - useMaterial3: false, - ); - - var theme = StreamChatThemeData.fromTheme(materialTheme); - theme = theme.copyWith( - ownMessageTheme: theme.ownMessageTheme.copyWith( - messageDeletedStyle: theme.ownMessageTheme.messageTextStyle!.copyWith( - fontWeight: FontWeight.bold, - color: Colors.red, - ), - ), - ); - - return MaterialAppWrapper( - theme: materialTheme, - home: StreamChat( - streamChatThemeData: theme, - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: StreamChannel( - showLoading: false, - channel: channel, - child: Scaffold( - body: Center( - child: StreamDeletedMessage( - messageTheme: theme.ownMessageTheme, - reverse: true, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ), - ), - ), - ); - }, - ); -} 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 deleted file mode 100644 index ec3cb12740..0000000000 Binary files a/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_custom.png and /dev/null 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 deleted file mode 100644 index 0664144d1f..0000000000 Binary files a/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_dark.png and /dev/null 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 deleted file mode 100644 index 0eaf38d8c2..0000000000 Binary files a/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/deleted_message_light.png and /dev/null differ diff --git a/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/message_text.png b/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/message_text.png deleted file mode 100644 index 656da4e1cc..0000000000 Binary files a/packages/stream_chat_flutter/test/src/message_widget/goldens/ci/message_text.png and /dev/null differ diff --git a/packages/stream_chat_flutter/test/src/message_widget/message_text_test.dart b/packages/stream_chat_flutter/test/src/message_widget/message_text_test.dart deleted file mode 100644 index 1b294f9c21..0000000000 --- a/packages/stream_chat_flutter/test/src/message_widget/message_text_test.dart +++ /dev/null @@ -1,251 +0,0 @@ -import 'package:alchemist/alchemist.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../material_app_wrapper.dart'; -import '../mocks.dart'; -import '../simple_frame.dart'; - -void expectTextStrings(Iterable widgets, List strings) { - var currentString = 0; - for (final widget in widgets) { - if (widget is RichText) { - final span = widget.text as TextSpan; - final text = _extractTextFromTextSpan(span); - expect(text, equals(strings[currentString])); - currentString += 1; - } - } -} - -String _extractTextFromTextSpan(TextSpan span) { - var text = span.text ?? ''; - if (span.children != null) { - for (final child in span.children! as Iterable) { - text += _extractTextFromTextSpan(child); - } - } - return text; -} - -void main() { - testWidgets( - 'it should show correct message text', - (WidgetTester tester) async { - final currentUser = OwnUser(id: 'user-id'); - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - final channelState = MockChannelState(); - final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(currentUser); - when(() => clientState.currentUserStream) - .thenAnswer((_) => Stream.value(currentUser)); - when(() => channel.lastMessageAt).thenReturn(lastMessageAt); - when(() => channel.state).thenReturn(channelState); - when(() => channel.client).thenReturn(client); - when(() => channel.isMuted).thenReturn(false); - when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer((i) => Stream.value({ - 'name': 'test', - })); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); - - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamMessageText( - message: Message( - text: 'demo', - ), - messageTheme: streamTheme.otherMessageTheme), - ), - ), - ), - )); - - // wait for the initial state to be rendered. - await tester.pumpAndSettle(); - - expect(find.byType(MarkdownBody), findsOneWidget); - }, - ); - - group('Message with i18n field', () { - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - final channelState = MockChannelState(); - const messageTheme = StreamMessageThemeData(); - - final currentUser = OwnUser( - id: 'sahil', - language: 'hi', - ); - - setUp(() { - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(currentUser); - when(() => clientState.currentUserStream) - .thenAnswer((_) => Stream.value(currentUser)); - - when(() => channel.state).thenReturn(channelState); - when(() => channel.client).thenReturn(client); - when(() => channel.isMuted).thenReturn(false); - when(() => channel.isMutedStream).thenAnswer((_) => Stream.value(false)); - }); - - testWidgets( - 'should show correct translated message text as per user language', - (WidgetTester tester) async { - final message = Message( - text: 'Hello', - i18n: const { - 'en_text': 'Hello', - 'hi_text': 'नमस्ते', - 'language': 'en', - }, - ); - - await tester.pumpWidget( - MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamMessageText( - message: message, - messageTheme: messageTheme, - ), - ), - ), - ), - ), - ); - - // wait for the initial state to be rendered. - await tester.pump(Duration.zero); - - expect(find.byType(MarkdownBody), findsOneWidget); - - final widgets = tester.allWidgets; - expectTextStrings(widgets, ['नमस्ते']); - }, - ); - - testWidgets( - '''should show default text if i18n does not contain translations as per user language''', - (WidgetTester tester) async { - final message = Message( - text: 'Hello', - i18n: const { - 'en_text': 'Hello', - 'fr_text': 'Bonjour', - 'language': 'en', - }, - ); - - await tester.pumpWidget( - MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamMessageText( - message: message, - messageTheme: messageTheme, - ), - ), - ), - ), - ), - ); - - // wait for the initial state to be rendered. - await tester.pump(Duration.zero); - - expect(find.byType(MarkdownBody), findsOneWidget); - - final widgets = tester.allWidgets; - expectTextStrings(widgets, ['Hello']); - }, - ); - }); - - goldenTest( - 'control test', - fileName: 'message_text', - constraints: const BoxConstraints.tightFor(width: 300, height: 200), - builder: () { - final currentUser = OwnUser(id: 'user-id'); - final client = MockClient(); - final clientState = MockClientState(); - final channel = MockChannel(); - final channelState = MockChannelState(); - final lastMessageAt = DateTime.parse('2020-06-22 12:00:00'); - final themeData = ThemeData(); - final streamTheme = StreamChatThemeData.fromTheme(themeData); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(currentUser); - when(() => clientState.currentUserStream) - .thenAnswer((_) => Stream.value(currentUser)); - when(() => channel.lastMessageAt).thenReturn(lastMessageAt); - when(() => channel.state).thenReturn(channelState); - when(() => channel.client).thenReturn(client); - when(() => channel.isMuted).thenReturn(false); - when(() => channel.isMutedStream).thenAnswer((i) => Stream.value(false)); - when(() => channel.extraDataStream).thenAnswer( - (i) => Stream.value({ - 'name': 'test', - }), - ); - when(() => channel.extraData).thenReturn({ - 'name': 'test', - }); - - const messageText = ''' -a message. -with multiple lines -and a list: -- a. okasd -- b lllll - -cool.'''; - - return MaterialAppWrapper( - home: SimpleFrame( - child: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.wifi]), - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamMessageText( - message: Message( - text: messageText, - ), - messageTheme: streamTheme.otherMessageTheme, - ), - ), - ), - ), - ), - ); - }, - ); -} diff --git a/packages/stream_chat_flutter/test/src/message_widget/username_test.dart b/packages/stream_chat_flutter/test/src/message_widget/username_test.dart deleted file mode 100644 index bde411cb38..0000000000 --- a/packages/stream_chat_flutter/test/src/message_widget/username_test.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/src/message_widget/username.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -void main() { - testWidgets('Username', (tester) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Center( - child: Username( - message: Message(), - messageTheme: StreamChatThemeData.light().ownMessageTheme, - ), - ), - ), - ), - ); - - expect(find.byType(Text), findsOneWidget); - }); -} diff --git a/packages/stream_chat_flutter/test/src/misc/audio_waveform_test.dart b/packages/stream_chat_flutter/test/src/misc/audio_waveform_test.dart index e9a8b75902..f9663694f9 100644 --- a/packages/stream_chat_flutter/test/src/misc/audio_waveform_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/audio_waveform_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/misc/audio_waveform.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import '../mocks.dart'; @@ -215,16 +214,19 @@ Widget _wrapWithMaterialApp( Brightness? brightness, }) { return MaterialApp( + theme: ThemeData(brightness: brightness), 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: widget), - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Center(child: widget), + ); + }, + ), ), ); } diff --git a/packages/stream_chat_flutter/test/src/misc/back_button_test.dart b/packages/stream_chat_flutter/test/src/misc/back_button_test.dart index c4ae6efc2a..5025c69c86 100644 --- a/packages/stream_chat_flutter/test/src/misc/back_button_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/back_button_test.dart @@ -122,8 +122,7 @@ void main() { when(() => client.state).thenReturn(clientState); when(() => clientState.totalUnreadCount).thenAnswer((_) => 0); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((_) => Stream.value(0)); + when(() => clientState.totalUnreadCountStream).thenAnswer((_) => Stream.value(0)); await tester.pumpWidget( MaterialApp( diff --git a/packages/stream_chat_flutter/test/src/misc/date_divider_test.dart b/packages/stream_chat_flutter/test/src/misc/date_divider_test.dart index af500a2039..a9eb6fb83a 100644 --- a/packages/stream_chat_flutter/test/src/misc/date_divider_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/date_divider_test.dart @@ -49,8 +49,7 @@ void main() { child: Scaffold( body: StreamDateDivider( dateTime: testDate, - formatter: (context, date) => - 'Custom: ${date.day}/${date.month}', + formatter: (context, date) => 'Custom: ${date.day}/${date.month}', ), ), ), 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..b982f08bc4 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..8cf9709e62 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..4bef4095f2 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..f3cfab3aa4 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..eef3745e53 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..f9b6f05925 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..32fe1bae91 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..d15af71fc8 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..f4809eeaee 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..9774ed5ad8 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..de9e300313 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..a3a682b912 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..fa045a518a 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..54385c30f5 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..66f6094871 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..7e60fc17c7 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..d7b8a4dfea 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_dark.png b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_timestamp_dark.png index 90adba2dc0..4581216051 100644 Binary files a/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_timestamp_dark.png and b/packages/stream_chat_flutter/test/src/misc/goldens/ci/stream_timestamp_dark.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 68cab44d12..0fe2ab210e 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 b4f7b3cf34..3c10fdf7e3 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 c3a015a176..636b22dfa2 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/misc/reaction_bubble_test.dart b/packages/stream_chat_flutter/test/src/misc/reaction_bubble_test.dart deleted file mode 100644 index 2dd7ef0b19..0000000000 --- a/packages/stream_chat_flutter/test/src/misc/reaction_bubble_test.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'package:alchemist/alchemist.dart'; -import 'package:flutter/material.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../material_app_wrapper.dart'; -import '../mocks.dart'; - -void main() { - goldenTest( - 'it should show a like - light theme', - fileName: 'reaction_bubble_like_light', - constraints: const BoxConstraints.tightFor(width: 100, height: 100), - builder: () { - final client = MockClient(); - final clientState = MockClientState(); - final themeData = ThemeData.light( - useMaterial3: false, - ); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - final theme = StreamChatThemeData.fromTheme(themeData); - return MaterialAppWrapper( - theme: themeData, - home: StreamChat( - client: client, - streamChatThemeData: theme, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Scaffold( - body: Center( - child: StreamReactionBubble( - reactions: [ - Reaction( - type: 'like', - user: User(id: 'test'), - ), - ], - borderColor: theme.ownMessageTheme.reactionsBorderColor!, - backgroundColor: - theme.ownMessageTheme.reactionsBackgroundColor!, - maskColor: theme.ownMessageTheme.reactionsMaskColor!, - ), - ), - ), - ), - ); - }, - ); - - goldenTest( - 'it should show a like - dark theme', - fileName: 'reaction_bubble_like_dark', - constraints: const BoxConstraints.tightFor(width: 100, height: 100), - builder: () { - final client = MockClient(); - final clientState = MockClientState(); - final themeData = ThemeData.dark(); - final theme = StreamChatThemeData.fromTheme(themeData); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - return MaterialAppWrapper( - theme: themeData, - home: StreamChat( - client: client, - streamChatThemeData: StreamChatThemeData.fromTheme(themeData), - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Scaffold( - body: Center( - child: StreamReactionBubble( - reactions: [ - Reaction( - type: 'like', - user: User(id: 'test'), - ), - ], - borderColor: theme.ownMessageTheme.reactionsBorderColor!, - backgroundColor: - theme.ownMessageTheme.reactionsBackgroundColor!, - maskColor: theme.ownMessageTheme.reactionsMaskColor!, - ), - ), - ), - ), - ); - }, - ); - - goldenTest( - 'it should show three reactions - light theme', - fileName: 'reaction_bubble_3_light', - constraints: const BoxConstraints.tightFor(width: 140, height: 140), - builder: () { - final client = MockClient(); - final clientState = MockClientState(); - final themeData = ThemeData.light(); - final theme = StreamChatThemeData.fromTheme(themeData); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - return MaterialAppWrapper( - theme: themeData, - home: StreamChat( - client: client, - streamChatThemeData: StreamChatThemeData.fromTheme(themeData), - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Scaffold( - body: Center( - child: StreamReactionBubble( - reactions: [ - Reaction( - type: 'like', - user: User(id: 'test'), - ), - Reaction( - type: 'like', - user: User(id: 'user-id'), - ), - Reaction( - type: 'like', - user: User(id: 'test'), - ), - ], - borderColor: theme.ownMessageTheme.reactionsBorderColor!, - backgroundColor: - theme.ownMessageTheme.reactionsBackgroundColor!, - maskColor: theme.ownMessageTheme.reactionsMaskColor!, - ), - ), - ), - ), - ); - }, - ); - - goldenTest( - 'it should show three reactions - dark theme', - fileName: 'reaction_bubble_3_dark', - constraints: const BoxConstraints.tightFor(width: 140, height: 140), - builder: () { - final client = MockClient(); - final clientState = MockClientState(); - final themeData = ThemeData.dark(); - final theme = StreamChatThemeData.fromTheme(themeData); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - return MaterialAppWrapper( - theme: themeData, - home: StreamChat( - client: client, - streamChatThemeData: StreamChatThemeData.fromTheme(themeData), - connectivityStream: Stream.value([ConnectivityResult.mobile]), - child: Scaffold( - body: Center( - child: StreamReactionBubble( - reactions: [ - Reaction( - type: 'like', - user: User(id: 'test'), - ), - Reaction( - type: 'like', - user: User(id: 'user-id'), - ), - Reaction( - type: 'like', - user: User(id: 'test'), - ), - ], - borderColor: theme.ownMessageTheme.reactionsBorderColor!, - backgroundColor: - theme.ownMessageTheme.reactionsBackgroundColor!, - maskColor: theme.ownMessageTheme.reactionsMaskColor!, - ), - ), - ), - ), - ); - }, - ); - - goldenTest( - 'it should show two reactions with customized ui', - fileName: 'reaction_bubble_2', - constraints: const BoxConstraints.tightFor(width: 200, height: 200), - builder: () { - final client = MockClient(); - final clientState = MockClientState(); - final themeData = ThemeData( - useMaterial3: false, - ); - - when(() => client.state).thenReturn(clientState); - when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); - - return MaterialAppWrapper( - theme: themeData, - home: StreamChat( - client: client, - connectivityStream: Stream.value([ConnectivityResult.mobile]), - streamChatThemeData: StreamChatThemeData.fromTheme(themeData), - child: Scaffold( - body: Center( - child: StreamReactionBubble( - reactions: [ - Reaction( - type: 'like', - user: User(id: 'test'), - ), - Reaction( - type: 'love', - user: User(id: 'user-id'), - ), - Reaction( - type: 'unknown', - user: User(id: 'test'), - ), - ], - borderColor: Colors.red, - backgroundColor: Colors.blue, - maskColor: Colors.green, - reverse: true, - flipTail: true, - tailCirclesSpacing: 4, - ), - ), - ), - ), - ); - }, - ); -} diff --git a/packages/stream_chat_flutter/test/src/misc/system_message_test.dart b/packages/stream_chat_flutter/test/src/misc/system_message_test.dart index bbbb8dd896..03e4234d09 100644 --- a/packages/stream_chat_flutter/test/src/misc/system_message_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/system_message_test.dart @@ -32,27 +32,28 @@ void main() { }); when(() => clientState.totalUnreadCount).thenReturn(10); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(10)); + when(() => clientState.totalUnreadCountStream).thenAnswer((i) => Stream.value(10)); var tapped = false; - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamSystemMessage( - onMessageTap: (m) => tapped = true, - message: Message( - text: 'demo message', + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: StreamSystemMessage( + onMessageTap: (m) => tapped = true, + message: Message( + text: 'demo message', + ), ), ), ), ), ), - )); + ); // wait for the initial state to be rendered. await tester.pumpAndSettle(); @@ -90,8 +91,7 @@ void main() { }); when(() => clientState.totalUnreadCount).thenReturn(10); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(10)); + when(() => clientState.totalUnreadCountStream).thenAnswer((i) => Stream.value(10)); return MaterialAppWrapper( theme: ThemeData.light(), @@ -142,8 +142,7 @@ void main() { }); when(() => clientState.totalUnreadCount).thenReturn(10); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(10)); + when(() => clientState.totalUnreadCountStream).thenAnswer((i) => Stream.value(10)); return MaterialAppWrapper( theme: ThemeData.dark(), diff --git a/packages/stream_chat_flutter/test/src/misc/thread_header_test.dart b/packages/stream_chat_flutter/test/src/misc/thread_header_test.dart index b7cbe4e96c..c818e9c2f8 100644 --- a/packages/stream_chat_flutter/test/src/misc/thread_header_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/thread_header_test.dart @@ -27,14 +27,13 @@ void main() { when(() => channel.name).thenReturn('test'); when(() => channel.nameStream).thenAnswer((i) => Stream.value('test')); when(() => channelState.unreadCount).thenReturn(1); - when(() => channelState.unreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); when(() => channelState.membersStream).thenAnswer( (i) => Stream.value([ Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -43,34 +42,33 @@ void main() { user: User(id: 'user-id'), ), ]); - when(() => client.wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); + when(() => client.wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.connecting)); when(() => clientState.totalUnreadCount).thenAnswer((i) => 1); - when(() => clientState.totalUnreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => clientState.totalUnreadCountStream).thenAnswer((i) => Stream.value(1)); - await tester.pumpWidget(MaterialApp( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamThreadHeader( - parent: Message(), + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: StreamThreadHeader( + parent: Message(replyCount: 1), + showTypingIndicator: false, + ), ), ), ), ), - )); + ); // wait for the initial state to be rendered. await tester.pumpAndSettle(); - expect(find.text('with '), findsOneWidget); - expect(find.byType(StreamChannelName), findsOneWidget); expect(find.byType(StreamBackButton), findsOneWidget); - expect(find.text('1'), findsOneWidget); - expect(find.text('Thread Reply'), findsOneWidget); + expect(find.text('1 reply'), findsOneWidget); + expect(find.text('Thread'), findsOneWidget); }, ); @@ -99,14 +97,13 @@ void main() { 'name': 'test', }); when(() => channelState.unreadCount).thenReturn(1); - when(() => channelState.unreadCountStream) - .thenAnswer((i) => Stream.value(1)); + when(() => channelState.unreadCountStream).thenAnswer((i) => Stream.value(1)); when(() => channelState.membersStream).thenAnswer( (i) => Stream.value([ Member( userId: 'user-id', user: User(id: 'user-id'), - ) + ), ]), ); when(() => channelState.members).thenReturn([ @@ -117,28 +114,30 @@ void main() { ]); var tapped = false; - await tester.pumpWidget(MaterialAppWrapper( - home: StreamChat( - client: client, - child: StreamChannel( - channel: channel, - child: Scaffold( - body: StreamThreadHeader( - parent: Message(), - subtitle: const Text('subtitle'), - leading: const Text('leading'), - title: const Text('title'), - onTitleTap: () { - tapped = true; - }, - actions: const [ - Text('action'), - ], + await tester.pumpWidget( + MaterialAppWrapper( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: Scaffold( + body: StreamThreadHeader( + parent: Message(), + subtitle: const Text('subtitle'), + leading: const Text('leading'), + title: const Text('title'), + onTitleTap: () { + tapped = true; + }, + actions: const [ + Text('action'), + ], + ), ), ), ), ), - )); + ); // wait for the initial state to be rendered. await tester.pumpAndSettle(); diff --git a/packages/stream_chat_flutter/test/src/misc/timestamp_test.dart b/packages/stream_chat_flutter/test/src/misc/timestamp_test.dart index f08263ae77..0be181e0b6 100644 --- a/packages/stream_chat_flutter/test/src/misc/timestamp_test.dart +++ b/packages/stream_chat_flutter/test/src/misc/timestamp_test.dart @@ -34,13 +34,15 @@ Widget _wrapWithMaterialApp( return MaterialApp( home: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: Center(child: widget), - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Center(child: widget), + ); + }, + ), ), ); } diff --git a/packages/stream_chat_flutter/test/src/mocks.dart b/packages/stream_chat_flutter/test/src/mocks.dart index 5418d29303..8fd0231363 100644 --- a/packages/stream_chat_flutter/test/src/mocks.dart +++ b/packages/stream_chat_flutter/test/src/mocks.dart @@ -6,8 +6,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class MockClient extends Mock implements StreamChatClient { MockClient() { when(() => wsConnectionStatus).thenReturn(ConnectionStatus.connected); - when(() => wsConnectionStatusStream) - .thenAnswer((_) => Stream.value(ConnectionStatus.connected)); + when(() => wsConnectionStatusStream).thenAnswer((_) => Stream.value(ConnectionStatus.connected)); + when(() => state).thenReturn(MockClientState()); } } @@ -21,7 +21,8 @@ class MockChannel extends Mock implements Channel { ChannelCapability.sendMessage, ChannelCapability.uploadFile, ], - }); + Stream? eventStream, + }) : _eventStream = eventStream ?? const Stream.empty(); @override final String type; @@ -61,6 +62,19 @@ class MockChannel extends Mock implements Channel { Future keyStroke([String? parentId]) async { return; } + + final Stream _eventStream; + + @override + Stream on([ + String? eventType, + String? eventType2, + String? eventType3, + String? eventType4, + ]) { + if (eventType == null) return _eventStream; + return _eventStream.where((e) => e.type == eventType); + } } class MockChannelState extends Mock implements ChannelClientState { @@ -95,8 +109,7 @@ class MockAttachment extends Mock implements Attachment {} class MockVlcManagerDesktop extends Mock implements VlcManagerDesktop {} -class MockStreamMemberListController extends Mock - implements StreamMemberListController { +class MockStreamMemberListController extends Mock implements StreamMemberListController { @override PagedValue value = const PagedValue.loading(); } diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_dark.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_dark.png index 365f508f16..3147e1e01d 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_light.png b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_light.png index 507c29ed23..72d412692d 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_light.png and b/packages/stream_chat_flutter/test/src/poll/creator/goldens/ci/poll_delete_option_dialog_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 f41c7e807d..7d50ee1a0b 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 6cc73bd1f2..21e5bd213f 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 6b7efe88a2..629bb9ce23 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 b9948d4951..24c03f908d 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 ffb9a6793d..fdffc5c05a 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 60c54343d2..325781cf82 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 dfdeaadcbc..64ac751f02 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 0ce9d73fb2..ae1e3618ed 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 bccb8b50d3..52ca2ca4c9 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 f0ab2e22ba..180d152692 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 66e08bc545..73462eb438 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 1e35c96ac7..97ca70c7e8 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/creator/poll_option_reorderable_list_view_test.dart b/packages/stream_chat_flutter/test/src/poll/creator/poll_option_reorderable_list_view_test.dart index d0d6ab7e66..b32317d9b2 100644 --- a/packages/stream_chat_flutter/test/src/poll/creator/poll_option_reorderable_list_view_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/creator/poll_option_reorderable_list_view_test.dart @@ -5,8 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/src/poll/creator/poll_option_reorderable_list_view.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -import '../../utils/finders.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; void main() { for (final brightness in Brightness.values) { @@ -53,15 +52,17 @@ void main() { testWidgets('should enforce minimum options requirement', (tester) async { var optionsChanged = []; - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: 3, max: null), - initialOptions: [ - PollOptionItem(text: 'Option 1'), - ], - onOptionsChanged: (options) => optionsChanged = options, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: 3, max: null), + initialOptions: [ + PollOptionItem(text: 'Option 1'), + ], + onOptionsChanged: (options) => optionsChanged = options, + ), ), - )); + ); // Should automatically add options to meet minimum requirement final textFields = find.byType(TextField); @@ -73,16 +74,18 @@ void main() { }); testWidgets('should respect maximum options limit', (tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: null, max: 3), - initialOptions: [ - PollOptionItem(text: 'Option 1'), - PollOptionItem(text: 'Option 2'), - PollOptionItem(text: 'Option 3'), - ], + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: null, max: 3), + initialOptions: [ + PollOptionItem(text: 'Option 1'), + PollOptionItem(text: 'Option 2'), + PollOptionItem(text: 'Option 3'), + ], + ), ), - )); + ); // Find the add button final addButton = find.byType(FilledButton); @@ -94,15 +97,17 @@ void main() { }); testWidgets('should respect both min and max options', (tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: 2, max: 4), - initialOptions: [ - PollOptionItem(text: 'Option 1'), - PollOptionItem(text: 'Option 2'), - ], + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: 2, max: 4), + initialOptions: [ + PollOptionItem(text: 'Option 1'), + PollOptionItem(text: 'Option 2'), + ], + ), ), - )); + ); // Should have 2 options initially (meeting minimum) final textFields = find.byType(TextField); @@ -133,15 +138,17 @@ void main() { testWidgets( 'should work with unlimited options when max is null', (tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: 2, max: null), - initialOptions: [ - PollOptionItem(text: 'Option 1'), - PollOptionItem(text: 'Option 2'), - ], + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: 2, max: null), + initialOptions: [ + PollOptionItem(text: 'Option 1'), + PollOptionItem(text: 'Option 2'), + ], + ), ), - )); + ); // Add button should be enabled for unlimited options final addButton = find.byType(FilledButton); @@ -153,14 +160,16 @@ void main() { group('Auto-Focus Functionality', () { testWidgets('should auto-focus on newly added option', (tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: 1, max: null), - initialOptions: [ - PollOptionItem(text: 'Option 1'), - ], + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: 1, max: null), + initialOptions: [ + PollOptionItem(text: 'Option 1'), + ], + ), ), - )); + ); // Find the add button and tap it final addButton = find.byType(FilledButton); @@ -182,16 +191,18 @@ void main() { testWidgets( 'should disable add button when empty option exists', (tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: null, max: 5), - initialOptions: [ - PollOptionItem(text: 'Option 1'), - PollOptionItem(text: 'Option 2'), - PollOptionItem(text: ''), // Empty option - ], + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: null, max: 5), + initialOptions: [ + PollOptionItem(text: 'Option 1'), + PollOptionItem(text: 'Option 2'), + PollOptionItem(text: ''), // Empty option + ], + ), ), - )); + ); // Find the add button final addButton = find.byType(FilledButton); @@ -206,15 +217,17 @@ void main() { testWidgets( 'should enable add button when no empty options exist', (tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: null, max: 5), - initialOptions: [ - PollOptionItem(text: 'Option 1'), - PollOptionItem(text: 'Option 2'), - ], + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: null, max: 5), + initialOptions: [ + PollOptionItem(text: 'Option 1'), + PollOptionItem(text: 'Option 2'), + ], + ), ), - )); + ); // Find the add button final addButton = find.byType(FilledButton); @@ -229,15 +242,17 @@ void main() { testWidgets( 'should re-enable add button after filling empty option', (tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: null, max: 5), - initialOptions: [ - PollOptionItem(text: 'Option 1'), - PollOptionItem(text: ''), // Empty option - ], + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: null, max: 5), + initialOptions: [ + PollOptionItem(text: 'Option 1'), + PollOptionItem(text: ''), // Empty option + ], + ), ), - )); + ); // Initially, add button should be disabled var addButton = find.byType(FilledButton); @@ -263,16 +278,18 @@ void main() { (tester) async { var optionsChanged = []; - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: 2, max: 5), - initialOptions: [ - PollOptionItem(text: 'Option 1'), - PollOptionItem(text: 'Option 2'), - ], - onOptionsChanged: (options) => optionsChanged = options, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: 2, max: 5), + initialOptions: [ + PollOptionItem(text: 'Option 1'), + PollOptionItem(text: 'Option 2'), + ], + onOptionsChanged: (options) => optionsChanged = options, + ), ), - )); + ); // Find the add button and tap it final addButton = find.byType(FilledButton); @@ -291,13 +308,15 @@ void main() { (tester) async { var optionsChanged = []; - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: 2, max: null), - initialOptions: const [], // No initial options - onOptionsChanged: (options) => optionsChanged = options, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: 2, max: null), + initialOptions: const [], // No initial options + onOptionsChanged: (options) => optionsChanged = options, + ), ), - )); + ); // Should auto-add options to meet minimum requirement final textFields = find.byType(TextField); @@ -307,27 +326,31 @@ void main() { ); testWidgets('should handle updating initial options', (tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - initialOptions: [ - PollOptionItem(text: 'Option 1'), - ], + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + initialOptions: [ + PollOptionItem(text: 'Option 1'), + ], + ), ), - )); + ); // Initially should have 1 option expect(find.byType(TextField), findsNWidgets(1)); // Update with new options - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - initialOptions: [ - PollOptionItem(text: 'Option 1'), - PollOptionItem(text: 'Option 2'), - PollOptionItem(text: 'Option 3'), - ], + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + initialOptions: [ + PollOptionItem(text: 'Option 1'), + PollOptionItem(text: 'Option 2'), + PollOptionItem(text: 'Option 3'), + ], + ), ), - )); + ); // Should now have 3 options expect(find.byType(TextField), findsNWidgets(3)); @@ -336,19 +359,21 @@ void main() { group('Delete Option Functionality', () { testWidgets('should show delete confirmation dialog', (tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: 2, max: null), - initialOptions: [ - PollOptionItem(text: 'Option 1'), - PollOptionItem(text: 'Option 2'), - PollOptionItem(text: 'Option 3'), - ], + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: 2, max: null), + initialOptions: [ + PollOptionItem(text: 'Option 1'), + PollOptionItem(text: 'Option 2'), + PollOptionItem(text: 'Option 3'), + ], + ), ), - )); + ); // Find the delete buttons - final deleteButtons = find.bySvgIcon(StreamSvgIcons.delete); + final deleteButtons = find.byIcon(StreamIconData.delete20); expect(deleteButtons, findsNWidgets(3)); // Tap the first delete button @@ -368,23 +393,25 @@ void main() { testWidgets('should delete option when confirmed', (tester) async { var optionsChanged = []; - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: 2, max: null), - initialOptions: [ - PollOptionItem(text: 'Option 1'), - PollOptionItem(text: 'Option 2'), - PollOptionItem(text: 'Option 3'), - ], - onOptionsChanged: (options) => optionsChanged = options, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: 2, max: null), + initialOptions: [ + PollOptionItem(text: 'Option 1'), + PollOptionItem(text: 'Option 2'), + PollOptionItem(text: 'Option 3'), + ], + onOptionsChanged: (options) => optionsChanged = options, + ), ), - )); + ); // Initially should have 3 options expect(find.byType(TextField), findsNWidgets(3)); // Find and tap the delete button for the first option - final deleteButtons = find.bySvgIcon(StreamSvgIcons.delete); + final deleteButtons = find.byIcon(StreamIconData.delete20); await tester.tap(deleteButtons.first); await tester.pumpAndSettle(); @@ -400,23 +427,25 @@ void main() { testWidgets('should not delete option when cancelled', (tester) async { var optionsChanged = []; - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: 2, max: null), - initialOptions: [ - PollOptionItem(text: 'Option 1'), - PollOptionItem(text: 'Option 2'), - PollOptionItem(text: 'Option 3'), - ], - onOptionsChanged: (options) => optionsChanged = options, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: 2, max: null), + initialOptions: [ + PollOptionItem(text: 'Option 1'), + PollOptionItem(text: 'Option 2'), + PollOptionItem(text: 'Option 3'), + ], + onOptionsChanged: (options) => optionsChanged = options, + ), ), - )); + ); // Initially should have 3 options expect(find.byType(TextField), findsNWidgets(3)); // Find and tap the delete button for the first option - final deleteButtons = find.bySvgIcon(StreamSvgIcons.delete); + final deleteButtons = find.byIcon(StreamIconData.delete20); await tester.tap(deleteButtons.first); await tester.pumpAndSettle(); @@ -436,19 +465,21 @@ void main() { final option1 = PollOptionItem(text: 'Option 1'); final option2 = PollOptionItem(text: 'Option 2'); - await tester.pumpWidget(_wrapWithMaterialApp( - PollOptionReorderableListView( - optionsRange: (min: 2, max: null), - initialOptions: [option1, option2], - onOptionsChanged: (options) => optionsChanged = options, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollOptionReorderableListView( + optionsRange: (min: 2, max: null), + initialOptions: [option1, option2], + onOptionsChanged: (options) => optionsChanged = options, + ), ), - )); + ); // Should have 2 options (minimum) expect(find.byType(TextField), findsNWidgets(2)); // Try to delete the first option - final deleteButtons = find.bySvgIcon(StreamSvgIcons.delete); + final deleteButtons = find.byIcon(StreamIconData.delete20); await tester.tap(deleteButtons.first); await tester.pumpAndSettle(); diff --git a/packages/stream_chat_flutter/test/src/poll/creator/poll_question_text_field_test.dart b/packages/stream_chat_flutter/test/src/poll/creator/poll_question_text_field_test.dart index 8a07c4e7a9..8a77888e51 100644 --- a/packages/stream_chat_flutter/test/src/poll/creator/poll_question_text_field_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/creator/poll_question_text_field_test.dart @@ -47,18 +47,20 @@ Widget _wrapWithMaterialApp( return MaterialApp( 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: widget, + 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: widget, + ), ), - ), - ); - }), + ); + }, + ), ), ); } diff --git a/packages/stream_chat_flutter/test/src/poll/creator/stream_poll_creator_widget_test.dart b/packages/stream_chat_flutter/test/src/poll/creator/stream_poll_creator_widget_test.dart index ba45ab36cb..78eb1b99ae 100644 --- a/packages/stream_chat_flutter/test/src/poll/creator/stream_poll_creator_widget_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/creator/stream_poll_creator_widget_test.dart @@ -28,8 +28,7 @@ void main() { expect(find.byType(PollSwitchListTile), findsNWidgets(4)); }); - testWidgets('StreamPollCreatorWidget updates poll state correctly', - (tester) async { + testWidgets('StreamPollCreatorWidget updates poll state correctly', (tester) async { final controller = StreamPollController( config: const PollConfig( nameRange: (min: 1, max: 150), 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 f41c7e807d..7d50ee1a0b 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 6b7efe88a2..629bb9ce23 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 b9948d4951..24c03f908d 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 ffb9a6793d..fdffc5c05a 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 dfdeaadcbc..64ac751f02 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 0ce9d73fb2..ae1e3618ed 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 bccb8b50d3..52ca2ca4c9 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 f0ab2e22ba..180d152692 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 66e08bc545..73462eb438 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 1e35c96ac7..97ca70c7e8 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 ec1ddc32ad..171e6bf959 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 75b26f4636..4dcc08a726 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 0b7d62a345..5c812dcd31 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 96b3b2e4e0..ec960f5a14 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 025eabde0c..2ca407113b 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 cd3061ecdc..352a6c0e31 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 5886d2ed80..d39a882fe2 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 1548f48b97..87c0315c35 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 2d57a6786e..9d1c3dd68f 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 f0e42978e5..d236e1ed1a 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_dark.png b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_end_vote_dialog_dark.png index 33575c2c74..8d71d47001 100644 Binary files a/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_end_vote_dialog_dark.png and b/packages/stream_chat_flutter/test/src/poll/interactor/goldens/ci/poll_end_vote_dialog_dark.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 4f80cb952f..8c46dcbeb6 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 7f546a1b99..65f1794b38 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 2e47747f1c..375c7b8b60 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 866b92da65..c8619f89a5 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 4c8650e0d7..dc46957c29 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 06ef0acf10..6e321373a7 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 32f08a3db9..6b0fc025b2 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 db9034b504..f184ea8d46 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 012afaf352..ce1143f7ab 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 73177c6775..4b06c19324 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 87bc90e9c9..3704324551 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 7f546a1b99..65f1794b38 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 2e47747f1c..375c7b8b60 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 1ef86b9ee9..066c6c333e 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 87af27ccac..3166377ddd 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 d1ed1d271f..ef0f74b8b5 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 77c4f22054..dd2c06920b 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 20e74ef186..e0f0576813 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 2297e20808..c98b0ddebb 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 c4a779c87c..dbbc5da12b 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 0930a1f988..990c4b3de9 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/poll/interactor/poll_end_vote_dialog_test.dart b/packages/stream_chat_flutter/test/src/poll/interactor/poll_end_vote_dialog_test.dart index a255dac23d..e981bf873d 100644 --- a/packages/stream_chat_flutter/test/src/poll/interactor/poll_end_vote_dialog_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/interactor/poll_end_vote_dialog_test.dart @@ -102,7 +102,7 @@ void main() { goldenTest( '[${brightness.name}] -> PollEndVoteDialog looks fine', fileName: 'poll_end_vote_dialog_${brightness.name}', - constraints: const BoxConstraints.tightFor(width: 400, height: 200), + constraints: const BoxConstraints.tightFor(width: 400, height: 220), builder: () => _wrapWithMaterialApp( brightness: brightness, const PollEndVoteDialog(), diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/poll_footer_test.dart b/packages/stream_chat_flutter/test/src/poll/interactor/poll_footer_test.dart index 1a61932345..cdc50e37af 100644 --- a/packages/stream_chat_flutter/test/src/poll/interactor/poll_footer_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/interactor/poll_footer_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/src/poll/interactor/poll_footer.dart'; import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +import 'package:stream_core_flutter/stream_core_flutter.dart'; void main() async { final currentUser = User(id: 'user-1', name: 'User'); @@ -20,23 +21,25 @@ void main() async { testWidgets( 'End Vote button is visible and enabled for the creator on open poll', (WidgetTester tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollFooter( - poll: poll.copyWith(createdBy: currentUser), - currentUser: currentUser, - onEndVote: () {}, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollFooter( + poll: poll.copyWith(createdBy: currentUser), + currentUser: currentUser, + onEndVote: () {}, + ), ), - )); + ); final endVoteButton = find.ancestor( of: find.text('End Vote'), - matching: find.byType(PollFooterButton), + matching: find.byType(StreamButton), ); expect(endVoteButton, findsOneWidget); expect( - tester.widget(endVoteButton).onPressed, + tester.widget(endVoteButton).props.onTap, isNotNull, ); }, @@ -45,17 +48,19 @@ void main() async { testWidgets( 'End Vote button is not visible for non-creator', (WidgetTester tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollFooter( - poll: poll, - currentUser: currentUser, - onEndVote: () {}, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollFooter( + poll: poll, + currentUser: currentUser, + onEndVote: () {}, + ), ), - )); + ); final endVoteButton = find.ancestor( of: find.text('End Vote'), - matching: find.byType(PollFooterButton), + matching: find.byType(StreamButton), ); expect(endVoteButton, findsNothing); @@ -65,20 +70,22 @@ void main() async { testWidgets( 'End Vote button is not visible for closed poll', (WidgetTester tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollFooter( - poll: poll.copyWith( - isClosed: true, - createdBy: currentUser, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollFooter( + poll: poll.copyWith( + isClosed: true, + createdBy: currentUser, + ), + currentUser: currentUser, + onEndVote: () {}, ), - currentUser: currentUser, - onEndVote: () {}, ), - )); + ); final endVoteButton = find.ancestor( of: find.text('End Vote'), - matching: find.byType(PollFooterButton), + matching: find.byType(StreamButton), ); expect(endVoteButton, findsNothing); @@ -88,22 +95,24 @@ void main() async { testWidgets( 'Add Comment button is visible and enabled when poll allows answers', (WidgetTester tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollFooter( - poll: poll.copyWith(allowAnswers: true), - currentUser: currentUser, - onAddComment: () {}, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollFooter( + poll: poll.copyWith(allowAnswers: true), + currentUser: currentUser, + onAddComment: () {}, + ), ), - )); + ); final addCommentButton = find.ancestor( of: find.text('Add a comment'), - matching: find.byType(PollFooterButton), + matching: find.byType(StreamButton), ); expect(addCommentButton, findsOneWidget); expect( - tester.widget(addCommentButton).onPressed, + tester.widget(addCommentButton).props.onTap, isNotNull, ); }, @@ -112,20 +121,22 @@ void main() async { testWidgets( 'Add Comment button is not visible when poll is closed', (WidgetTester tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollFooter( - poll: poll.copyWith( - isClosed: true, - allowAnswers: true, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollFooter( + poll: poll.copyWith( + isClosed: true, + allowAnswers: true, + ), + currentUser: currentUser, + onAddComment: () {}, ), - currentUser: currentUser, - onAddComment: () {}, ), - )); + ); final addCommentButton = find.ancestor( of: find.text('Add a comment'), - matching: find.byType(PollFooterButton), + matching: find.byType(StreamButton), ); expect(addCommentButton, findsNothing); @@ -135,22 +146,24 @@ void main() async { testWidgets( 'View Comments button is visible and enabled if there are answers', (WidgetTester tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollFooter( - poll: poll.copyWith(answersCount: 1), - currentUser: currentUser, - onViewComments: () {}, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollFooter( + poll: poll.copyWith(answersCount: 1), + currentUser: currentUser, + onViewComments: () {}, + ), ), - )); + ); final viewCommentsButton = find.ancestor( of: find.text('View Comments'), - matching: find.byType(PollFooterButton), + matching: find.byType(StreamButton), ); expect(viewCommentsButton, findsOneWidget); expect( - tester.widget(viewCommentsButton).onPressed, + tester.widget(viewCommentsButton).props.onTap, isNotNull, ); }, @@ -159,19 +172,21 @@ void main() async { testWidgets( 'View Comments button is not visible when there are no answers', (WidgetTester tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollFooter( - poll: poll.copyWith( - answersCount: 0, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollFooter( + poll: poll.copyWith( + answersCount: 0, + ), + currentUser: currentUser, + onViewComments: () {}, ), - currentUser: currentUser, - onViewComments: () {}, ), - )); + ); final viewCommentsButton = find.ancestor( of: find.text('View Comments'), - matching: find.byType(PollFooterButton), + matching: find.byType(StreamButton), ); expect(viewCommentsButton, findsNothing); @@ -181,24 +196,26 @@ void main() async { testWidgets( 'Suggest Option button is visible and enabled when allowed', (WidgetTester tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollFooter( - poll: poll.copyWith( - allowUserSuggestedOptions: true, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollFooter( + poll: poll.copyWith( + allowUserSuggestedOptions: true, + ), + currentUser: currentUser, + onSuggestOption: () {}, ), - currentUser: currentUser, - onSuggestOption: () {}, ), - )); + ); final suggestOptionButton = find.ancestor( of: find.text('Suggest an option'), - matching: find.byType(PollFooterButton), + matching: find.byType(StreamButton), ); expect(suggestOptionButton, findsOneWidget); expect( - tester.widget(suggestOptionButton).onPressed, + tester.widget(suggestOptionButton).props.onTap, isNotNull, ); }, @@ -207,20 +224,22 @@ void main() async { testWidgets( 'Suggest Option button is not visible when poll is closed', (WidgetTester tester) async { - await tester.pumpWidget(_wrapWithMaterialApp( - PollFooter( - poll: poll.copyWith( - isClosed: true, - allowUserSuggestedOptions: true, + await tester.pumpWidget( + _wrapWithMaterialApp( + PollFooter( + poll: poll.copyWith( + isClosed: true, + allowUserSuggestedOptions: true, + ), + currentUser: currentUser, + onSuggestOption: () {}, ), - currentUser: currentUser, - onSuggestOption: () {}, ), - )); + ); final suggestOptionButton = find.ancestor( of: find.text('Suggest an option'), - matching: find.byType(PollFooterButton), + matching: find.byType(StreamButton), ); expect(suggestOptionButton, findsNothing); @@ -242,19 +261,19 @@ void main() async { final viewResultsButton = find.ancestor( of: find.text('View Results'), - matching: find.byType(PollFooterButton), + matching: find.byType(StreamButton), ); expect(viewResultsButton, findsOneWidget); expect( - tester.widget(viewResultsButton).onPressed, + tester.widget(viewResultsButton).props.onTap, isNotNull, ); }, ); testWidgets( - 'View Results button is disabled if there are no votes', + 'View Results button is not visible if there are no votes', (WidgetTester tester) async { await tester.pumpWidget( _wrapWithMaterialApp( @@ -268,63 +287,10 @@ void main() async { final viewResultsButton = find.ancestor( of: find.text('View Results'), - matching: find.byType(PollFooterButton), - ); - - expect(viewResultsButton, findsOneWidget); - expect( - tester.widget(viewResultsButton).onPressed, - isNull, - ); - }, - ); - - testWidgets( - 'See More Options button is visible if there are more options', - (WidgetTester tester) async { - await tester.pumpWidget( - _wrapWithMaterialApp( - PollFooter( - poll: poll, - visibleOptionCount: 2, - currentUser: currentUser, - onSeeMoreOptions: () {}, - ), - ), - ); - - final seeMoreOptionsButton = find.ancestor( - of: find.text('See all ${poll.options.length} options'), - matching: find.byType(PollFooterButton), - ); - - expect(seeMoreOptionsButton, findsOneWidget); - expect( - tester.widget(seeMoreOptionsButton).onPressed, - isNotNull, - ); - }, - ); - - testWidgets( - 'See More Options button is not visible when all options are visible', - (WidgetTester tester) async { - await tester.pumpWidget( - _wrapWithMaterialApp( - PollFooter( - poll: poll, - currentUser: currentUser, - onSeeMoreOptions: () {}, - ), - ), - ); - - final seeMoreOptionsButton = find.ancestor( - of: find.text('See all ${poll.options.length} options'), - matching: find.byType(PollFooterButton), + matching: find.byType(StreamButton), ); - expect(seeMoreOptionsButton, findsNothing); + expect(viewResultsButton, findsNothing); }, ); } diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/poll_header_test.dart b/packages/stream_chat_flutter/test/src/poll/interactor/poll_header_test.dart index 301dd5d5f5..2f2324316b 100644 --- a/packages/stream_chat_flutter/test/src/poll/interactor/poll_header_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/interactor/poll_header_test.dart @@ -21,7 +21,7 @@ void main() { goldenTest( '[${brightness.name}] -> PollHeader looks fine', fileName: 'poll_header_${brightness.name}', - constraints: const BoxConstraints.tightFor(width: 300, height: 100), + constraints: const BoxConstraints.tightFor(width: 300, height: 120), builder: () => _wrapWithMaterialApp( brightness: brightness, PollHeader(poll: poll), @@ -31,7 +31,7 @@ void main() { goldenTest( '[${brightness.name}] -> PollHeader with long question looks fine', fileName: 'poll_header_long_question_${brightness.name}', - constraints: const BoxConstraints.tightFor(width: 300, height: 100), + constraints: const BoxConstraints.tightFor(width: 300, height: 150), builder: () => _wrapWithMaterialApp( brightness: brightness, PollHeader( @@ -45,7 +45,7 @@ void main() { goldenTest( '[${brightness.name}] -> PollHeader subtitle with voting mode disabled looks fine', fileName: 'poll_header_subtitle_voting_mode_disabled_${brightness.name}', - constraints: const BoxConstraints.tightFor(width: 300, height: 100), + constraints: const BoxConstraints.tightFor(width: 300, height: 120), builder: () => _wrapWithMaterialApp( brightness: brightness, PollHeader( @@ -57,7 +57,7 @@ void main() { goldenTest( '[${brightness.name}] -> PollHeader subtitle with voting mode unique looks fine', fileName: 'poll_header_subtitle_voting_mode_unique_${brightness.name}', - constraints: const BoxConstraints.tightFor(width: 300, height: 100), + constraints: const BoxConstraints.tightFor(width: 300, height: 120), builder: () => _wrapWithMaterialApp( brightness: brightness, PollHeader( @@ -69,7 +69,7 @@ void main() { goldenTest( '[${brightness.name}] -> PollHeader subtitle with voting mode limited looks fine', fileName: 'poll_header_subtitle_voting_mode_limited_${brightness.name}', - constraints: const BoxConstraints.tightFor(width: 300, height: 100), + constraints: const BoxConstraints.tightFor(width: 300, height: 120), builder: () => _wrapWithMaterialApp( brightness: brightness, PollHeader( @@ -81,7 +81,7 @@ void main() { goldenTest( '[${brightness.name}] -> PollHeader subtitle with voting mode all looks fine', fileName: 'poll_header_subtitle_voting_mode_all_${brightness.name}', - constraints: const BoxConstraints.tightFor(width: 300, height: 100), + constraints: const BoxConstraints.tightFor(width: 300, height: 120), builder: () => _wrapWithMaterialApp( brightness: brightness, PollHeader( @@ -99,17 +99,19 @@ Widget _wrapWithMaterialApp( return MaterialApp( home: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: Container( - color: theme.colorTheme.disabled, - padding: const EdgeInsets.all(16), - child: Center(child: widget), - ), - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Container( + color: theme.colorTheme.disabled, + padding: const EdgeInsets.all(16), + child: Center(child: widget), + ), + ); + }, + ), ), ); } diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/poll_suggest_option_dialog_test.dart b/packages/stream_chat_flutter/test/src/poll/interactor/poll_suggest_option_dialog_test.dart index fb66e438df..3cb4b0bd11 100644 --- a/packages/stream_chat_flutter/test/src/poll/interactor/poll_suggest_option_dialog_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/interactor/poll_suggest_option_dialog_test.dart @@ -19,8 +19,7 @@ void main() { goldenTest( '[${brightness.name}] -> PollSuggestOptionDialog with initialOption looks fine', - fileName: - 'poll_suggest_option_dialog_with_initial_option_${brightness.name}', + fileName: 'poll_suggest_option_dialog_with_initial_option_${brightness.name}', constraints: const BoxConstraints.tightFor(width: 600, height: 300), builder: () => _wrapWithMaterialApp( brightness: brightness, @@ -39,17 +38,19 @@ Widget _wrapWithMaterialApp( return MaterialApp( home: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: Container( - color: theme.colorTheme.disabled, - padding: const EdgeInsets.all(16), - child: Center(child: widget), - ), - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Container( + color: theme.colorTheme.disabled, + padding: const EdgeInsets.all(16), + child: Center(child: widget), + ), + ); + }, + ), ), ); } diff --git a/packages/stream_chat_flutter/test/src/poll/interactor/stream_poll_interactor_test.dart b/packages/stream_chat_flutter/test/src/poll/interactor/stream_poll_interactor_test.dart index acc34896a3..edbdb33d95 100644 --- a/packages/stream_chat_flutter/test/src/poll/interactor/stream_poll_interactor_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/interactor/stream_poll_interactor_test.dart @@ -114,13 +114,15 @@ Widget _wrapWithMaterialApp( data: StreamChatConfigurationData(), child: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: Center(child: widget), - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Center(child: widget), + ); + }, + ), ), ), ); diff --git a/packages/stream_chat_flutter/test/src/poll/poll_option_reorderable_list_view_test.dart b/packages/stream_chat_flutter/test/src/poll/poll_option_reorderable_list_view_test.dart index 2609417970..6919559d5c 100644 --- a/packages/stream_chat_flutter/test/src/poll/poll_option_reorderable_list_view_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/poll_option_reorderable_list_view_test.dart @@ -68,18 +68,20 @@ Widget _wrapWithMaterialApp( return MaterialApp( 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: widget, + 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: widget, + ), ), - ), - ); - }), + ); + }, + ), ), ); } diff --git a/packages/stream_chat_flutter/test/src/poll/poll_question_text_field_test.dart b/packages/stream_chat_flutter/test/src/poll/poll_question_text_field_test.dart index 24928bbc67..2c203c10ae 100644 --- a/packages/stream_chat_flutter/test/src/poll/poll_question_text_field_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/poll_question_text_field_test.dart @@ -55,18 +55,20 @@ Widget _wrapWithMaterialApp( return MaterialApp( 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: widget, + 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: widget, + ), ), - ), - ); - }), + ); + }, + ), ), ); } diff --git a/packages/stream_chat_flutter/test/src/poll/stream_poll_options_dialog_test.dart b/packages/stream_chat_flutter/test/src/poll/stream_poll_options_dialog_test.dart index 81ebd027ac..0a2de54fec 100644 --- a/packages/stream_chat_flutter/test/src/poll/stream_poll_options_dialog_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/stream_poll_options_dialog_test.dart @@ -94,13 +94,15 @@ Widget _wrapWithMaterialApp( data: StreamChatConfigurationData(), child: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: Center(child: widget), - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Center(child: widget), + ); + }, + ), ), ), ); diff --git a/packages/stream_chat_flutter/test/src/poll/stream_poll_results_dialog_test.dart b/packages/stream_chat_flutter/test/src/poll/stream_poll_results_dialog_test.dart index e5b8df69d0..5488228bbe 100644 --- a/packages/stream_chat_flutter/test/src/poll/stream_poll_results_dialog_test.dart +++ b/packages/stream_chat_flutter/test/src/poll/stream_poll_results_dialog_test.dart @@ -114,13 +114,15 @@ Widget _wrapWithMaterialApp( data: StreamChatConfigurationData(), child: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: Center(child: widget), - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Center(child: widget), + ); + }, + ), ), ), ); diff --git a/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_dark.png b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_dark.png new file mode 100644 index 0000000000..cd992667dc Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_dark.png b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_dark.png new file mode 100644 index 0000000000..bd98b5f377 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_light.png b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_light.png new file mode 100644 index 0000000000..aa307c61a2 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_filtered_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_light.png b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_light.png new file mode 100644 index 0000000000..006010aa45 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/detail/goldens/ci/reaction_detail_sheet_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/detail/reaction_detail_sheet_test.dart b/packages/stream_chat_flutter/test/src/reactions/detail/reaction_detail_sheet_test.dart new file mode 100644 index 0000000000..dde8d56a9f --- /dev/null +++ b/packages/stream_chat_flutter/test/src/reactions/detail/reaction_detail_sheet_test.dart @@ -0,0 +1,448 @@ +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/stream_chat_flutter.dart'; + +import '../../mocks.dart'; + +void main() { + late MockClient mockClient; + + setUpAll(() { + registerFallbackValue(const PaginationParams()); + registerFallbackValue(Filter.equal('type', 'like')); + }); + + setUp(() { + mockClient = MockClient(); + + final mockClientState = MockClientState(); + when(() => mockClient.state).thenReturn(mockClientState); + + final currentUser = OwnUser(id: 'current-user', name: 'Current User'); + when(() => mockClientState.currentUser).thenReturn(currentUser); + }); + + tearDown(() => reset(mockClient)); + + testWidgets('shows total reaction count and all reactions by default', (tester) async { + final reactions = [ + Reaction( + type: 'love', + messageId: 'test-message', + userId: 'user-1', + user: User(id: 'user-1', name: 'User 1'), + createdAt: DateTime.now(), + ), + Reaction( + type: 'like', + messageId: 'test-message', + userId: 'user-2', + user: User(id: 'user-2', name: 'User 2'), + createdAt: DateTime.now(), + ), + ]; + + when( + () => mockClient.queryReactions( + 'test-message', + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer( + (_) async => QueryReactionsResponse() + ..reactions = reactions + ..next = null, + ); + + final message = _buildMessage( + reactionGroups: { + 'love': ReactionGroup(count: 1, sumScores: 1), + 'like': ReactionGroup(count: 1, sumScores: 1), + }, + ); + + await tester.pumpWidget( + _wrapWithMaterialApp( + client: mockClient, + _ReactionDetailSheetLauncher(message: message), + ), + ); + + await tester.tap(find.text('Open Sheet')); + await tester.pumpAndSettle(); + + expect(find.byType(ReactionDetailSheet), findsOneWidget); + expect(find.text('2 Reactions'), findsOneWidget); + expect(find.byType(StreamUserAvatar), findsNWidgets(2)); + expect(find.text('User 1'), findsOneWidget); + expect(find.text('User 2'), findsOneWidget); + }); + + testWidgets('applies initial reaction filter when provided', (tester) async { + final loveReaction = Reaction( + type: 'love', + messageId: 'test-message', + userId: 'user-1', + user: User(id: 'user-1', name: 'User 1'), + createdAt: DateTime.now(), + ); + + // The controller is initialised with filter: type == 'love', so + // queryReactions will return only the love reaction. + when( + () => mockClient.queryReactions( + 'test-message', + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer( + (_) async => QueryReactionsResponse() + ..reactions = [loveReaction] + ..next = null, + ); + + final message = _buildMessage( + reactionGroups: { + 'love': ReactionGroup(count: 1, sumScores: 1), + 'like': ReactionGroup(count: 1, sumScores: 1), + }, + ); + + await tester.pumpWidget( + _wrapWithMaterialApp( + client: mockClient, + _ReactionDetailSheetLauncher( + message: message, + initialReactionType: 'love', + ), + ), + ); + + await tester.tap(find.text('Open Sheet')); + await tester.pumpAndSettle(); + + expect(find.text('1 Reaction'), findsOneWidget); + expect(find.byType(StreamUserAvatar), findsOneWidget); + expect(find.text('User 1'), findsOneWidget); + expect(find.text('User 2'), findsNothing); + }); + + testWidgets('pops with SelectReaction when own reaction row is tapped', (tester) async { + MessageAction? action; + + final reaction = Reaction( + type: 'love', + messageId: 'test-message', + userId: 'current-user', + user: User(id: 'current-user', name: 'Current User'), + createdAt: DateTime.now(), + ); + + when( + () => mockClient.queryReactions( + 'test-message', + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer( + (_) async => QueryReactionsResponse() + ..reactions = [reaction] + ..next = null, + ); + + final message = _buildMessage( + ownReactions: [reaction], + reactionGroups: { + 'love': ReactionGroup(count: 1, sumScores: 1), + }, + ); + + await tester.pumpWidget( + _wrapWithMaterialApp( + client: mockClient, + _ReactionDetailSheetLauncher( + message: message, + onAction: (value) => action = value, + ), + ), + ); + + await tester.tap(find.text('Open Sheet')); + await tester.pumpAndSettle(); + + expect(find.text('Tap to remove'), findsOneWidget); + + await tester.tap(find.text('Current User')); + await tester.pumpAndSettle(); + + expect(action, isA()); + expect((action! as SelectReaction).reaction.type, 'love'); + }); + + testWidgets('does not pop when non-own reaction row is tapped', (tester) async { + MessageAction? action; + + final otherReaction = Reaction( + type: 'love', + messageId: 'test-message', + userId: 'user-1', + user: User(id: 'user-1', name: 'User 1'), + createdAt: DateTime.now(), + ); + + when( + () => mockClient.queryReactions( + 'test-message', + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer( + (_) async => QueryReactionsResponse() + ..reactions = [otherReaction] + ..next = null, + ); + + final message = _buildMessage( + ownReactions: [ + Reaction( + type: 'love', + messageId: 'test-message', + userId: 'current-user', + user: User(id: 'current-user', name: 'Current User'), + createdAt: DateTime.now(), + ), + ], + reactionGroups: { + 'love': ReactionGroup(count: 1, sumScores: 1), + }, + ); + + await tester.pumpWidget( + _wrapWithMaterialApp( + client: mockClient, + _ReactionDetailSheetLauncher( + message: message, + onAction: (value) => action = value, + ), + ), + ); + + await tester.tap(find.text('Open Sheet')); + await tester.pumpAndSettle(); + + expect(find.text('Tap to remove'), findsNothing); + + await tester.tap(find.text('User 1')); + await tester.pumpAndSettle(); + + expect(action, isNull); + expect(find.byType(ReactionDetailSheet), findsOneWidget); + }); + + group('ReactionDetailSheet Golden Tests', () { + final reactions = [ + Reaction( + type: 'love', + messageId: 'test-message', + userId: 'user-1', + user: User(id: 'user-1', name: 'User 1'), + createdAt: DateTime(2026, 1, 1, 10, 0), + ), + Reaction( + type: 'like', + messageId: 'test-message', + userId: 'user-2', + user: User(id: 'user-2', name: 'User 2'), + createdAt: DateTime(2026, 1, 1, 10, 1), + ), + ]; + + final message = _buildMessage( + reactionGroups: { + 'love': ReactionGroup(count: 1, sumScores: 1), + 'like': ReactionGroup(count: 1, sumScores: 1), + }, + ); + + for (final brightness in Brightness.values) { + final theme = brightness.name; + + goldenTest( + 'ReactionDetailSheet in $theme theme', + fileName: 'reaction_detail_sheet_$theme', + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 700), + pumpBeforeTest: (tester) async { + when( + () => mockClient.queryReactions( + 'test-message', + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer( + (_) async => QueryReactionsResponse() + ..reactions = reactions + ..next = null, + ); + // Pump once to trigger post-frame modal opening, then settle animation. + await tester.pump(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + }, + builder: () => _wrapWithMaterialApp( + client: mockClient, + brightness: brightness, + _ReactionDetailSheetGoldenHost(message: message), + ), + ); + + goldenTest( + 'ReactionDetailSheet filtered in $theme theme', + fileName: 'reaction_detail_sheet_filtered_$theme', + constraints: const BoxConstraints(maxWidth: 400, maxHeight: 700), + pumpBeforeTest: (tester) async { + when( + () => mockClient.queryReactions( + 'test-message', + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer( + (_) async => QueryReactionsResponse() + ..reactions = reactions.where((r) => r.type == 'love').toList() + ..next = null, + ); + // Pump once to trigger post-frame modal opening, then settle animation. + await tester.pump(); + await tester.pumpAndSettle(const Duration(seconds: 1)); + }, + builder: () => _wrapWithMaterialApp( + client: mockClient, + brightness: brightness, + _ReactionDetailSheetGoldenHost( + message: message, + initialReactionType: 'love', + ), + ), + ); + } + }); +} + +class _ReactionDetailSheetLauncher extends StatelessWidget { + const _ReactionDetailSheetLauncher({ + required this.message, + this.initialReactionType, + this.onAction, + }); + + final Message message; + final String? initialReactionType; + final ValueChanged? onAction; + + @override + Widget build(BuildContext context) { + return Center( + child: TextButton( + onPressed: () async { + final action = await ReactionDetailSheet.show( + context: context, + message: message, + initialReactionType: initialReactionType, + ); + + onAction?.call(action); + }, + child: const Text('Open Sheet'), + ), + ); + } +} + +class _ReactionDetailSheetGoldenHost extends StatefulWidget { + const _ReactionDetailSheetGoldenHost({ + required this.message, + this.initialReactionType, + }); + + final Message message; + final String? initialReactionType; + + @override + State<_ReactionDetailSheetGoldenHost> createState() => _ReactionDetailSheetGoldenHostState(); +} + +class _ReactionDetailSheetGoldenHostState extends State<_ReactionDetailSheetGoldenHost> { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + ReactionDetailSheet.show( + context: context, + message: widget.message, + initialReactionType: widget.initialReactionType, + ); + }); + } + + @override + Widget build(BuildContext context) { + return const SizedBox.expand(); + } +} + +Message _buildMessage({ + List? latestReactions, + List? ownReactions, + Map? reactionGroups, +}) { + return Message( + id: 'test-message', + text: 'This is a test message', + createdAt: DateTime.now(), + user: User(id: 'test-user', name: 'Test User'), + latestReactions: latestReactions, + ownReactions: ownReactions, + reactionGroups: reactionGroups, + ); +} + +Widget _wrapWithMaterialApp( + Widget child, { + required StreamChatClient client, + Brightness? brightness, +}) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData(brightness: brightness), + builder: (context, child) => StreamChat( + client: client, + // Mock the connectivity stream to always return wifi. + connectivityStream: Stream.value([ConnectivityResult.wifi]), + streamChatThemeData: StreamChatThemeData(brightness: brightness), + child: child ?? const SizedBox.shrink(), + ), + home: 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/reactions/picker/goldens/ci/reaction_icon_button_selected_dark.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_icon_button_selected_dark.png new file mode 100644 index 0000000000..b23c628a3b Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_icon_button_selected_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_icon_button_selected_light.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_icon_button_selected_light.png new file mode 100644 index 0000000000..c47907a1c0 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_icon_button_selected_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_icon_button_unselected_dark.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_icon_button_unselected_dark.png new file mode 100644 index 0000000000..e7cac1e1a0 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_icon_button_unselected_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_icon_button_unselected_light.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_icon_button_unselected_light.png new file mode 100644 index 0000000000..a8d37e94bb Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_icon_button_unselected_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_picker_icon_list_dark.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_picker_icon_list_dark.png new file mode 100644 index 0000000000..3ea44c3993 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_picker_icon_list_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_picker_icon_list_light.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_picker_icon_list_light.png new file mode 100644 index 0000000000..dea0b18230 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_picker_icon_list_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_picker_icon_list_selected_dark.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_picker_icon_list_selected_dark.png new file mode 100644 index 0000000000..3de2c605ae Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_picker_icon_list_selected_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_picker_icon_list_selected_light.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_picker_icon_list_selected_light.png new file mode 100644 index 0000000000..6a1b705330 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/reaction_picker_icon_list_selected_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_dark.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_dark.png new file mode 100644 index 0000000000..d87a0d4f7a Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_light.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_light.png new file mode 100644 index 0000000000..1fd79c0542 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_selected_dark.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_selected_dark.png new file mode 100644 index 0000000000..c1207edd59 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_selected_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_selected_light.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_selected_light.png new file mode 100644 index 0000000000..0a6f550a21 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_selected_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_subset_dark.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_subset_dark.png new file mode 100644 index 0000000000..11072894a5 Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_subset_dark.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_subset_light.png b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_subset_light.png new file mode 100644 index 0000000000..0af154179a Binary files /dev/null and b/packages/stream_chat_flutter/test/src/reactions/picker/goldens/ci/stream_reaction_picker_subset_light.png differ diff --git a/packages/stream_chat_flutter/test/src/reactions/picker/reaction_picker_test.dart b/packages/stream_chat_flutter/test/src/reactions/picker/reaction_picker_test.dart new file mode 100644 index 0000000000..9d60675718 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/reactions/picker/reaction_picker_test.dart @@ -0,0 +1,482 @@ +import 'package:alchemist/alchemist.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/src/misc/staggered_scale_transition.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + const resolver = _TestReactionIconResolver(); + + testWidgets( + 'renders with correct message and reaction buttons', + (WidgetTester tester) async { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageReactionPicker( + message: message, + onReactionPicked: (_) {}, + ), + reactionIconResolver: resolver, + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Verify the widget renders with correct structure. + expect(find.byType(StreamMessageReactionPicker), findsOneWidget); + // Verify the correct number of reaction buttons. + expect( + find.byType(StreamEmojiButton), + findsNWidgets(resolver.defaultReactions.length), + ); + expect(find.byKey(const Key('add_reaction')), findsOneWidget); + }, + ); + + 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( + StreamMessageReactionPicker( + message: message, + onReactionPicked: (reaction) { + pickedReaction = reaction; + }, + ), + reactionIconResolver: resolver, + ), + ); + + // Wait for animations to complete. + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Tap the first reaction button. + await tester.tap(find.byKey(const Key('love'))); + await tester.pump(); + + // Verify the callback was called with the correct reaction. + expect(pickedReaction, isNotNull); + expect(pickedReaction!.type, 'love'); + expect(pickedReaction!.emojiCode, resolver.emojiCode('love')); + }, + ); + + testWidgets( + 'reuses own reaction when selected', + (WidgetTester tester) async { + final existingReaction = Reaction( + type: 'love', + messageId: 'test-message', + userId: 'test-user', + ); + + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ownReactions: [existingReaction], + ); + + Reaction? pickedReaction; + + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageReactionPicker( + message: message, + onReactionPicked: (reaction) { + pickedReaction = reaction; + }, + ), + reactionIconResolver: resolver, + ), + ); + + // Wait for animations to complete. + await tester.pumpAndSettle(const Duration(seconds: 1)); + + await tester.tap(find.byKey(const Key('love'))); + await tester.pump(); + + expect(pickedReaction, same(existingReaction)); + }, + ); + + testWidgets( + 'marks own reactions as selected', + (WidgetTester tester) async { + 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( + StreamMessageReactionPicker( + message: message, + onReactionPicked: (_) {}, + ), + reactionIconResolver: resolver, + ), + ); + + // Wait for animations to complete. + await tester.pumpAndSettle(const Duration(seconds: 1)); + + final selectedButton = tester.widget( + find.byKey(const Key('love')), + ); + final unselectedButton = tester.widget( + find.byKey(const Key('like')), + ); + + expect(selectedButton.props.isSelected, isTrue); + expect(unselectedButton.props.isSelected, isFalse); + }, + ); + + testWidgets( + 'updates reaction buttons when resolver default reactions change', + (WidgetTester tester) async { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + const compactResolver = _CustomReactionIconResolver({'love', 'like'}); + + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageReactionPicker( + message: message, + onReactionPicked: (_) {}, + ), + reactionIconResolver: compactResolver, + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + // Initial resolver exposes two quick reactions. + expect(find.byType(StreamEmojiButton), findsNWidgets(2)); + + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageReactionPicker( + message: message, + onReactionPicked: (_) {}, + ), + reactionIconResolver: resolver, + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + // Updated resolver exposes the default quick-reaction set. + expect( + find.byType(StreamEmojiButton), + findsNWidgets(resolver.defaultReactions.length), + ); + }, + ); + + testWidgets( + 'uses only defaultReactions even when supportedReactions contains more types', + (WidgetTester tester) async { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + const subsetResolver = _SubsetDefaultReactionIconResolver(); + + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageReactionPicker( + message: message, + onReactionPicked: (_) {}, + ), + reactionIconResolver: subsetResolver, + ), + ); + + // Wait for animations to complete. + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Picker should render only the resolver's quick-reaction defaults. + expect(find.byType(StreamEmojiButton), findsNWidgets(1)); + expect(find.byKey(const Key('love')), findsOneWidget); + expect(find.byKey(const Key('like')), findsNothing); + expect(find.byKey(const Key('wow')), findsNothing); + }, + ); + + testWidgets( + 'uses custom reaction resolver rendering', + (WidgetTester tester) async { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageReactionPicker( + message: message, + onReactionPicked: (_) {}, + ), + reactionIconResolver: const _TypeBasedReactionIconResolver(), + ), + ); + + // Wait for animations to complete. + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // _TypeBasedReactionIconResolver defines one reaction ('customParty') + // that resolves to StreamUnicodeEmoji('❓'). Verify the fallback emoji + // is rendered via a StreamEmoji widget inside the picker. + expect(find.byType(StreamEmoji), findsOneWidget); + expect(find.text('❓'), findsOneWidget); + }, + ); + + testWidgets( + 'renders picker without staggered transition animation', + (WidgetTester tester) async { + final message = Message( + id: 'test-message', + text: 'Hello world', + user: User(id: 'test-user'), + ); + + await tester.pumpWidget( + _wrapWithMaterialApp( + StreamMessageReactionPicker( + message: message, + onReactionPicked: (_) {}, + ), + reactionIconResolver: resolver, + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + + expect(find.byType(StaggeredScaleTransition), findsNothing); + }, + ); + + group('Golden tests', () { + for (final brightness in [Brightness.light, Brightness.dark]) { + final theme = brightness.name; + + goldenTest( + 'StreamMessageReactionPicker 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, + StreamMessageReactionPicker( + message: message, + onReactionPicked: (_) {}, + ), + reactionIconResolver: resolver, + ); + }, + ); + + goldenTest( + 'StreamMessageReactionPicker 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, + StreamMessageReactionPicker( + message: message, + onReactionPicked: (_) {}, + ), + reactionIconResolver: resolver, + ); + }, + ); + + goldenTest( + 'StreamMessageReactionPicker with subset defaults in $theme theme', + fileName: 'stream_reaction_picker_subset_$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, + StreamMessageReactionPicker( + message: message, + onReactionPicked: (_) {}, + ), + reactionIconResolver: const _SubsetDefaultReactionIconResolver(), + ); + }, + ); + } + }); +} + +Widget _wrapWithMaterialApp( + Widget child, { + Brightness? brightness, + ReactionIconResolver? reactionIconResolver, +}) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData(brightness: brightness), + builder: (context, child) => StreamChatConfiguration( + data: StreamChatConfigurationData( + reactionIconResolver: reactionIconResolver ?? const _TestReactionIconResolver(), + ), + child: StreamChatTheme( + data: StreamChatThemeData(brightness: brightness), + child: child ?? const SizedBox.shrink(), + ), + ), + home: 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, + ), + ), + ); + }, + ), + ); +} + +class _TestReactionIconResolver extends ReactionIconResolver { + const _TestReactionIconResolver(); + + static const _reactionTypes = {'like', 'haha', 'love', 'wow', 'sad'}; + + @override + Set get defaultReactions => _reactionTypes; + + @override + Set get supportedReactions => _reactionTypes; + + @override + String? emojiCode(String type) => streamSupportedEmojis[type]?.emoji; + + @override + StreamEmojiContent resolve(String type) { + if (emojiCode(type) case final emoji?) return StreamUnicodeEmoji(emoji); + return const StreamUnicodeEmoji('❓'); + } +} + +class _CustomReactionIconResolver extends ReactionIconResolver { + const _CustomReactionIconResolver(this._types); + + final Set _types; + + @override + Set get defaultReactions => _types; + + @override + Set get supportedReactions => _types; + + @override + String? emojiCode(String type) => streamSupportedEmojis[type]?.emoji; + + @override + StreamEmojiContent resolve(String type) { + if (emojiCode(type) case final emoji?) return StreamUnicodeEmoji(emoji); + return const StreamUnicodeEmoji('❓'); + } +} + +class _TypeBasedReactionIconResolver extends ReactionIconResolver { + const _TypeBasedReactionIconResolver(); + + @override + Set get defaultReactions => const {'customParty'}; + + @override + Set get supportedReactions => const {'customParty'}; + + @override + String? emojiCode(String type) => null; + + @override + StreamEmojiContent resolve(String type) { + return const StreamUnicodeEmoji('❓'); + } +} + +class _SubsetDefaultReactionIconResolver extends ReactionIconResolver { + const _SubsetDefaultReactionIconResolver(); + + @override + Set get defaultReactions => const {'love'}; + + @override + Set get supportedReactions => const {'love', 'like', 'wow'}; + + @override + String? emojiCode(String type) => streamSupportedEmojis[type]?.emoji; + + @override + StreamEmojiContent resolve(String type) { + if (emojiCode(type) case final emoji?) return StreamUnicodeEmoji(emoji); + return const StreamUnicodeEmoji('❓'); + } +} 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 98ed897a61..0be90f2024 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 a4388618b2..af3a1fd7f7 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/member_scroll_view/stream_member_list_view_test.dart b/packages/stream_chat_flutter/test/src/scroll_view/member_scroll_view/stream_member_list_view_test.dart index b5275a9790..13fb4f59a3 100644 --- a/packages/stream_chat_flutter/test/src/scroll_view/member_scroll_view/stream_member_list_view_test.dart +++ b/packages/stream_chat_flutter/test/src/scroll_view/member_scroll_view/stream_member_list_view_test.dart @@ -16,17 +16,13 @@ void main() { clientState = MockClientState(); when(() => client.state).thenAnswer((_) => clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'testid')); - when(() => clientState.currentUserStream) - .thenAnswer((_) => Stream.value(OwnUser(id: 'testid'))); + when(() => clientState.currentUserStream).thenAnswer((_) => Stream.value(OwnUser(id: 'testid'))); channel = MockChannel(); - when(() => channel.on(any(), any(), any(), any())) - .thenAnswer((_) => const Stream.empty()); channelClientState = MockChannelState(); when(() => channel.client).thenReturn(client); when(() => channel.state).thenReturn(channelClientState); - when(() => channelClientState.membersStream) - .thenAnswer((_) => const Stream.empty()); + when(() => channelClientState.membersStream).thenAnswer((_) => const Stream.empty()); when(() => channelClientState.members).thenReturn([]); }); 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 dfab79b8fb..b48d6c2d6c 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 9bcf92fd9f..5af950b1f9 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_dark.png b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_dark.png index bc3b26348a..0615b4018d 100644 Binary files a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_dark.png and b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/goldens/ci/stream_unread_threads_banner_dark.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 0dccaf0600..b4d2139909 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/scroll_view/thread_scroll_view/stream_thread_list_view_test.dart b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_thread_list_view_test.dart new file mode 100644 index 0000000000..d387ecbc90 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_thread_list_view_test.dart @@ -0,0 +1,109 @@ +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'; + +class MockStreamThreadListController extends Mock implements StreamThreadListController { + @override + PagedValue value = const PagedValue.loading(); +} + +void main() { + late StreamChatClient client; + late ClientState clientState; + late MockStreamThreadListController controller; + late Thread thread; + + setUp(() { + client = MockClient(); + clientState = MockClientState(); + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'current-user-id')); + + thread = Thread( + channelCid: 'messaging:general', + parentMessageId: 'parent-message-id', + parentMessage: Message( + id: 'parent-message-id', + text: 'Parent message from thread list', + user: User(id: 'other-user-id'), + createdAt: DateTime.now().toUtc(), + ), + createdByUserId: 'other-user-id', + replyCount: 2, + participantCount: 1, + ); + + controller = MockStreamThreadListController(); + when(controller.doInitialLoad).thenAnswer((_) async { + controller.value = PagedValue(items: [thread]); + }); + }); + + tearDown(() { + controller.dispose(); + }); + + testWidgets('renders parent message row with thread indicator', (tester) async { + await tester.pumpWidget( + _wrapWithMaterialApp( + client: client, + child: StreamThreadListView(controller: controller), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Parent message from thread list'), findsOneWidget); + expect(find.text('2 replies'), findsOneWidget); + }); + + testWidgets('honors per-instance messageBuilder override', (tester) async { + await tester.pumpWidget( + _wrapWithMaterialApp( + client: client, + child: StreamThreadListView( + controller: controller, + itemBuilder: (_, __, ___, ____) => const Text('custom-thread-row'), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('custom-thread-row'), findsOneWidget); + }); + + testWidgets('honors global threadListItem component builder override', (tester) async { + await tester.pumpWidget( + _wrapWithMaterialApp( + client: client, + componentBuilders: StreamComponentBuilders( + extensions: streamChatComponentBuilders( + threadListItem: (_, __) => const Text('global-thread-item'), + ), + ), + child: StreamThreadListView(controller: controller), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('global-thread-item'), findsOneWidget); + }); +} + +Widget _wrapWithMaterialApp({ + required StreamChatClient client, + required Widget child, + StreamComponentBuilders? componentBuilders, +}) { + return MaterialApp( + home: StreamChat( + client: client, + componentBuilders: componentBuilders, + streamChatConfigData: StreamChatConfigurationData(), + connectivityStream: Stream.value([ConnectivityResult.wifi]), + child: Scaffold(body: child), + ), + ); +} diff --git a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_unread_threads_banner_test.dart b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_unread_threads_banner_test.dart index 897d140aed..4c1a8e7a83 100644 --- a/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_unread_threads_banner_test.dart +++ b/packages/stream_chat_flutter/test/src/scroll_view/thread_scroll_view/stream_unread_threads_banner_test.dart @@ -23,13 +23,15 @@ Widget _wrapWithMaterialApp( return MaterialApp( home: StreamChatTheme( data: StreamChatThemeData(brightness: brightness), - child: Builder(builder: (context) { - final theme = StreamChatTheme.of(context); - return Scaffold( - backgroundColor: theme.colorTheme.appBg, - body: Center(child: widget), - ); - }), + child: Builder( + builder: (context) { + final theme = StreamChatTheme.of(context); + return Scaffold( + backgroundColor: theme.colorTheme.appBg, + body: Center(child: widget), + ); + }, + ), ), ); } diff --git a/packages/stream_chat_flutter/test/src/stream_chat_configuration_test.dart b/packages/stream_chat_flutter/test/src/stream_chat_configuration_test.dart index d9db650ab2..dcd94aba13 100644 --- a/packages/stream_chat_flutter/test/src/stream_chat_configuration_test.dart +++ b/packages/stream_chat_flutter/test/src/stream_chat_configuration_test.dart @@ -9,15 +9,17 @@ void main() { (t) async { final configuration = StreamChatConfigurationData(); late final StreamChatConfigurationData configurationFromProvider; - await t.pumpWidget(StreamChatConfiguration( - data: configuration, - child: Builder( - builder: (context) { - configurationFromProvider = StreamChatConfiguration.of(context); - return const SizedBox(); - }, + await t.pumpWidget( + StreamChatConfiguration( + data: configuration, + child: Builder( + builder: (context) { + configurationFromProvider = StreamChatConfiguration.of(context); + return const SizedBox(); + }, + ), ), - )); + ); expect(configuration, configurationFromProvider); }, @@ -30,15 +32,17 @@ void main() { enforceUniqueReactions: false, ); late final StreamChatConfigurationData configurationFromProvider; - await t.pumpWidget(StreamChatConfiguration( - data: configuration, - child: Builder( - builder: (context) { - configurationFromProvider = StreamChatConfiguration.of(context); - return const SizedBox(); - }, + await t.pumpWidget( + StreamChatConfiguration( + data: configuration, + child: Builder( + builder: (context) { + configurationFromProvider = StreamChatConfiguration.of(context); + return const SizedBox(); + }, + ), ), - )); + ); expect(configuration, configurationFromProvider); }, diff --git a/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart index 44f6f85113..bda9511ed9 100644 --- a/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart @@ -4,18 +4,16 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('AvatarThemeData copyWith, ==, hashCode basics', () { - expect(const StreamAvatarThemeData(), - const StreamAvatarThemeData().copyWith()); - expect(const StreamAvatarThemeData().hashCode, - const StreamAvatarThemeData().copyWith().hashCode); + expect(const StreamAvatarThemeData(), const StreamAvatarThemeData().copyWith()); + expect(const StreamAvatarThemeData().hashCode, const StreamAvatarThemeData().copyWith().hashCode); }); group('AvatarThemeData lerps correctly', () { test('Lerp completely', () { expect( - const StreamAvatarThemeData() - .lerp(_avatarThemeDataControl1, _avatarThemeDataControl2, 1), - _avatarThemeDataControl2); + const StreamAvatarThemeData().lerp(_avatarThemeDataControl1, _avatarThemeDataControl2, 1), + _avatarThemeDataControl2, + ); }); test('Lerp halfway', () { @@ -34,8 +32,7 @@ void main() { }); test('Merging two AvatarThemeData results in the latter', () { - expect(_avatarThemeDataControl1.merge(_avatarThemeDataControl2), - _avatarThemeDataControl2); + expect(_avatarThemeDataControl1.merge(_avatarThemeDataControl2), _avatarThemeDataControl2); }); } 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..65cd2b5a0f 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 @@ -4,25 +4,19 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('ChannelHeaderThemeData copyWith, ==, hashCode basics', () { - expect(const StreamChannelHeaderThemeData(), - const StreamChannelHeaderThemeData().copyWith()); - expect(const StreamChannelHeaderThemeData().hashCode, - const StreamChannelHeaderThemeData().copyWith().hashCode); + expect(const StreamChannelHeaderThemeData(), const StreamChannelHeaderThemeData().copyWith()); + expect(const StreamChannelHeaderThemeData().hashCode, const StreamChannelHeaderThemeData().copyWith().hashCode); }); group('ChannelHeaderThemeData lerps', () { - test( - '''Light ChannelHeaderThemeData lerps completely to dark ChannelHeaderThemeData''', - () { + test('''Light ChannelHeaderThemeData lerps completely to dark ChannelHeaderThemeData''', () { expect( - const StreamChannelHeaderThemeData() - .lerp(_channelThemeControl, _channelThemeControlDark, 1), - _channelThemeControlDark); + const StreamChannelHeaderThemeData().lerp(_channelThemeControl, _channelThemeControlDark, 1), + _channelThemeControlDark, + ); }); - test( - '''Light ChannelHeaderThemeData lerps halfway to dark ChannelHeaderThemeData''', - () { + test('''Light ChannelHeaderThemeData lerps halfway to dark ChannelHeaderThemeData''', () { expect( const StreamChannelHeaderThemeData().lerp( _channelThemeControl, @@ -36,19 +30,16 @@ void main() { ); }); - test( - '''Dark ChannelHeaderThemeData lerps completely to light ChannelHeaderThemeData''', - () { + test('''Dark ChannelHeaderThemeData lerps completely to light ChannelHeaderThemeData''', () { expect( - const StreamChannelHeaderThemeData() - .lerp(_channelThemeControlDark, _channelThemeControl, 1), - _channelThemeControl); + const StreamChannelHeaderThemeData().lerp(_channelThemeControlDark, _channelThemeControl, 1), + _channelThemeControl, + ); }); }); test('Merging dark and light themes results in a dark theme', () { - expect(_channelThemeControl.merge(_channelThemeControlDark), - _channelThemeControlDark); + expect(_channelThemeControl.merge(_channelThemeControlDark), _channelThemeControlDark); }); } @@ -61,12 +52,12 @@ final _channelThemeControl = StreamChannelHeaderThemeData( ), ), color: const Color(0xff101418), - titleStyle: StreamTextTheme.light().headlineBold.copyWith( - color: const Color(0xffffffff), - ), - subtitleStyle: StreamTextTheme.light().footnote.copyWith( - color: const Color(0xff7a7a7a), - ), + titleStyle: const StreamTextTheme.light().headlineBold.copyWith( + color: const Color(0xffffffff), + ), + subtitleStyle: const StreamTextTheme.light().footnote.copyWith( + color: const Color(0xff7a7a7a), + ), ); final _channelThemeControlMidLerp = StreamChannelHeaderThemeData( @@ -83,9 +74,9 @@ final _channelThemeControlMidLerp = StreamChannelHeaderThemeData( fontWeight: FontWeight.w500, fontSize: 16, ), - subtitleStyle: StreamTextTheme.light().footnote.copyWith( - color: const Color(0xff7a7a7a), - ), + subtitleStyle: const StreamTextTheme.light().footnote.copyWith( + color: const Color(0xff7a7a7a), + ), ); final _channelThemeControlDark = StreamChannelHeaderThemeData( @@ -96,9 +87,9 @@ final _channelThemeControlDark = StreamChannelHeaderThemeData( width: 40, ), ), - color: StreamColorTheme.dark().barsBg, - titleStyle: StreamTextTheme.dark().headlineBold, - subtitleStyle: StreamTextTheme.dark().footnote.copyWith( - color: const Color(0xff7A7A7A), - ), + 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..883fb441be 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 @@ -4,27 +4,26 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('ChannelListHeaderThemeData copyWith, ==, hashCode basics', () { - expect(const StreamChannelListHeaderThemeData(), - const StreamChannelListHeaderThemeData().copyWith()); - expect(const StreamChannelListHeaderThemeData().hashCode, - const StreamChannelListHeaderThemeData().copyWith().hashCode); + expect(const StreamChannelListHeaderThemeData(), const StreamChannelListHeaderThemeData().copyWith()); + expect( + const StreamChannelListHeaderThemeData().hashCode, + const StreamChannelListHeaderThemeData().copyWith().hashCode, + ); }); group('ChannelListHeaderThemeData lerps', () { - test( - '''Light ChannelListHeaderThemeData lerps completely to dark ChannelListHeaderThemeData''', - () { + test('''Light ChannelListHeaderThemeData lerps completely to dark ChannelListHeaderThemeData''', () { expect( - const StreamChannelListHeaderThemeData().lerp( - _channelListHeaderThemeControl, - _channelListHeaderThemeControlDark, - 1), - _channelListHeaderThemeControlDark); + const StreamChannelListHeaderThemeData().lerp( + _channelListHeaderThemeControl, + _channelListHeaderThemeControlDark, + 1, + ), + _channelListHeaderThemeControlDark, + ); }); - test( - '''Light ChannelListHeaderThemeData lerps halfway to dark ChannelListHeaderThemeData''', - () { + test('''Light ChannelListHeaderThemeData lerps halfway to dark ChannelListHeaderThemeData''', () { expect( const StreamChannelListHeaderThemeData().lerp( _channelListHeaderThemeControl, @@ -38,23 +37,23 @@ void main() { ); }); - test( - '''Dark ChannelListHeaderThemeData lerps completely to light ChannelListHeaderThemeData''', - () { + test('''Dark ChannelListHeaderThemeData lerps completely to light ChannelListHeaderThemeData''', () { expect( - const StreamChannelListHeaderThemeData().lerp( - _channelListHeaderThemeControlDark, - _channelListHeaderThemeControl, - 1), - _channelListHeaderThemeControl); + const StreamChannelListHeaderThemeData().lerp( + _channelListHeaderThemeControlDark, + _channelListHeaderThemeControl, + 1, + ), + _channelListHeaderThemeControl, + ); }); }); test('Merging dark and light themes results in a dark theme', () { expect( - _channelListHeaderThemeControl - .merge(_channelListHeaderThemeControlDark), - _channelListHeaderThemeControlDark); + _channelListHeaderThemeControl.merge(_channelListHeaderThemeControlDark), + _channelListHeaderThemeControlDark, + ); }); } @@ -66,8 +65,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 +93,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 4f04d916d2..b0fd386b2e 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 @@ -6,25 +6,19 @@ String _dummyFormatter(BuildContext context, DateTime date) => 'formatted'; void main() { test('ChannelPreviewThemeData copyWith, ==, hashCode basics', () { - expect(const StreamChannelPreviewThemeData(), - const StreamChannelPreviewThemeData().copyWith()); - expect(const StreamChannelPreviewThemeData().hashCode, - const StreamChannelPreviewThemeData().copyWith().hashCode); + expect(const StreamChannelPreviewThemeData(), const StreamChannelPreviewThemeData().copyWith()); + expect(const StreamChannelPreviewThemeData().hashCode, const StreamChannelPreviewThemeData().copyWith().hashCode); }); group('ChannelPreviewThemeData lerps', () { - test( - '''Light ChannelPreviewThemeData lerps completely to dark ChannelPreviewThemeData''', - () { + test('''Light ChannelPreviewThemeData lerps completely to dark ChannelPreviewThemeData''', () { expect( - const StreamChannelPreviewThemeData().lerp( - _channelPreviewThemeControl, _channelPreviewThemeControlDark, 1), - _channelPreviewThemeControlDark); + const StreamChannelPreviewThemeData().lerp(_channelPreviewThemeControl, _channelPreviewThemeControlDark, 1), + _channelPreviewThemeControlDark, + ); }); - test( - '''Light ChannelPreviewThemeData lerps halfway to dark ChannelPreviewThemeData''', - () { + test('''Light ChannelPreviewThemeData lerps halfway to dark ChannelPreviewThemeData''', () { expect( const StreamChannelPreviewThemeData().lerp( _channelPreviewThemeControl, @@ -38,24 +32,21 @@ void main() { ); }); - test( - '''Dark ChannelPreviewThemeData lerps completely to light ChannelPreviewThemeData''', - () { + test('''Dark ChannelPreviewThemeData lerps completely to light ChannelPreviewThemeData''', () { expect( - const StreamChannelPreviewThemeData().lerp( - _channelPreviewThemeControlDark, _channelPreviewThemeControl, 1), - _channelPreviewThemeControl); + const StreamChannelPreviewThemeData().lerp(_channelPreviewThemeControlDark, _channelPreviewThemeControl, 1), + _channelPreviewThemeControl, + ); }); }); test('Merging dark and light themes results in a dark theme', () { - expect(_channelPreviewThemeControl.merge(_channelPreviewThemeControlDark), - _channelPreviewThemeControlDark); + expect(_channelPreviewThemeControl.merge(_channelPreviewThemeControlDark), _channelPreviewThemeControlDark); }); } final _channelPreviewThemeControl = StreamChannelPreviewThemeData( - unreadCounterColor: StreamColorTheme.light().accentError, + unreadCounterColor: const StreamColorTheme.light().accentError, avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -63,14 +54,14 @@ final _channelPreviewThemeControl = StreamChannelPreviewThemeData( width: 40, ), ), - titleStyle: StreamTextTheme.light().bodyBold, - subtitleStyle: StreamTextTheme.light().footnote.copyWith( - color: const Color(0xff7A7A7A), - ), - lastMessageAtStyle: StreamTextTheme.light().footnote.copyWith( - // ignore: deprecated_member_use - color: StreamColorTheme.light().textHighEmphasis.withOpacity(0.5), - ), + titleStyle: const StreamTextTheme.light().bodyBold, + subtitleStyle: const StreamTextTheme.light().footnote.copyWith( + color: const Color(0xff7A7A7A), + ), + lastMessageAtStyle: const StreamTextTheme.light().footnote.copyWith( + // ignore: deprecated_member_use + color: const StreamColorTheme.light().textHighEmphasis.withOpacity(0.5), + ), lastMessageAtFormatter: _dummyFormatter, indicatorIconSize: 16, ); @@ -94,16 +85,16 @@ final _channelPreviewThemeControlMidLerp = StreamChannelPreviewThemeData( fontSize: 12, fontWeight: FontWeight.w400, ), - lastMessageAtStyle: StreamTextTheme.light().footnote.copyWith( - // ignore: deprecated_member_use - color: const Color(0x807f7f7f).withOpacity(0.5), - ), + lastMessageAtStyle: const StreamTextTheme.light().footnote.copyWith( + // ignore: deprecated_member_use + color: const Color(0x807f7f7f).withOpacity(0.5), + ), lastMessageAtFormatter: _dummyFormatter, indicatorIconSize: 16, ); final _channelPreviewThemeControlDark = StreamChannelPreviewThemeData( - unreadCounterColor: StreamColorTheme.dark().accentError, + unreadCounterColor: const StreamColorTheme.dark().accentError, avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -111,14 +102,14 @@ final _channelPreviewThemeControlDark = StreamChannelPreviewThemeData( width: 40, ), ), - titleStyle: StreamTextTheme.dark().bodyBold, - subtitleStyle: StreamTextTheme.dark().footnote.copyWith( - color: const Color(0xff7A7A7A), - ), - lastMessageAtStyle: StreamTextTheme.dark().footnote.copyWith( - // ignore: deprecated_member_use - color: StreamColorTheme.dark().textHighEmphasis.withOpacity(0.5), - ), + titleStyle: const StreamTextTheme.dark().bodyBold, + subtitleStyle: const StreamTextTheme.dark().footnote.copyWith( + color: const Color(0xff7A7A7A), + ), + lastMessageAtStyle: const StreamTextTheme.dark().footnote.copyWith( + // ignore: deprecated_member_use + color: const StreamColorTheme.dark().textHighEmphasis.withOpacity(0.5), + ), lastMessageAtFormatter: _dummyFormatter, indicatorIconSize: 16, ); diff --git a/packages/stream_chat_flutter/test/src/theme/draft_list_tile_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/draft_list_tile_theme_test.dart index e7f91b0fba..2f9e4c939b 100644 --- a/packages/stream_chat_flutter/test/src/theme/draft_list_tile_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/draft_list_tile_theme_test.dart @@ -5,8 +5,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; String _dummyFormatter(BuildContext context, DateTime date) => 'formatted'; void main() { - testWidgets('StreamDraftListTileTheme merges with ancestor theme', - (tester) async { + testWidgets('StreamDraftListTileTheme merges with ancestor theme', (tester) async { const backgroundColor = Colors.blue; const childBackgroundColor = Colors.red; @@ -164,14 +163,14 @@ void main() { // t = 0.5 should return something in between final lerpedAt05 = data1.lerp(data1, data2, 0.5); - expect(lerpedAt05.backgroundColor, - Color.lerp(Colors.black, Colors.white, 0.5)); + expect(lerpedAt05.backgroundColor, Color.lerp(Colors.black, Colors.white, 0.5)); expect( - lerpedAt05.padding, - EdgeInsetsGeometry.lerp( - const EdgeInsets.all(8), - const EdgeInsets.all(16), - 0.5, - )); + lerpedAt05.padding, + EdgeInsetsGeometry.lerp( + const EdgeInsets.all(8), + const EdgeInsets.all(16), + 0.5, + ), + ); }); } 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..fb0c5699a0 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 @@ -7,26 +7,18 @@ class MockStreamChatClient extends Mock implements StreamChatClient {} void main() { test('GalleryFooterThemeData copyWith, ==, hashCode basics', () { - expect(const StreamGalleryFooterThemeData(), - const StreamGalleryFooterThemeData().copyWith()); - expect(const StreamGalleryFooterThemeData().hashCode, - const StreamGalleryFooterThemeData().copyWith().hashCode); + expect(const StreamGalleryFooterThemeData(), const StreamGalleryFooterThemeData().copyWith()); + expect(const StreamGalleryFooterThemeData().hashCode, const StreamGalleryFooterThemeData().copyWith().hashCode); }); - test( - '''Light GalleryFooterThemeData lerps completely to dark GalleryFooterThemeData''', - () { + test('''Light GalleryFooterThemeData lerps completely to dark GalleryFooterThemeData''', () { expect( - const StreamGalleryFooterThemeData().lerp( - _galleryFooterThemeDataControl, - _galleryFooterThemeDataControlDark, - 1), - _galleryFooterThemeDataControlDark); + const StreamGalleryFooterThemeData().lerp(_galleryFooterThemeDataControl, _galleryFooterThemeDataControlDark, 1), + _galleryFooterThemeDataControlDark, + ); }); - test( - '''Light GalleryFooterThemeData lerps halfway to dark GalleryFooterThemeData''', - () { + test('''Light GalleryFooterThemeData lerps halfway to dark GalleryFooterThemeData''', () { expect( const StreamGalleryFooterThemeData().lerp( _galleryFooterThemeDataControl, @@ -40,34 +32,25 @@ void main() { ); }); - test( - '''Dark GalleryFooterThemeData lerps completely to light GalleryFooterThemeData''', - () { + test('''Dark GalleryFooterThemeData lerps completely to light GalleryFooterThemeData''', () { expect( - const StreamGalleryFooterThemeData().lerp( - _galleryFooterThemeDataControlDark, - _galleryFooterThemeDataControl, - 1), - _galleryFooterThemeDataControl); + const StreamGalleryFooterThemeData().lerp(_galleryFooterThemeDataControlDark, _galleryFooterThemeDataControl, 1), + _galleryFooterThemeDataControl, + ); }); test('Merging dark and light themes results in a dark theme', () { expect( - _galleryFooterThemeDataControl - .merge(_galleryFooterThemeDataControlDark), - _galleryFooterThemeDataControlDark); + _galleryFooterThemeDataControl.merge(_galleryFooterThemeDataControlDark), + _galleryFooterThemeDataControlDark, + ); }); test('Merging dark and light themes results in a dark theme', () { - expect( - _galleryFooterThemeDataControlDark - .merge(_galleryFooterThemeDataControl), - _galleryFooterThemeDataControl); + expect(_galleryFooterThemeDataControlDark.merge(_galleryFooterThemeDataControl), _galleryFooterThemeDataControl); }); - testWidgets( - 'Passing no GalleryFooterThemeData returns default light theme values', - (WidgetTester tester) async { + testWidgets('Passing no GalleryFooterThemeData returns default light theme values', (WidgetTester tester) async { late BuildContext _context; await tester.pumpWidget( MaterialApp( @@ -85,27 +68,17 @@ void main() { ); final imageFooterTheme = StreamGalleryFooterTheme.of(_context); - expect(imageFooterTheme.backgroundColor, - _galleryFooterThemeDataControl.backgroundColor); - expect(imageFooterTheme.shareIconColor, - _galleryFooterThemeDataControl.shareIconColor); - expect(imageFooterTheme.titleTextStyle, - _galleryFooterThemeDataControl.titleTextStyle); - expect(imageFooterTheme.gridIconButtonColor, - _galleryFooterThemeDataControl.gridIconButtonColor); - expect(imageFooterTheme.bottomSheetBarrierColor, - _galleryFooterThemeDataControl.bottomSheetBarrierColor); - expect(imageFooterTheme.bottomSheetBackgroundColor, - _galleryFooterThemeDataControl.bottomSheetBackgroundColor); - expect(imageFooterTheme.bottomSheetCloseIconColor, - _galleryFooterThemeDataControl.bottomSheetCloseIconColor); - expect(imageFooterTheme.bottomSheetPhotosTextStyle, - _galleryFooterThemeDataControl.bottomSheetPhotosTextStyle); + expect(imageFooterTheme.backgroundColor, _galleryFooterThemeDataControl.backgroundColor); + expect(imageFooterTheme.shareIconColor, _galleryFooterThemeDataControl.shareIconColor); + expect(imageFooterTheme.titleTextStyle, _galleryFooterThemeDataControl.titleTextStyle); + expect(imageFooterTheme.gridIconButtonColor, _galleryFooterThemeDataControl.gridIconButtonColor); + expect(imageFooterTheme.bottomSheetBarrierColor, _galleryFooterThemeDataControl.bottomSheetBarrierColor); + expect(imageFooterTheme.bottomSheetBackgroundColor, _galleryFooterThemeDataControl.bottomSheetBackgroundColor); + expect(imageFooterTheme.bottomSheetCloseIconColor, _galleryFooterThemeDataControl.bottomSheetCloseIconColor); + expect(imageFooterTheme.bottomSheetPhotosTextStyle, _galleryFooterThemeDataControl.bottomSheetPhotosTextStyle); }); - testWidgets( - 'Passing no GalleryFooterThemeData returns default dark theme values', - (WidgetTester tester) async { + testWidgets('Passing no GalleryFooterThemeData returns default dark theme values', (WidgetTester tester) async { late BuildContext _context; await tester.pumpWidget( MaterialApp( @@ -124,35 +97,27 @@ void main() { ); final imageFooterTheme = StreamGalleryFooterTheme.of(_context); - expect(imageFooterTheme.backgroundColor, - _galleryFooterThemeDataControlDark.backgroundColor); - expect(imageFooterTheme.shareIconColor, - _galleryFooterThemeDataControlDark.shareIconColor); - expect(imageFooterTheme.titleTextStyle, - _galleryFooterThemeDataControlDark.titleTextStyle); - expect(imageFooterTheme.gridIconButtonColor, - _galleryFooterThemeDataControlDark.gridIconButtonColor); - expect(imageFooterTheme.bottomSheetBarrierColor, - _galleryFooterThemeDataControlDark.bottomSheetBarrierColor); - expect(imageFooterTheme.bottomSheetBackgroundColor, - _galleryFooterThemeDataControlDark.bottomSheetBackgroundColor); - expect(imageFooterTheme.bottomSheetCloseIconColor, - _galleryFooterThemeDataControlDark.bottomSheetCloseIconColor); - expect(imageFooterTheme.bottomSheetPhotosTextStyle, - _galleryFooterThemeDataControlDark.bottomSheetPhotosTextStyle); + expect(imageFooterTheme.backgroundColor, _galleryFooterThemeDataControlDark.backgroundColor); + expect(imageFooterTheme.shareIconColor, _galleryFooterThemeDataControlDark.shareIconColor); + expect(imageFooterTheme.titleTextStyle, _galleryFooterThemeDataControlDark.titleTextStyle); + expect(imageFooterTheme.gridIconButtonColor, _galleryFooterThemeDataControlDark.gridIconButtonColor); + expect(imageFooterTheme.bottomSheetBarrierColor, _galleryFooterThemeDataControlDark.bottomSheetBarrierColor); + expect(imageFooterTheme.bottomSheetBackgroundColor, _galleryFooterThemeDataControlDark.bottomSheetBackgroundColor); + expect(imageFooterTheme.bottomSheetCloseIconColor, _galleryFooterThemeDataControlDark.bottomSheetCloseIconColor); + expect(imageFooterTheme.bottomSheetPhotosTextStyle, _galleryFooterThemeDataControlDark.bottomSheetPhotosTextStyle); }); } // 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 +142,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/gallery_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart index 6c080193ed..f3bd223539 100644 --- a/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart @@ -7,26 +7,18 @@ class MockStreamChatClient extends Mock implements StreamChatClient {} void main() { test('GalleryHeaderThemeData copyWith, ==, hashCode basics', () { - expect(const StreamGalleryHeaderThemeData(), - const StreamGalleryHeaderThemeData().copyWith()); - expect(const StreamGalleryHeaderThemeData().hashCode, - const StreamGalleryHeaderThemeData().copyWith().hashCode); + expect(const StreamGalleryHeaderThemeData(), const StreamGalleryHeaderThemeData().copyWith()); + expect(const StreamGalleryHeaderThemeData().hashCode, const StreamGalleryHeaderThemeData().copyWith().hashCode); }); - test( - '''Light GalleryHeaderThemeData lerps completely to dark GalleryHeaderThemeData''', - () { + test('''Light GalleryHeaderThemeData lerps completely to dark GalleryHeaderThemeData''', () { expect( - const StreamGalleryHeaderThemeData().lerp( - _galleryHeaderThemeDataControl, - _galleryHeaderThemeDataDarkControl, - 1), - _galleryHeaderThemeDataDarkControl); + const StreamGalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl, _galleryHeaderThemeDataDarkControl, 1), + _galleryHeaderThemeDataDarkControl, + ); }); - test( - '''Light GalleryHeaderThemeData lerps halfway to dark GalleryHeaderThemeData''', - () { + test('''Light GalleryHeaderThemeData lerps halfway to dark GalleryHeaderThemeData''', () { expect( const StreamGalleryHeaderThemeData().lerp( _galleryHeaderThemeDataControl, @@ -40,27 +32,21 @@ void main() { ); }); - test( - '''Dark GalleryHeaderThemeData lerps completely to light GalleryHeaderThemeData''', - () { + test('''Dark GalleryHeaderThemeData lerps completely to light GalleryHeaderThemeData''', () { expect( - const StreamGalleryHeaderThemeData().lerp( - _galleryHeaderThemeDataDarkControl, - _galleryHeaderThemeDataControl, - 1), - _galleryHeaderThemeDataControl); + const StreamGalleryHeaderThemeData().lerp(_galleryHeaderThemeDataDarkControl, _galleryHeaderThemeDataControl, 1), + _galleryHeaderThemeDataControl, + ); }); test('Merging dark and light themes results in a dark theme', () { expect( - _galleryHeaderThemeDataControl - .merge(_galleryHeaderThemeDataDarkControl), - _galleryHeaderThemeDataDarkControl); + _galleryHeaderThemeDataControl.merge(_galleryHeaderThemeDataDarkControl), + _galleryHeaderThemeDataDarkControl, + ); }); - testWidgets( - 'Passing no GalleryHeaderThemeData returns default light theme values', - (WidgetTester tester) async { + testWidgets('Passing no GalleryHeaderThemeData returns default light theme values', (WidgetTester tester) async { late BuildContext _context; await tester.pumpWidget( MaterialApp( @@ -78,23 +64,15 @@ void main() { ); final imageHeaderTheme = StreamGalleryHeaderTheme.of(_context); - expect(imageHeaderTheme.closeButtonColor, - _galleryHeaderThemeDataControl.closeButtonColor); - expect(imageHeaderTheme.backgroundColor, - _galleryHeaderThemeDataControl.backgroundColor); - expect(imageHeaderTheme.iconMenuPointColor, - _galleryHeaderThemeDataControl.iconMenuPointColor); - expect(imageHeaderTheme.titleTextStyle, - _galleryHeaderThemeDataControl.titleTextStyle); - expect(imageHeaderTheme.subtitleTextStyle, - _galleryHeaderThemeDataControl.subtitleTextStyle); - expect(imageHeaderTheme.bottomSheetBarrierColor, - _galleryHeaderThemeDataControl.bottomSheetBarrierColor); + expect(imageHeaderTheme.closeButtonColor, _galleryHeaderThemeDataControl.closeButtonColor); + expect(imageHeaderTheme.backgroundColor, _galleryHeaderThemeDataControl.backgroundColor); + expect(imageHeaderTheme.iconMenuPointColor, _galleryHeaderThemeDataControl.iconMenuPointColor); + expect(imageHeaderTheme.titleTextStyle, _galleryHeaderThemeDataControl.titleTextStyle); + expect(imageHeaderTheme.subtitleTextStyle, _galleryHeaderThemeDataControl.subtitleTextStyle); + expect(imageHeaderTheme.bottomSheetBarrierColor, _galleryHeaderThemeDataControl.bottomSheetBarrierColor); }); - testWidgets( - 'Passing no GalleryHeaderThemeData returns default dark theme values', - (WidgetTester tester) async { + testWidgets('Passing no GalleryHeaderThemeData returns default dark theme values', (WidgetTester tester) async { late BuildContext _context; await tester.pumpWidget( MaterialApp( @@ -113,18 +91,12 @@ void main() { ); final imageHeaderTheme = StreamGalleryHeaderTheme.of(_context); - expect(imageHeaderTheme.closeButtonColor, - _galleryHeaderThemeDataDarkControl.closeButtonColor); - expect(imageHeaderTheme.backgroundColor, - _galleryHeaderThemeDataDarkControl.backgroundColor); - expect(imageHeaderTheme.iconMenuPointColor, - _galleryHeaderThemeDataDarkControl.iconMenuPointColor); - expect(imageHeaderTheme.titleTextStyle, - _galleryHeaderThemeDataDarkControl.titleTextStyle); - expect(imageHeaderTheme.subtitleTextStyle, - _galleryHeaderThemeDataDarkControl.subtitleTextStyle); - expect(imageHeaderTheme.bottomSheetBarrierColor, - _galleryHeaderThemeDataDarkControl.bottomSheetBarrierColor); + expect(imageHeaderTheme.closeButtonColor, _galleryHeaderThemeDataDarkControl.closeButtonColor); + expect(imageHeaderTheme.backgroundColor, _galleryHeaderThemeDataDarkControl.backgroundColor); + expect(imageHeaderTheme.iconMenuPointColor, _galleryHeaderThemeDataDarkControl.iconMenuPointColor); + expect(imageHeaderTheme.titleTextStyle, _galleryHeaderThemeDataDarkControl.titleTextStyle); + expect(imageHeaderTheme.subtitleTextStyle, _galleryHeaderThemeDataDarkControl.subtitleTextStyle); + expect(imageHeaderTheme.bottomSheetBarrierColor, _galleryHeaderThemeDataDarkControl.bottomSheetBarrierColor); }); } @@ -138,13 +110,14 @@ final _galleryHeaderThemeDataControl = StreamGalleryHeaderThemeData( fontWeight: FontWeight.w500, color: Colors.black, ), - subtitleTextStyle: const TextStyle( - fontSize: 12, - color: Colors.black, - fontWeight: FontWeight.w400, - ).copyWith( - color: const Color(0xff7A7A7A), - ), + subtitleTextStyle: + const TextStyle( + fontSize: 12, + color: Colors.black, + fontWeight: FontWeight.w400, + ).copyWith( + color: const Color(0xff7A7A7A), + ), bottomSheetBarrierColor: const Color.fromRGBO(0, 0, 0, 0.2), ); @@ -158,13 +131,14 @@ final _galleryHeaderThemeDataHalfLerpControl = StreamGalleryHeaderThemeData( fontWeight: FontWeight.w500, color: Color(0xff7f7f7f), ), - subtitleTextStyle: const TextStyle( - fontSize: 12, - color: Color(0xff7a7a7a), - fontWeight: FontWeight.w400, - ).copyWith( - color: const Color(0xff7A7A7A), - ), + subtitleTextStyle: + const TextStyle( + fontSize: 12, + color: Color(0xff7a7a7a), + fontWeight: FontWeight.w400, + ).copyWith( + color: const Color(0xff7A7A7A), + ), bottomSheetBarrierColor: const Color(0x4c000000), ); @@ -178,12 +152,13 @@ final _galleryHeaderThemeDataDarkControl = StreamGalleryHeaderThemeData( fontWeight: FontWeight.w500, color: Colors.white, ), - subtitleTextStyle: const TextStyle( - fontSize: 12, - color: Colors.white, - fontWeight: FontWeight.w400, - ).copyWith( - color: const Color(0xff7A7A7A), - ), + subtitleTextStyle: + const TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w400, + ).copyWith( + color: const Color(0xff7A7A7A), + ), bottomSheetBarrierColor: const Color.fromRGBO(0, 0, 0, 0.4), ); 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..96cb7fd979 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 @@ -4,18 +4,16 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('MessageInputThemeData copyWith, ==, hashCode basics', () { - expect(const StreamMessageInputThemeData(), - const StreamMessageInputThemeData().copyWith()); - expect(const StreamMessageInputThemeData().hashCode, - const StreamMessageInputThemeData().copyWith().hashCode); + expect(const StreamMessageInputThemeData(), const StreamMessageInputThemeData().copyWith()); + expect(const StreamMessageInputThemeData().hashCode, const StreamMessageInputThemeData().copyWith().hashCode); }); group('MessageInputThemeData lerps correctly', () { test('Lerp completely from light to dark', () { expect( - const StreamMessageInputThemeData().lerp( - _messageInputThemeControl, _messageInputThemeControlDark, 1), - _messageInputThemeControlDark); + const StreamMessageInputThemeData().lerp(_messageInputThemeControl, _messageInputThemeControlDark, 1), + _messageInputThemeControlDark, + ); }); test('Lerp halfway from light to dark', () { @@ -34,40 +32,39 @@ void main() { test('Lerp completely from dark to light', () { expect( - const StreamMessageInputThemeData().lerp( - _messageInputThemeControlDark, _messageInputThemeControl, 1), - _messageInputThemeControl); + const StreamMessageInputThemeData().lerp(_messageInputThemeControlDark, _messageInputThemeControl, 1), + _messageInputThemeControl, + ); }); }); test('Merging two MessageInputThemeData results in the latter', () { - expect(_messageInputThemeControl.merge(_messageInputThemeControlDark), - _messageInputThemeControlDark); + expect(_messageInputThemeControl.merge(_messageInputThemeControlDark), _messageInputThemeControlDark); }); } 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 +102,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..b1e8f4842b 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 @@ -9,26 +9,22 @@ class MockStreamChatClient extends Mock implements StreamChatClient {} void main() { test('MessageListViewThemeData copyWith, ==, hashCode basics', () { - expect(const StreamMessageListViewThemeData(), - const StreamMessageListViewThemeData().copyWith()); - expect(const StreamMessageListViewThemeData().hashCode, - const StreamMessageListViewThemeData().copyWith().hashCode); + expect(const StreamMessageListViewThemeData(), const StreamMessageListViewThemeData().copyWith()); + expect(const StreamMessageListViewThemeData().hashCode, const StreamMessageListViewThemeData().copyWith().hashCode); }); - test( - '''Light MessageListViewThemeData lerps completely to dark MessageListViewThemeData''', - () { + test('''Light MessageListViewThemeData lerps completely to dark MessageListViewThemeData''', () { expect( - const StreamMessageListViewThemeData().lerp( - _messageListViewThemeDataControl, - _messageListViewThemeDataControlDark, - 1), - _messageListViewThemeDataControlDark); + const StreamMessageListViewThemeData().lerp( + _messageListViewThemeDataControl, + _messageListViewThemeDataControlDark, + 1, + ), + _messageListViewThemeDataControlDark, + ); }); - test( - '''Light MessageListViewThemeData lerps halfway to dark MessageListViewThemeData''', - () { + test('''Light MessageListViewThemeData lerps halfway to dark MessageListViewThemeData''', () { expect( const StreamMessageListViewThemeData().lerp( _messageListViewThemeDataControl, @@ -42,27 +38,25 @@ void main() { ); }); - test( - '''Dark MessageListViewThemeData lerps completely to light MessageListViewThemeData''', - () { + test('''Dark MessageListViewThemeData lerps completely to light MessageListViewThemeData''', () { expect( - const StreamMessageListViewThemeData().lerp( - _messageListViewThemeDataControlDark, - _messageListViewThemeDataControl, - 1), - _messageListViewThemeDataControl); + const StreamMessageListViewThemeData().lerp( + _messageListViewThemeDataControlDark, + _messageListViewThemeDataControl, + 1, + ), + _messageListViewThemeDataControl, + ); }); test('Merging dark and light themes results in a dark theme', () { expect( - _messageListViewThemeDataControl - .merge(_messageListViewThemeDataControlDark), - _messageListViewThemeDataControlDark); + _messageListViewThemeDataControl.merge(_messageListViewThemeDataControlDark), + _messageListViewThemeDataControlDark, + ); }); - testWidgets( - 'Passing no MessageListViewThemeData returns default light theme values', - (WidgetTester tester) async { + testWidgets('Passing no MessageListViewThemeData returns default light theme values', (WidgetTester tester) async { late BuildContext _context; await tester.pumpWidget( MaterialApp( @@ -80,13 +74,10 @@ void main() { ); final messageListViewTheme = StreamMessageListViewTheme.of(_context); - expect(messageListViewTheme.backgroundColor, - _messageListViewThemeDataControl.backgroundColor); + expect(messageListViewTheme.backgroundColor, _messageListViewThemeDataControl.backgroundColor); }); - testWidgets( - 'Passing no MessageListViewThemeData returns default dark theme values', - (WidgetTester tester) async { + testWidgets('Passing no MessageListViewThemeData returns default dark theme values', (WidgetTester tester) async { late BuildContext _context; await tester.pumpWidget( MaterialApp( @@ -105,20 +96,18 @@ void main() { ); final messageListViewTheme = StreamMessageListViewTheme.of(_context); - expect(messageListViewTheme.backgroundColor, - _messageListViewThemeDataControlDark.backgroundColor); + expect(messageListViewTheme.backgroundColor, _messageListViewThemeDataControlDark.backgroundColor); }); - testWidgets( - 'Pass backgroundImage to MessageListViewThemeData return backgroundImage', - (WidgetTester tester) async { + testWidgets('Pass backgroundImage to MessageListViewThemeData return backgroundImage', (WidgetTester tester) async { late BuildContext _context; await tester.pumpWidget( MaterialApp( builder: (context, child) => StreamChat( client: MockStreamChatClient(), - streamChatThemeData: StreamChatThemeData.light() - .copyWith(messageListViewTheme: _messageListViewThemeDataImage), + streamChatThemeData: StreamChatThemeData.light().copyWith( + messageListViewTheme: _messageListViewThemeDataImage, + ), child: child, ), home: Builder( @@ -136,13 +125,12 @@ void main() { ); final messageListViewTheme = StreamMessageListViewTheme.of(_context); - expect(messageListViewTheme.backgroundImage, - _messageListViewThemeDataImage.backgroundImage); + expect(messageListViewTheme.backgroundImage, _messageListViewThemeDataImage.backgroundImage); }); } final _messageListViewThemeDataControl = StreamMessageListViewThemeData( - backgroundColor: StreamColorTheme.light().barsBg, + backgroundColor: const StreamColorTheme.light().appBg, ); const _messageListViewThemeDataControlHalfLerp = StreamMessageListViewThemeData( @@ -150,7 +138,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 b60cdc259b..d8759b2be3 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 @@ -6,53 +6,48 @@ String _dummyFormatter(BuildContext context, DateTime date) => 'formatted'; void main() { test('MessageThemeData copyWith, ==, hashCode basics', () { - expect(const StreamMessageThemeData(), - const StreamMessageThemeData().copyWith()); - expect(const StreamMessageThemeData().hashCode, - const StreamMessageThemeData().copyWith().hashCode); + expect(const StreamMessageThemeData(), const StreamMessageThemeData().copyWith()); + expect(const StreamMessageThemeData().hashCode, const StreamMessageThemeData().copyWith().hashCode); }); group('MessageThemeData lerps', () { - test('''Light MessageThemeData lerps completely to dark MessageThemeData''', - () { + test('''Light MessageThemeData lerps completely to dark MessageThemeData''', () { expect( - const StreamMessageThemeData() - .lerp(_messageThemeControl, _messageThemeControlDark, 1), - _messageThemeControlDark); + const StreamMessageThemeData().lerp(_messageThemeControl, _messageThemeControlDark, 1), + _messageThemeControlDark, + ); }); - test('''Dark MessageThemeData lerps completely to light MessageThemeData''', - () { + test('''Dark MessageThemeData lerps completely to light MessageThemeData''', () { expect( - const StreamMessageThemeData() - .lerp(_messageThemeControlDark, _messageThemeControl, 1), - _messageThemeControl); + const StreamMessageThemeData().lerp(_messageThemeControlDark, _messageThemeControl, 1), + _messageThemeControl, + ); }); }); test('Merging dark and light themes results in a dark theme', () { - expect(_messageThemeControl.merge(_messageThemeControlDark), - _messageThemeControlDark); + expect(_messageThemeControl.merge(_messageThemeControlDark), _messageThemeControlDark); }); } final _messageThemeControl = StreamMessageThemeData( - messageAuthorStyle: StreamTextTheme.light().footnote.copyWith( - color: StreamColorTheme.light().textLowEmphasis, - ), - messageTextStyle: StreamTextTheme.light().body, - createdAtStyle: StreamTextTheme.light().footnote.copyWith( - color: StreamColorTheme.light().textLowEmphasis, - ), + messageAuthorStyle: const StreamTextTheme.light().footnote.copyWith( + color: const StreamColorTheme.light().textLowEmphasis, + ), + messageTextStyle: const StreamTextTheme.light().body, + createdAtStyle: const StreamTextTheme.light().footnote.copyWith( + color: const StreamColorTheme.light().textLowEmphasis, + ), createdAtFormatter: _dummyFormatter, - repliesStyle: StreamTextTheme.light().footnoteBold.copyWith( - color: StreamColorTheme.light().accentPrimary, - ), - messageBackgroundColor: StreamColorTheme.light().disabled, - reactionsBackgroundColor: StreamColorTheme.light().barsBg, - reactionsBorderColor: StreamColorTheme.light().borders, - reactionsMaskColor: StreamColorTheme.light().appBg, - messageBorderColor: StreamColorTheme.light().disabled, + repliesStyle: const StreamTextTheme.light().footnoteBold.copyWith( + color: const StreamColorTheme.light().accentPrimary, + ), + 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( @@ -61,28 +56,28 @@ 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, - ), - messageTextStyle: StreamTextTheme.dark().body, - createdAtStyle: StreamTextTheme.dark().footnote.copyWith( - color: StreamColorTheme.dark().textLowEmphasis, - ), + messageAuthorStyle: const StreamTextTheme.dark().footnote.copyWith( + color: const StreamColorTheme.dark().textLowEmphasis, + ), + messageTextStyle: const StreamTextTheme.dark().body, + createdAtStyle: const StreamTextTheme.dark().footnote.copyWith( + color: const StreamColorTheme.dark().textLowEmphasis, + ), createdAtFormatter: _dummyFormatter, - repliesStyle: StreamTextTheme.dark().footnoteBold.copyWith( - color: StreamColorTheme.dark().accentPrimary, - ), - messageBackgroundColor: StreamColorTheme.dark().disabled, - reactionsBackgroundColor: StreamColorTheme.dark().barsBg, - reactionsBorderColor: StreamColorTheme.dark().borders, - reactionsMaskColor: StreamColorTheme.dark().appBg, - messageBorderColor: StreamColorTheme.dark().disabled, + repliesStyle: const StreamTextTheme.dark().footnoteBold.copyWith( + color: const StreamColorTheme.dark().accentPrimary, + ), + 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( @@ -91,7 +86,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/theme/thread_list_tile_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/thread_list_tile_theme_test.dart index dd857151f6..d4c38dece2 100644 --- a/packages/stream_chat_flutter/test/src/theme/thread_list_tile_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/thread_list_tile_theme_test.dart @@ -5,8 +5,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; String _dummyFormatter(BuildContext context, DateTime date) => 'formatted'; void main() { - testWidgets('StreamThreadListTileTheme merges with ancestor theme', - (tester) async { + testWidgets('StreamThreadListTileTheme merges with ancestor theme', (tester) async { const backgroundColor = Colors.blue; const childBackgroundColor = Colors.red; @@ -116,12 +115,9 @@ void main() { expect(copied.padding, newPadding); // Unchanged properties should remain the same expect(copied.threadChannelNameStyle, original.threadChannelNameStyle); - expect( - copied.threadReplyToMessageStyle, original.threadReplyToMessageStyle); - expect(copied.threadLatestReplyTimestampStyle, - original.threadLatestReplyTimestampStyle); - expect(copied.threadLatestReplyTimestampFormatter, - original.threadLatestReplyTimestampFormatter); + expect(copied.threadReplyToMessageStyle, original.threadReplyToMessageStyle); + expect(copied.threadLatestReplyTimestampStyle, original.threadLatestReplyTimestampStyle); + expect(copied.threadLatestReplyTimestampFormatter, original.threadLatestReplyTimestampFormatter); }); test('StreamThreadListTileThemeData merge', () { @@ -147,12 +143,9 @@ void main() { expect(merged.padding, other.padding); // Null properties in 'other' should not override 'original' expect(merged.threadChannelNameStyle, original.threadChannelNameStyle); - expect( - merged.threadReplyToMessageStyle, original.threadReplyToMessageStyle); - expect(merged.threadLatestReplyTimestampStyle, - original.threadLatestReplyTimestampStyle); - expect(merged.threadLatestReplyTimestampFormatter, - original.threadLatestReplyTimestampFormatter); + expect(merged.threadReplyToMessageStyle, original.threadReplyToMessageStyle); + expect(merged.threadLatestReplyTimestampStyle, original.threadLatestReplyTimestampStyle); + expect(merged.threadLatestReplyTimestampFormatter, original.threadLatestReplyTimestampFormatter); // Merging with null should return original final mergedWithNull = original.merge(null); @@ -176,29 +169,26 @@ void main() { final lerpedAt0 = data1.lerp(data1, data2, 0); expect(lerpedAt0.backgroundColor, data1.backgroundColor); expect(lerpedAt0.padding, data1.padding); - expect(lerpedAt0.threadLatestReplyTimestampFormatter, - data1.threadLatestReplyTimestampFormatter); + expect(lerpedAt0.threadLatestReplyTimestampFormatter, data1.threadLatestReplyTimestampFormatter); // t = 1 should return data2 final lerpedAt1 = data1.lerp(data1, data2, 1); expect(lerpedAt1.backgroundColor, data2.backgroundColor); expect(lerpedAt1.padding, data2.padding); - expect(lerpedAt1.threadLatestReplyTimestampFormatter, - data2.threadLatestReplyTimestampFormatter); + expect(lerpedAt1.threadLatestReplyTimestampFormatter, data2.threadLatestReplyTimestampFormatter); // t = 0.5 should return something in between final lerpedAt05 = data1.lerp(data1, data2, 0.5); - expect(lerpedAt05.backgroundColor, - Color.lerp(Colors.black, Colors.white, 0.5)); + expect(lerpedAt05.backgroundColor, Color.lerp(Colors.black, Colors.white, 0.5)); expect( - lerpedAt05.padding, - EdgeInsetsGeometry.lerp( - const EdgeInsets.all(8), - const EdgeInsets.all(16), - 0.5, - )); + lerpedAt05.padding, + EdgeInsetsGeometry.lerp( + const EdgeInsets.all(8), + const EdgeInsets.all(16), + 0.5, + ), + ); // For t < 0.5, should use data1's formatter - expect(lerpedAt05.threadLatestReplyTimestampFormatter, - data1.threadLatestReplyTimestampFormatter); + expect(lerpedAt05.threadLatestReplyTimestampFormatter, data1.threadLatestReplyTimestampFormatter); }); } diff --git a/packages/stream_chat_flutter/test/src/utils/date_formatter_test.dart b/packages/stream_chat_flutter/test/src/utils/date_formatter_test.dart new file mode 100644 index 0000000000..0ba320f8f1 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/utils/date_formatter_test.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/src/utils/date_formatter.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import '../mocks.dart'; + +void main() { + group('formatRecentDateTime', () { + final referenceDate = DateTime(2026, 4, 7, 10, 0); + + testWidgets('formats dates within a minute as Just now', (tester) async { + await tester.pumpWidget( + _wrapWithStreamChat( + Builder( + builder: (context) { + return Text( + formatRecentDateTime( + context, + DateTime(2026, 4, 7, 9, 59, 30), + referenceDate: referenceDate, + ), + ); + }, + ), + ), + ); + + expect(find.text('Just now'), findsOneWidget); + }); + + testWidgets('formats same-day dates as Today at H:mm', (tester) async { + await tester.pumpWidget( + _wrapWithStreamChat( + Builder( + builder: (context) { + return Text( + formatRecentDateTime( + context, + DateTime(2026, 4, 7, 9, 41), + referenceDate: referenceDate, + ), + ); + }, + ), + ), + ); + + expect(find.text('Today at 9:41'), findsOneWidget); + }); + + testWidgets('formats previous-day dates as Yesterday at H:mm', (tester) async { + await tester.pumpWidget( + _wrapWithStreamChat( + Builder( + builder: (context) { + return Text( + formatRecentDateTime( + context, + DateTime(2026, 4, 6, 9, 41), + referenceDate: referenceDate, + ), + ); + }, + ), + ), + ); + + expect(find.text('Yesterday at 9:41'), findsOneWidget); + }); + + testWidgets('formats recent dates within a week as Weekday at H:mm', (tester) async { + await tester.pumpWidget( + _wrapWithStreamChat( + Builder( + builder: (context) { + return Text( + formatRecentDateTime( + context, + DateTime(2026, 4, 4, 9, 41), + referenceDate: referenceDate, + ), + ); + }, + ), + ), + ); + + expect(find.text('Saturday at 9:41'), findsOneWidget); + }); + + testWidgets('formats older dates as MMM do at H:mm', (tester) async { + await tester.pumpWidget( + _wrapWithStreamChat( + Builder( + builder: (context) { + return Text( + formatRecentDateTime( + context, + DateTime(2026, 1, 1, 9, 41), + referenceDate: referenceDate, + ), + ); + }, + ), + ), + ); + + expect(find.text('Jan 1st at 9:41'), findsOneWidget); + }); + }); +} + +Widget _wrapWithStreamChat(Widget child) { + final client = MockClient(); + final clientState = MockClientState(); + + when(() => client.state).thenReturn(clientState); + when(() => clientState.currentUser).thenReturn(OwnUser(id: 'current-user-id', name: 'Current User')); + + return MaterialApp( + home: StreamChat( + client: client, + child: Scaffold(body: child), + ), + ); +} 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 ce99d0786a..9f3b7bdb76 100644 --- a/packages/stream_chat_flutter/test/src/utils/extension_test.dart +++ b/packages/stream_chat_flutter/test/src/utils/extension_test.dart @@ -163,8 +163,8 @@ void main() { final modifiedMessage = message.replaceMentions(); - expect(modifiedMessage.text, contains('[@Alice](user1)')); - expect(modifiedMessage.text, contains('[@Bob](user2)')); + expect(modifiedMessage.text, contains('[@Alice](mention:user1)')); + expect(modifiedMessage.text, contains('[@Bob](mention:user2)')); }); test('replaceMentions without linkify should not add links', () { @@ -190,7 +190,7 @@ void main() { final modifiedMessage = message.replaceMentions(); - expect(modifiedMessage.text, contains('[@Alice](user1)')); + expect(modifiedMessage.text, contains('[@Alice](mention:user1)')); }); test( @@ -222,8 +222,8 @@ void main() { final modifiedMessage = message.replaceMentions(); - expect(modifiedMessage.text, contains('[@Alice](user1)')); - expect(modifiedMessage.text, contains('[@Bob](user2)')); + expect(modifiedMessage.text, contains('[@Alice](mention:user1)')); + expect(modifiedMessage.text, contains('[@Bob](mention:user2)')); }, ); @@ -238,7 +238,7 @@ void main() { final modifiedMessage = message.replaceMentions(); - expect(modifiedMessage.text, equals('Hello, [@Tester (X)](user1)!')); + expect(modifiedMessage.text, equals('Hello, [@Tester (X)](mention:user1)!')); }); test('should handle usernames with square brackets', () { @@ -251,7 +251,7 @@ void main() { final modifiedMessage = message.replaceMentions(); - expect(modifiedMessage.text, equals('Hello, [@User[123]](user1)!')); + expect(modifiedMessage.text, equals('Hello, [@User[123]](mention:user1)!')); }); test('should handle usernames with dots and asterisks', () { @@ -264,7 +264,7 @@ void main() { final modifiedMessage = message.replaceMentions(); - expect(modifiedMessage.text, equals('Hello, [@user.name*](user1)!')); + expect(modifiedMessage.text, equals('Hello, [@user.name*](mention:user1)!')); }); test('should handle usernames with plus and question marks', () { @@ -277,7 +277,7 @@ void main() { final modifiedMessage = message.replaceMentions(); - expect(modifiedMessage.text, equals('Hello, [@test+user?](user1)!')); + expect(modifiedMessage.text, equals('Hello, [@test+user?](mention:user1)!')); }); test('should handle usernames without linkify', () { @@ -305,7 +305,7 @@ void main() { expect( modifiedMessage.text, - equals('Hello, [@Test (X)](user1) and @Test (Y)!'), + equals('Hello, [@Test (X)](mention:user1) and @Test (Y)!'), ); }); @@ -321,12 +321,11 @@ void main() { expect( modifiedMessage.text, - equals('Hello, [@TestUser](user.id+123)!'), + equals('Hello, [@TestUser](mention:user.id+123)!'), ); }); - test('should handle both userId and userName with special characters', - () { + test('should handle both userId and userName with special characters', () { final user = User(id: 'user[123]', name: 'Test (X)'); final message = Message( @@ -338,7 +337,7 @@ void main() { expect( modifiedMessage.text, - equals('Hello, [@Test (X)](user[123]) and [@Test (X)](user[123])!'), + equals('Hello, [@Test (X)](mention:user[123]) and [@Test (X)](mention:user[123])!'), ); }); }); @@ -388,124 +387,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/test/src/utils/stream_image_cdn_test.dart b/packages/stream_chat_flutter/test/src/utils/stream_image_cdn_test.dart new file mode 100644 index 0000000000..967ba5a9ed --- /dev/null +++ b/packages/stream_chat_flutter/test/src/utils/stream_image_cdn_test.dart @@ -0,0 +1,237 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/src/utils/stream_image_cdn.dart'; + +void main() { + const cdn = StreamImageCDN(); + + group('StreamImageCDN.resolveUrl', () { + group('Stream CDN URLs', () { + test('returns unchanged URL when resize is null', () { + const url = + 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg' + '?Policy=abc&Signature=xyz&Key-Pair-Id=123'; + + expect(cdn.resolveUrl(url), equals(url)); + }); + + test('adds resize params when none exist', () { + const url = 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg'; + const resize = ImageResize(width: 200, height: 300); + + final result = cdn.resolveUrl(url, resize: resize); + + expect(result, contains('w=200')); + expect(result, contains('h=300')); + expect(result, contains('resize=clip')); + expect(result, contains('ro=0')); + expect(result, isNot(contains('crop='))); + }); + + test('includes crop param only when mode is crop', () { + const url = 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg'; + const resize = ImageResize( + width: 400, + height: 400, + mode: ResizeMode.crop, + crop: CropMode.top, + ); + + final result = cdn.resolveUrl(url, resize: resize); + + expect(result, contains('resize=crop')); + expect(result, contains('crop=top')); + expect(result, contains('ro=0')); + }); + + test('does not include crop param when mode is not crop', () { + const url = 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg'; + + for (final mode in [ + ResizeMode.clip, + ResizeMode.scale, + ResizeMode.fill, + ]) { + final result = cdn.resolveUrl( + url, + resize: ImageResize(width: 200, height: 200, mode: mode), + ); + + expect( + result, + isNot(contains('crop=')), + reason: 'crop should not be present for mode ${mode.value}', + ); + } + }); + + test('always overrides existing resize params', () { + const url = + 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg' + '?w=100&h=100&resize=fill'; + const resize = ImageResize( + width: 200, + height: 300, + mode: ResizeMode.crop, + crop: CropMode.left, + ); + + final result = cdn.resolveUrl(url, resize: resize); + + expect(result, contains('w=200')); + expect(result, contains('h=300')); + expect(result, contains('resize=crop')); + expect(result, contains('crop=left')); + }); + + test('preserves existing non-resize query parameters', () { + const url = + 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg' + '?Policy=abc&Signature=xyz&Key-Pair-Id=123'; + const resize = ImageResize(width: 200, height: 300); + + final result = cdn.resolveUrl(url, resize: resize); + + expect(result, contains('Policy=abc')); + expect(result, contains('Signature=xyz')); + expect(result, contains('Key-Pair-Id=123')); + expect(result, contains('w=200')); + }); + + test('floors fractional dimensions', () { + const url = 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg'; + const resize = ImageResize(width: 199.7, height: 300.3); + + final result = cdn.resolveUrl(url, resize: resize); + + expect(result, contains('w=199')); + expect(result, contains('h=300')); + }); + + test('uses wildcard for zero dimensions', () { + const url = 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg'; + const resize = ImageResize(width: 0, height: 300); + + final result = cdn.resolveUrl(url, resize: resize); + + expect(result, contains('w=%2A')); + expect(result, contains('h=300')); + }); + }); + + group('non-Stream URLs', () { + test('returns URL unchanged regardless of resize', () { + const url = 'https://example.com/photo.jpg'; + const resize = ImageResize(width: 200, height: 300); + + expect(cdn.resolveUrl(url, resize: resize), equals(url)); + }); + + test('returns URL unchanged when resize is null', () { + const url = 'https://example.com/photo.jpg?token=abc'; + + expect(cdn.resolveUrl(url), equals(url)); + }); + }); + }); + + group('StreamImageCDN.cacheKey', () { + group('Stream CDN URLs', () { + test('strips signing parameters', () { + const url = + 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg' + '?Key-Pair-Id=APKAIHG&Policy=eyJTdGF0&Signature=OeMK5' + '&w=200&h=300&resize=clip&crop=center'; + + final key = cdn.cacheKey(url); + + expect(key, contains('w=200')); + expect(key, contains('h=300')); + expect(key, contains('resize=clip')); + expect(key, contains('crop=center')); + expect(key, isNot(contains('Key-Pair-Id'))); + expect(key, isNot(contains('Policy'))); + expect(key, isNot(contains('Signature'))); + }); + + test('returns URL path only when no resize params exist', () { + const url = + 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg' + '?Key-Pair-Id=APKAIHG&Policy=eyJTdGF0&Signature=OeMK5'; + + final key = cdn.cacheKey(url); + + expect(key, isNot(contains('Key-Pair-Id'))); + expect(key, isNot(contains('Policy'))); + expect(key, isNot(contains('Signature'))); + expect( + key, + 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg?', + ); + }); + + test('produces same key for same image with different signatures', () { + const url1 = + 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg' + '?Key-Pair-Id=APKAIHG&Policy=policy1&Signature=sig1' + '&w=200&h=300'; + const url2 = + 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg' + '?Key-Pair-Id=APKAIHG&Policy=policy2&Signature=sig2' + '&w=200&h=300'; + + expect(cdn.cacheKey(url1), equals(cdn.cacheKey(url2))); + }); + + test('produces different keys for different resize dimensions', () { + const url1 = + 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg' + '?w=200&h=300'; + const url2 = + 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg' + '?w=400&h=600'; + + expect(cdn.cacheKey(url1), isNot(equals(cdn.cacheKey(url2)))); + }); + + test('strips oh and ow parameters', () { + const url = + 'https://us-east.stream-io-cdn.com/102400/images/photo.jpg' + '?oh=4032&ow=3024&w=200&h=300'; + + final key = cdn.cacheKey(url); + + expect(key, isNot(contains('oh='))); + expect(key, isNot(contains('ow='))); + expect(key, contains('w=200')); + expect(key, contains('h=300')); + }); + }); + + group('non-Stream URLs', () { + test('returns full URL string unchanged', () { + const url = 'https://example.com/photo.jpg?token=abc'; + + expect(cdn.cacheKey(url), equals(url)); + }); + }); + }); + + group('ResizeMode', () { + test('all modes have correct string values', () { + expect(ResizeMode.clip.value, 'clip'); + expect(ResizeMode.crop.value, 'crop'); + expect(ResizeMode.scale.value, 'scale'); + expect(ResizeMode.fill.value, 'fill'); + }); + }); + + group('CropMode', () { + test('all modes have correct string values', () { + expect(CropMode.center.value, 'center'); + expect(CropMode.top.value, 'top'); + expect(CropMode.bottom.value, 'bottom'); + expect(CropMode.left.value, 'left'); + expect(CropMode.right.value, 'right'); + }); + }); +} diff --git a/packages/stream_chat_flutter/test/test_utils/data_generator.dart b/packages/stream_chat_flutter/test/test_utils/data_generator.dart index 5759136ba6..94f32285e3 100644 --- a/packages/stream_chat_flutter/test/test_utils/data_generator.dart +++ b/packages/stream_chat_flutter/test/test_utils/data_generator.dart @@ -8,9 +8,10 @@ List generateConversation( int unreadCount = 0, }) { assert( - users == null || noOfUsers == null, - 'Only one of users or noOfUsers ' - 'should be provided'); + users == null || noOfUsers == null, + 'Only one of users or noOfUsers ' + 'should be provided', + ); assert(count > 0, 'Count should be greater than 0'); assert(count > unreadCount, 'Count should be greater than unreadCount'); @@ -38,8 +39,7 @@ List generateConversation( id: faker.datatype.uuid(), text: faker.lorem.sentence(), user: user, - createdAt: - DateTime.now().subtract(Duration(minutes: i + count - unreadCount)), + createdAt: DateTime.now().subtract(Duration(minutes: i + count - unreadCount)), ), ); } diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index 67cf23e867..dfb8267402 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,15 +1,37 @@ +## 10.0.0-beta.13 + +🛑️ Breaking +- SDK Redesign Changes. For more details, please refer to the [migration guide](https://github.com/GetStream/stream-chat-flutter/blob/210ff93f955be3f85c62e860309bd9aa240a5446/migrations). + The SDK redesign introduces a fresher default UI, but also better APIs for customization of the components. + +## 10.0.0-beta.12 + +- Included the changes from version [`9.23.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + ## 9.23.0 - Updated `stream_chat` dependency to [`9.23.0`](https://pub.dev/packages/stream_chat/changelog). +## 10.0.0-beta.11 + +- Included the changes from version [`9.22.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + ## 9.22.0 - Updated `stream_chat` dependency to [`9.22.0`](https://pub.dev/packages/stream_chat/changelog). +## 10.0.0-beta.10 + +- Included the changes from version [`9.21.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + ## 9.21.0 - Updated `stream_chat` dependency to [`9.21.0`](https://pub.dev/packages/stream_chat/changelog). +## 10.0.0-beta.9 + +- Included the changes from version [`9.20.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + ## 9.20.0 🐞 Fixed @@ -17,18 +39,34 @@ - Fixed race condition where `connectUser` could be blocked when connectivity monitoring triggers during initial connection. [[#2409]](https://github.com/GetStream/stream-chat-flutter/issues/2409) +## 10.0.0-beta.8 + +- Included the changes from version [`9.19.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + ## 9.19.0 - Updated `stream_chat` dependency to [`9.19.0`](https://pub.dev/packages/stream_chat/changelog). +## 10.0.0-beta.7 + +- Included the changes from version [`9.18.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + ## 9.18.0 - Updated `stream_chat` dependency to [`9.18.0`](https://pub.dev/packages/stream_chat/changelog). +## 10.0.0-beta.6 + +- Included the changes from version [`9.17.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + ## 9.17.0 - Updated `stream_chat` dependency to [`9.17.0`](https://pub.dev/packages/stream_chat/changelog). +## 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 @@ -39,6 +77,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 @@ -53,6 +95,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 @@ -60,6 +106,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 @@ -67,6 +117,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/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index 0a523f81ac..40392a8c19 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -19,11 +19,7 @@ Future main() async { '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''', ); - runApp( - StreamExample( - client: client, - ), - ); + runApp(StreamExample(client: client)); } /// Example application using Stream Chat core widgets. @@ -37,10 +33,7 @@ class StreamExample extends StatelessWidget { /// /// If you'd prefer using pre-made UI widgets for your app, please see our /// other package, `stream_chat_flutter`. - const StreamExample({ - Key? key, - required this.client, - }) : super(key: key); + const StreamExample({Key? key, required this.client}) : super(key: key); /// Instance of Stream Client. /// Stream's [StreamChatClient] can be used to connect to our servers and @@ -50,13 +43,10 @@ class StreamExample extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( - title: 'Stream Chat Core Example', - home: HomeScreen(), - builder: (context, child) => StreamChatCore( - client: client, - child: child!, - ), - ); + title: 'Stream Chat Core Example', + home: HomeScreen(), + builder: (context, child) => StreamChatCore(client: client, child: child!), + ); } /// Basic layout displaying a list of [Channel]s the user is a part of. @@ -80,12 +70,7 @@ class _HomeScreenState extends State { client: StreamChatCore.of(context).client, filter: Filter.and([ Filter.equal('type', 'messaging'), - Filter.in_( - 'members', - [ - StreamChatCore.of(context).currentUser!.id, - ], - ), + Filter.in_('members', [StreamChatCore.of(context).currentUser!.id]), ]), ); @@ -103,91 +88,89 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: const Text('Channels'), - ), - body: PagedValueListenableBuilder( - valueListenable: channelListController, - builder: (context, value, child) { - return value.when( - (channels, nextPageKey, error) => LazyLoadScrollView( - onEndOfPage: () async { - if (nextPageKey != null) { - channelListController.loadMore(nextPageKey); + appBar: AppBar(title: const Text('Channels')), + body: PagedValueListenableBuilder( + valueListenable: channelListController, + builder: (context, value, child) { + return value.when( + (channels, nextPageKey, error) => LazyLoadScrollView( + onEndOfPage: () async { + if (nextPageKey != null) { + channelListController.loadMore(nextPageKey); + } + }, + child: ListView.builder( + /// We're using the channels length when there are no more + /// pages to load and there are no errors with pagination. + /// In case we need to show a loading indicator or and error + /// tile we're increasing the count by 1. + itemCount: (nextPageKey != null || error != null) + ? channels.length + 1 + : channels.length, + itemBuilder: (BuildContext context, int index) { + if (index == channels.length) { + if (error != null) { + return TextButton( + onPressed: () { + channelListController.retry(); + }, + child: Text(error.message), + ); } - }, - child: ListView.builder( - /// We're using the channels length when there are no more - /// pages to load and there are no errors with pagination. - /// In case we need to show a loading indicator or and error - /// tile we're increasing the count by 1. - itemCount: (nextPageKey != null || error != null) - ? channels.length + 1 - : channels.length, - itemBuilder: (BuildContext context, int index) { - if (index == channels.length) { - if (error != null) { - return TextButton( - onPressed: () { - channelListController.retry(); - }, - child: Text(error.message), - ); - } - return CircularProgressIndicator(); - } + return CircularProgressIndicator(); + } - final _item = channels[index]; - return ListTile( - title: Text(_item.name ?? ''), - subtitle: StreamBuilder( - stream: _item.state!.lastMessageStream, - initialData: _item.state!.lastMessage, - builder: (context, snapshot) { - if (snapshot.hasData) { - return Text(snapshot.data!.text!); - } + final _item = channels[index]; + return ListTile( + title: Text(_item.name ?? ''), + subtitle: StreamBuilder( + stream: _item.state!.lastMessageStream, + initialData: _item.state!.lastMessage, + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text(snapshot.data!.text!); + } - return const SizedBox(); - }, + return const SizedBox(); + }, + ), + onTap: () { + /// Display a list of messages when the user taps on + /// an item. We can use [StreamChannel] to wrap our + /// [MessageScreen] screen with the selected channel. + /// + /// This allows us to use a built-in inherited widget + /// for accessing our `channel` later on. + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: _item, + child: const MessageScreen(), + ), ), - onTap: () { - /// Display a list of messages when the user taps on - /// an item. We can use [StreamChannel] to wrap our - /// [MessageScreen] screen with the selected channel. - /// - /// This allows us to use a built-in inherited widget - /// for accessing our `channel` later on. - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: _item, - child: const MessageScreen(), - ), - ), - ); - }, ); }, - ), - ), - loading: () => const Center( - child: SizedBox( - height: 100, - width: 100, - child: CircularProgressIndicator(), - ), - ), - error: (e) => Center( - child: Text( - 'Oh no, something went wrong. ' - 'Please check your config. $e', - ), - ), - ); - }, - ), - ); + ); + }, + ), + ), + loading: () => const Center( + child: SizedBox( + height: 100, + width: 100, + child: CircularProgressIndicator(), + ), + ), + error: (e) => Center( + child: Text( + 'Oh no, something went wrong. ' + 'Please check your config. $e', + ), + ), + ); + }, + ), + ); } /// A list of messages sent in the current channel. @@ -259,9 +242,8 @@ class _MessageScreenState extends State { }, child: MessageListCore( messageListController: messageListController, - emptyBuilder: (BuildContext context) => const Center( - child: Text('Nothing here yet'), - ), + emptyBuilder: (BuildContext context) => + const Center(child: Text('Nothing here yet')), loadingBuilder: (BuildContext context) => const Center( child: SizedBox( height: 100, @@ -269,44 +251,43 @@ class _MessageScreenState extends State { child: CircularProgressIndicator(), ), ), - messageListBuilder: ( - BuildContext context, - List messages, - ) => - ListView.builder( - controller: _scrollController, - itemCount: messages.length, - reverse: true, - itemBuilder: (BuildContext context, int index) { - final item = messages[index]; - final client = StreamChatCore.of(context).client; - if (item.user!.id == client.uid) { - return Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(item.text!), + messageListBuilder: + (BuildContext context, List messages) => + ListView.builder( + controller: _scrollController, + itemCount: messages.length, + reverse: true, + itemBuilder: (BuildContext context, int index) { + final item = messages[index]; + final client = StreamChatCore.of(context).client; + if (item.user!.id == client.uid) { + return Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(item.text!), + ), + ); + } else { + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text(item.text!), + ), + ); + } + }, ), - ); - } else { - return Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(item.text!), - ), - ); - } - }, - ), errorBuilder: (BuildContext context, error) { print(error.toString()); return const Center( child: SizedBox( height: 100, width: 100, - child: - Text('Oh no, an error occured. Please see logs.'), + child: Text( + 'Oh no, an error occured. Please see logs.', + ), ), ); }, @@ -344,10 +325,7 @@ class _MessageScreenState extends State { child: const Padding( padding: EdgeInsets.all(8), child: Center( - child: Icon( - Icons.send, - color: Colors.white, - ), + child: Icon(Icons.send, color: Colors.white), ), ), ), diff --git a/packages/stream_chat_flutter_core/example/pubspec.yaml b/packages/stream_chat_flutter_core/example/pubspec.yaml index a2268fe974..bc19ead3aa 100644 --- a/packages/stream_chat_flutter_core/example/pubspec.yaml +++ b/packages/stream_chat_flutter_core/example/pubspec.yaml @@ -16,14 +16,14 @@ version: 1.0.0+1 # 2. Add it to the melos.yaml file for future updates. environment: - sdk: ^3.6.2 - flutter: ">=3.27.4" + sdk: ^3.10.0 + flutter: ">=3.38.1" dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat_flutter_core: ^9.23.0 + stream_chat_flutter_core: ^10.0.0-beta.13 flutter: uses-material-design: true diff --git a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart index 6305224b61..22344c81ef 100644 --- a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart +++ b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart @@ -40,8 +40,7 @@ class BetterStreamBuilder extends StatefulWidget { _BetterStreamBuilderState createState() => _BetterStreamBuilderState(); } -class _BetterStreamBuilderState - extends State> { +class _BetterStreamBuilderState extends State> { T? _lastEvent; StreamSubscription? _subscription; Object? _lastError; @@ -102,8 +101,7 @@ class _BetterStreamBuilderState void _onEvent(T? event) { _lastError = null; - final isEqual = - widget.comparator?.call(_lastEvent, event) ?? event == _lastEvent; + final isEqual = widget.comparator?.call(_lastEvent, event) ?? event == _lastEvent; if (!isEqual) { _lastEvent = event; if (mounted) { diff --git a/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart b/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart index d56d5f239a..1495a94c9b 100644 --- a/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart +++ b/packages/stream_chat_flutter_core/lib/src/lazy_load_scroll_view.dart @@ -53,11 +53,10 @@ class _LazyLoadScrollViewState extends State { double _scrollPosition = 0; @override - Widget build(BuildContext context) => - NotificationListener( - onNotification: _onNotification, - child: widget.child, - ); + Widget build(BuildContext context) => NotificationListener( + onNotification: _onNotification, + child: widget.child, + ); bool _onNotification(ScrollNotification notification) { if (notification is ScrollStartNotification) { @@ -78,8 +77,7 @@ class _LazyLoadScrollViewState extends State { final minScrollExtent = notification.metrics.minScrollExtent; final scrollOffset = widget.scrollOffset; - if (pixels > (minScrollExtent + scrollOffset) && - pixels < (maxScrollExtent - scrollOffset)) { + if (pixels > (minScrollExtent + scrollOffset) && pixels < (maxScrollExtent - scrollOffset)) { if (widget.onInBetweenOfPage != null) { widget.onInBetweenOfPage!(); return !widget.allowNotificationBubbling; diff --git a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart index 44b3c2619c..7a9b3d6c0e 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_list_core.dart @@ -9,12 +9,11 @@ import 'package:stream_chat_flutter_core/src/stream_channel.dart'; import 'package:stream_chat_flutter_core/src/typedef.dart'; /// Default filter for the message list -bool Function(Message) defaultMessageFilter(String currentUserId) => - (Message m) { - final isMyMessage = m.user?.id == currentUserId; - if (m.shadowed && !isMyMessage) return false; - return true; - }; +bool Function(Message) defaultMessageFilter(String currentUserId) => (Message m) { + final isMyMessage = m.user?.id == currentUserId; + if (m.shadowed && !isMyMessage) return false; + return true; +}; /// [MessageListCore] is a simplified class that allows fetching a list of /// messages while exposing UI builders. @@ -131,8 +130,8 @@ class MessageListCoreState extends State { Widget build(BuildContext context) { final messagesStream = _isThreadConversation ? _streamChannel!.channel.state?.threadsStream - .where((threads) => threads.containsKey(widget.parentMessage!.id)) - .map((threads) => threads[widget.parentMessage!.id]) + .where((threads) => threads.containsKey(widget.parentMessage!.id)) + .map((threads) => threads[widget.parentMessage!.id]) : _streamChannel!.channel.state?.messagesStream; final initialData = _isThreadConversation diff --git a/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart index ae730c06b8..f485b75c5a 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; /// A function that takes a [BuildContext] and returns a [TextStyle]. -typedef TextStyleBuilder = TextStyle? Function( - BuildContext context, - String text, -); +typedef TextStyleBuilder = + TextStyle? Function( + BuildContext context, + String text, + ); /// Controller for the [StreamTextField] widget. class MessageTextFieldController extends TextEditingController { diff --git a/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart index 83643a64d0..38bfa98756 100644 --- a/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart @@ -8,8 +8,7 @@ part 'paged_value_notifier.freezed.dart'; const defaultInitialPagedLimitMultiplier = 3; /// Value listenable for paged data. -typedef PagedValueListenableBuilder - = ValueListenableBuilder>; +typedef PagedValueListenableBuilder = ValueListenableBuilder>; /// A [PagedValueNotifier] that uses a [PagedListenable] to load data. /// @@ -19,8 +18,7 @@ typedef PagedValueListenableBuilder /// /// [PagedValueNotifier] is a [ValueNotifier] that emits a [PagedValue] /// whenever the data is loaded or an error occurs. -abstract class PagedValueNotifier - extends ValueNotifier> { +abstract class PagedValueNotifier extends ValueNotifier> { /// Creates a [PagedValueNotifier] PagedValueNotifier(this._initialValue) : super(_initialValue); @@ -148,17 +146,18 @@ extension PagedValuePatternMatching on PagedValue { List items, Key? nextPageKey, StreamChatError? error, - ) success, { + ) + success, { required TResult Function() loading, required TResult Function(StreamChatError error) error, }) { final pagedValue = this; return switch (pagedValue) { Success() => success( - pagedValue.items, - pagedValue.nextPageKey, - pagedValue.error, - ), + pagedValue.items, + pagedValue.nextPageKey, + pagedValue.error, + ), Loading() => loading(), Error() => error(pagedValue.error), }; @@ -171,17 +170,18 @@ extension PagedValuePatternMatching on PagedValue { List items, Key? nextPageKey, StreamChatError? error, - )? success, { + )? + success, { TResult? Function()? loading, TResult? Function(StreamChatError error)? error, }) { final pagedValue = this; return switch (pagedValue) { Success() => success?.call( - pagedValue.items, - pagedValue.nextPageKey, - pagedValue.error, - ), + pagedValue.items, + pagedValue.nextPageKey, + pagedValue.error, + ), Loading() => loading?.call(), Error() => error?.call(pagedValue.error), }; @@ -194,7 +194,8 @@ extension PagedValuePatternMatching on PagedValue { List items, Key? nextPageKey, StreamChatError? error, - )? success, { + )? + success, { TResult Function()? loading, TResult Function(StreamChatError error)? error, required TResult orElse(), @@ -202,10 +203,10 @@ extension PagedValuePatternMatching on PagedValue { final pagedValue = this; final result = switch (pagedValue) { Success() => success?.call( - pagedValue.items, - pagedValue.nextPageKey, - pagedValue.error, - ), + pagedValue.items, + pagedValue.nextPageKey, + pagedValue.error, + ), Loading() => loading?.call(), Error() => error?.call(pagedValue.error), }; diff --git a/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart index a078f471c7..ce60e68bb0 100644 --- a/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_scroll_view.dart @@ -5,18 +5,20 @@ import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; /// Signature for a function that creates a widget for a given index, e.g., in a /// [PagedValueListView] and [PagedValueGridView]. -typedef PagedValueScrollViewIndexedWidgetBuilder = Widget Function( - BuildContext context, - List values, - int index, -); +typedef PagedValueScrollViewIndexedWidgetBuilder = + Widget Function( + BuildContext context, + List values, + int index, + ); /// Signature for the item builder that creates the children of the /// [PagedValueListView] and [PagedValueGridView]. -typedef PagedValueScrollViewLoadMoreErrorBuilder = Widget Function( - BuildContext context, - StreamChatError error, -); +typedef PagedValueScrollViewLoadMoreErrorBuilder = + Widget Function( + BuildContext context, + StreamChatError error, + ); /// A [ListView] that loads more pages when the user scrolls to the end of the /// list. @@ -260,8 +262,7 @@ class PagedValueListView extends StatefulWidget { final Clip clipBehavior; @override - State> createState() => - _PagedValueListViewState(); + State> createState() => _PagedValueListViewState(); } class _PagedValueListViewState extends State> { @@ -288,65 +289,62 @@ class _PagedValueListViewState extends State> { @override Widget build(BuildContext context) => PagedValueListenableBuilder( - valueListenable: _controller, - builder: (context, value, _) => value.when( - (items, nextPageKey, error) { - if (items.isEmpty) { - return widget.emptyBuilder(context); - } - - return ListView.separated( - scrollDirection: widget.scrollDirection, - padding: widget.padding, - physics: widget.physics, - reverse: widget.reverse, - controller: widget.scrollController, - primary: widget.primary, - shrinkWrap: widget.shrinkWrap, - addAutomaticKeepAlives: widget.addAutomaticKeepAlives, - addRepaintBoundaries: widget.addRepaintBoundaries, - addSemanticIndexes: widget.addSemanticIndexes, - keyboardDismissBehavior: widget.keyboardDismissBehavior, - restorationId: widget.restorationId, - dragStartBehavior: widget.dragStartBehavior, - cacheExtent: widget.cacheExtent, - clipBehavior: widget.clipBehavior, - itemCount: value.itemCount, - separatorBuilder: (context, index) => - widget.separatorBuilder(context, items, index), - itemBuilder: (context, index) { - if (!_hasRequestedNextPage) { - final newPageRequestTriggerIndex = - items.length - widget.loadMoreTriggerIndex; - final isBuildingTriggerIndexItem = - index == newPageRequestTriggerIndex; - if (nextPageKey != null && isBuildingTriggerIndexItem) { - // Schedules the request for the end of this frame. - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (error == null) { - await _controller.loadMore(nextPageKey); - } - _hasRequestedNextPage = false; - }); - _hasRequestedNextPage = true; + valueListenable: _controller, + builder: (context, value, _) => value.when( + (items, nextPageKey, error) { + if (items.isEmpty) { + return widget.emptyBuilder(context); + } + + return ListView.separated( + scrollDirection: widget.scrollDirection, + padding: widget.padding, + physics: widget.physics, + reverse: widget.reverse, + controller: widget.scrollController, + primary: widget.primary, + shrinkWrap: widget.shrinkWrap, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + addRepaintBoundaries: widget.addRepaintBoundaries, + addSemanticIndexes: widget.addSemanticIndexes, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + restorationId: widget.restorationId, + dragStartBehavior: widget.dragStartBehavior, + cacheExtent: widget.cacheExtent, + clipBehavior: widget.clipBehavior, + itemCount: value.itemCount, + separatorBuilder: (context, index) => widget.separatorBuilder(context, items, index), + itemBuilder: (context, index) { + if (!_hasRequestedNextPage) { + final newPageRequestTriggerIndex = items.length - widget.loadMoreTriggerIndex; + final isBuildingTriggerIndexItem = index == newPageRequestTriggerIndex; + if (nextPageKey != null && isBuildingTriggerIndexItem) { + // Schedules the request for the end of this frame. + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (error == null) { + await _controller.loadMore(nextPageKey); } - } + _hasRequestedNextPage = false; + }); + _hasRequestedNextPage = true; + } + } - if (index == items.length) { - if (error != null) { - return widget.loadMoreErrorBuilder(context, error); - } - return widget.loadMoreIndicatorBuilder(context); - } + if (index == items.length) { + if (error != null) { + return widget.loadMoreErrorBuilder(context, error); + } + return widget.loadMoreIndicatorBuilder(context); + } - return widget.itemBuilder(context, items, index); - }, - ); + return widget.itemBuilder(context, items, index); }, - loading: () => widget.loadingBuilder(context), - error: (error) => widget.errorBuilder(context, error), - ), - ); + ); + }, + loading: () => widget.loadingBuilder(context), + error: (error) => widget.errorBuilder(context, error), + ), + ); } /// A [GridView] that loads more pages when the user scrolls to the end of the @@ -367,6 +365,7 @@ class PagedValueGridView extends StatefulWidget { required this.loadingBuilder, required this.errorBuilder, this.loadMoreTriggerIndex = 3, + this.leadingItemBuilder, this.scrollDirection = Axis.vertical, this.reverse = false, this.scrollController, @@ -415,6 +414,13 @@ class PagedValueGridView extends StatefulWidget { /// The index to take into account when triggering [controller.loadMore]. final int loadMoreTriggerIndex; + /// An optional builder for a single item prepended before the paged items. + /// + /// When provided, [itemBuilder] still receives regular item indices starting + /// at 0 — the leading item is handled separately, similar to + /// [loadMoreIndicatorBuilder]. + final WidgetBuilder? leadingItemBuilder; + /// {@template flutter.widgets.scroll_view.scrollDirection} /// The axis along which the scroll view scrolls. /// @@ -616,8 +622,7 @@ class PagedValueGridView extends StatefulWidget { final Clip clipBehavior; @override - State> createState() => - _PagedValueGridViewState(); + State> createState() => _PagedValueGridViewState(); } class _PagedValueGridViewState extends State> { @@ -644,63 +649,66 @@ class _PagedValueGridViewState extends State> { @override Widget build(BuildContext context) => PagedValueListenableBuilder( - valueListenable: _controller, - builder: (context, value, _) => value.when( - (items, nextPageKey, error) { - if (items.isEmpty) { - return widget.emptyBuilder(context); + valueListenable: _controller, + builder: (context, value, _) => value.when( + (items, nextPageKey, error) { + if (items.isEmpty) { + return widget.emptyBuilder(context); + } + + return GridView.builder( + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + controller: widget.scrollController, + primary: widget.primary, + physics: widget.physics, + shrinkWrap: widget.shrinkWrap, + padding: widget.padding, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + addRepaintBoundaries: widget.addRepaintBoundaries, + addSemanticIndexes: widget.addSemanticIndexes, + cacheExtent: widget.cacheExtent, + semanticChildCount: widget.semanticChildCount, + dragStartBehavior: widget.dragStartBehavior, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + restorationId: widget.restorationId, + clipBehavior: widget.clipBehavior, + itemCount: value.itemCount + (widget.leadingItemBuilder != null ? 1 : 0), + gridDelegate: widget.gridDelegate, + itemBuilder: (context, index) { + var adjustedIndex = index; + if (widget.leadingItemBuilder != null) { + if (index == 0) return widget.leadingItemBuilder!(context); + adjustedIndex = index - 1; } - return GridView.builder( - scrollDirection: widget.scrollDirection, - reverse: widget.reverse, - controller: widget.scrollController, - primary: widget.primary, - physics: widget.physics, - shrinkWrap: widget.shrinkWrap, - padding: widget.padding, - addAutomaticKeepAlives: widget.addAutomaticKeepAlives, - addRepaintBoundaries: widget.addRepaintBoundaries, - addSemanticIndexes: widget.addSemanticIndexes, - cacheExtent: widget.cacheExtent, - semanticChildCount: widget.semanticChildCount, - dragStartBehavior: widget.dragStartBehavior, - keyboardDismissBehavior: widget.keyboardDismissBehavior, - restorationId: widget.restorationId, - clipBehavior: widget.clipBehavior, - itemCount: value.itemCount, - gridDelegate: widget.gridDelegate, - itemBuilder: (context, index) { - if (!_hasRequestedNextPage) { - final newPageRequestTriggerIndex = - items.length - widget.loadMoreTriggerIndex; - final isBuildingTriggerIndexItem = - index == newPageRequestTriggerIndex; - if (nextPageKey != null && isBuildingTriggerIndexItem) { - // Schedules the request for the end of this frame. - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (error == null) { - await _controller.loadMore(nextPageKey); - } - _hasRequestedNextPage = false; - }); - _hasRequestedNextPage = true; + if (!_hasRequestedNextPage) { + final newPageRequestTriggerIndex = items.length - widget.loadMoreTriggerIndex; + if (nextPageKey != null && adjustedIndex == newPageRequestTriggerIndex) { + // Schedules the request for the end of this frame. + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (error == null) { + await _controller.loadMore(nextPageKey); } - } + _hasRequestedNextPage = false; + }); + _hasRequestedNextPage = true; + } + } - if (index == items.length) { - if (error != null) { - return widget.loadMoreErrorBuilder(context, error); - } - return widget.loadMoreIndicatorBuilder(context); - } + if (adjustedIndex == items.length) { + if (error != null) { + return widget.loadMoreErrorBuilder(context, error); + } + return widget.loadMoreIndicatorBuilder(context); + } - return widget.itemBuilder(context, items, index); - }, - ); + return widget.itemBuilder(context, items, adjustedIndex); }, - loading: () => widget.loadingBuilder(context), - error: (error) => widget.errorBuilder(context, error), - ), - ); + ); + }, + loading: () => widget.loadingBuilder(context), + error: (error) => widget.errorBuilder(context, error), + ), + ); } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index 77ef984bc9..ee6edd0c2b 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -17,11 +17,12 @@ enum QueryDirection { /// Signature used by [StreamChannel.errorBuilder] to create a replacement /// widget for an error that occurs while asynchronously building the channel. // TODO: Remove once ErrorBuilder supports passing stacktrace. -typedef ErrorWidgetBuilder = Widget Function( - BuildContext context, - Object error, - StackTrace? stackTrace, -); +typedef ErrorWidgetBuilder = + Widget Function( + BuildContext context, + Object error, + StackTrace? stackTrace, + ); Color _getDefaultBackgroundColor(BuildContext context) { final brightness = Theme.of(context).brightness; @@ -95,8 +96,7 @@ class StreamChannel extends StatefulWidget { color: backgroundColor, child: Center( child: switch (exception) { - DioException(type: DioExceptionType.badResponse) => - Text(exception.message ?? 'Bad response'), + DioException(type: DioExceptionType.badResponse) => Text(exception.message ?? 'Bad response'), DioException() => const Text('Check your connection and retry'), _ => Text(exception.toString()), }, @@ -180,8 +180,7 @@ class StreamChannelState extends State { String? get initialMessageId => widget.initialMessageId; /// Current channel state stream - Stream? get channelStateStream => - widget.channel.state?.channelStateStream; + Stream? get channelStateStream => widget.channel.state?.channelStateStream; final _queryTopMessagesController = BehaviorSubject.seeded(false); final _queryBottomMessagesController = BehaviorSubject.seeded(false); @@ -427,31 +426,30 @@ class StreamChannelState extends State { String? messageId, { int limit = 30, bool preferOffline = false, - }) => - _queryAtMessage( - messageId: messageId, - limit: limit, - preferOffline: preferOffline, - ); + }) => _queryAtMessage( + messageId: messageId, + limit: limit, + preferOffline: preferOffline, + ); /// Loads channel at specific message Future loadChannelAtTimestamp( DateTime timestamp, { int limit = 30, bool preferOffline = false, - }) => - _queryAtTimestamp( - timestamp: timestamp, - limit: limit, - preferOffline: preferOffline, - ); + }) => _queryAtTimestamp( + timestamp: timestamp, + limit: limit, + preferOffline: preferOffline, + ); // If we are jumping to a message we can determine if we loaded the oldest // page or the newest page, depending on where the aroundMessageId is located. ({ bool endOfPrependReached, bool endOfAppendReached, - }) _inferBoundariesFromAnchorId( + }) + _inferBoundariesFromAnchorId( String anchorId, List loadedMessages, ) { @@ -539,7 +537,8 @@ class StreamChannelState extends State { ({ bool endOfPrependReached, bool endOfAppendReached, - }) _inferBoundariesFromAnchorTimestamp( + }) + _inferBoundariesFromAnchorTimestamp( DateTime anchorTimestamp, List loadedMessages, ) { @@ -564,9 +563,11 @@ class StreamChannelState extends State { DateTime anchorTimestamp, List loadedMessages, ) { - final messageTimestamps = loadedMessages.map((it) { - return it.createdAt.millisecondsSinceEpoch; - }).toList(growable: false); + final messageTimestamps = loadedMessages + .map((it) { + return it.createdAt.millisecondsSinceEpoch; + }) + .toList(growable: false); return messageTimestamps.lowerBoundBy( anchorTimestamp.millisecondsSinceEpoch, @@ -820,8 +821,7 @@ class StreamChannelState extends State { @override void didUpdateWidget(StreamChannel oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.channel.cid != widget.channel.cid || - oldWidget.initialMessageId != widget.initialMessageId) { + if (oldWidget.channel.cid != widget.channel.cid || oldWidget.initialMessageId != widget.initialMessageId) { // Re-initialize channel if the channel CID or initial message ID changes. _channelInitFuture = [_maybeInitChannel(), channel.initialized].wait; } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart index db2ff2c4c6..bd671a0bcc 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -55,8 +55,8 @@ class StreamChannelListController extends PagedValueNotifier { this.limit = defaultChannelPagedLimit, this.messageLimit, this.memberLimit, - }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(), - super(const PagedValue.loading()); + }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(), + super(const PagedValue.loading()); /// Creates a [StreamChannelListController] from the passed [value]. StreamChannelListController.fromValue( @@ -113,14 +113,14 @@ class StreamChannelListController extends PagedValueNotifier { super.value = switch (channelStateSort) { null => newValue, final channelSort => newValue.maybeMap( - orElse: () => newValue, - (success) => success.copyWith( - items: success.items.sortedByCompare( - (it) => it.state!.channelState, - channelSort.compare, - ), + orElse: () => newValue, + (success) => success.copyWith( + items: success.items.sortedByCompare( + (it) => it.state!.channelState, + channelSort.compare, ), ), + ), }; } @@ -269,8 +269,7 @@ class StreamChannelListController extends PagedValueNotifier { _eventHandler.onNotificationMessageNew(event, this); } else if (eventType == EventType.notificationRemovedFromChannel) { _eventHandler.onNotificationRemovedFromChannel(event, this); - } else if (eventType == 'user.presence.changed' || - eventType == EventType.userUpdated) { + } else if (eventType == 'user.presence.changed' || eventType == EventType.userUpdated) { _eventHandler.onUserPresenceChanged(event, this); } else if (eventType == EventType.memberUpdated) { _eventHandler.onMemberUpdated(event, this); diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart index b59d16e67c..59a21f13c4 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart @@ -171,8 +171,7 @@ mixin class StreamChannelListEventHandler { StreamChannelListController controller, ) { final channels = [...controller.currentItems]; - final updatedChannels = - channels.where((it) => it.cid != event.channel?.cid); + final updatedChannels = channels.where((it) => it.cid != event.channel?.cid); final listChanged = channels.length != updatedChannels.length; if (!listChanged) return; diff --git a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart index 252ffee341..44d77f156b 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_chat_core.dart @@ -137,8 +137,7 @@ class StreamChatCore extends StatefulWidget { } /// State class associated with [StreamChatCore]. -class StreamChatCoreState extends State - with WidgetsBindingObserver { +class StreamChatCoreState extends State with WidgetsBindingObserver { /// The current user User? get currentUser => client.state.currentUser; @@ -168,15 +167,13 @@ class StreamChatCoreState extends State case PlatformType.macOS: final info = await DeviceInfoPlugin().macOsInfo; - osVersion = [info.majorVersion, info.minorVersion, info.patchVersion] - .join('.'); + osVersion = [info.majorVersion, info.minorVersion, info.patchVersion].join('.'); deviceModel = info.model; break; case PlatformType.windows: final info = await DeviceInfoPlugin().windowsInfo; - osVersion = [info.majorVersion, info.minorVersion, info.buildNumber] - .join('.'); + osVersion = [info.majorVersion, info.minorVersion, info.buildNumber].join('.'); deviceModel = null; break; diff --git a/packages/stream_chat_flutter_core/lib/src/stream_draft_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_draft_list_controller.dart index 8845ec93f1..cebe6c965a 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_draft_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_draft_list_controller.dart @@ -34,10 +34,10 @@ class StreamDraftListController extends PagedValueNotifier { this.filter, this.sort = defaultDraftListSort, this.limit = defaultDraftPagedLimit, - }) : _activeFilter = filter, - _activeSort = sort, - _eventHandler = eventHandler ?? StreamDraftListEventHandler(), - super(const PagedValue.loading()); + }) : _activeFilter = filter, + _activeSort = sort, + _eventHandler = eventHandler ?? StreamDraftListEventHandler(), + super(const PagedValue.loading()); /// Creates a [StreamThreadListController] from the passed [value]. StreamDraftListController.fromValue( @@ -47,9 +47,9 @@ class StreamDraftListController extends PagedValueNotifier { this.filter, this.sort = defaultDraftListSort, this.limit = defaultDraftPagedLimit, - }) : _activeFilter = filter, - _activeSort = sort, - _eventHandler = eventHandler ?? StreamDraftListEventHandler(); + }) : _activeFilter = filter, + _activeSort = sort, + _eventHandler = eventHandler ?? StreamDraftListEventHandler(); /// The Stream client used to perform the queries. final StreamChatClient client; @@ -94,11 +94,11 @@ class StreamDraftListController extends PagedValueNotifier { super.value = switch (_activeSort) { null => newValue, final draftSort => newValue.maybeMap( - orElse: () => newValue, - (success) => success.copyWith( - items: success.items.sorted(draftSort.compare), - ), + orElse: () => newValue, + (success) => success.copyWith( + items: success.items.sorted(draftSort.compare), ), + ), }; } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart index 16185685d9..0bc07a4c43 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_member_list_controller.dart @@ -36,9 +36,9 @@ class StreamMemberListController extends PagedValueNotifier { this.filter, this.sort = defaultMemberListSort, this.limit = defaultMemberPagedLimit, - }) : _activeFilter = filter, - _activeSort = sort, - super(const PagedValue.loading()); + }) : _activeFilter = filter, + _activeSort = sort, + super(const PagedValue.loading()); /// Creates a [StreamMemberListController] from the passed [value]. StreamMemberListController.fromValue( @@ -47,8 +47,8 @@ class StreamMemberListController extends PagedValueNotifier { this.filter, this.sort = defaultMemberListSort, this.limit = defaultMemberPagedLimit, - }) : _activeFilter = filter, - _activeSort = sort; + }) : _activeFilter = filter, + _activeSort = sort; /// The client to use for the channels list. final Channel channel; @@ -97,11 +97,11 @@ class StreamMemberListController extends PagedValueNotifier { super.value = switch (_activeSort) { null => newValue, final memberSort => newValue.maybeMap( - orElse: () => newValue, - (success) => success.copyWith( - items: success.items.sorted(memberSort.compare), - ), + orElse: () => newValue, + (success) => success.copyWith( + items: success.items.sorted(memberSort.compare), ), + ), }; } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart index 478fdacce7..f2f6708d61 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_message_input_controller.dart @@ -23,42 +23,39 @@ class StreamMessageInputController extends ValueNotifier { factory StreamMessageInputController({ Message? message, Map? textPatternStyle, - }) => - StreamMessageInputController._( - initialMessage: message ?? Message(), - textPatternStyle: textPatternStyle, - ); + }) => StreamMessageInputController._( + initialMessage: message ?? Message(), + textPatternStyle: textPatternStyle, + ); /// Creates a controller for an editable text field from an initial [text]. factory StreamMessageInputController.fromText( String? text, { Map? textPatternStyle, - }) => - StreamMessageInputController._( - initialMessage: Message(text: text), - textPatternStyle: textPatternStyle, - ); + }) => StreamMessageInputController._( + initialMessage: Message(text: text), + textPatternStyle: textPatternStyle, + ); /// Creates a controller for an editable text field from initial /// [attachments]. factory StreamMessageInputController.fromAttachments( List attachments, { Map? textPatternStyle, - }) => - StreamMessageInputController._( - initialMessage: Message(attachments: attachments), - textPatternStyle: textPatternStyle, - ); + }) => StreamMessageInputController._( + initialMessage: Message(attachments: attachments), + textPatternStyle: textPatternStyle, + ); StreamMessageInputController._({ required Message initialMessage, Map? textPatternStyle, - }) : _initialMessage = initialMessage, - _textFieldController = MessageTextFieldController.fromValue( - _textEditingValueFromMessage(initialMessage), - textPatternStyle: textPatternStyle, - ), - super(initialMessage) { + }) : _initialMessage = initialMessage, + _textFieldController = MessageTextFieldController.fromValue( + _textEditingValueFromMessage(initialMessage), + textPatternStyle: textPatternStyle, + ), + super(initialMessage) { _textFieldController.addListener(_textFieldListener); } @@ -185,7 +182,7 @@ class StreamMessageInputController extends ValueNotifier { } /// Sets a command for the message. - set command(String command) { + set command(String? command) { // Setting the command should also clear the text and attachments. message = message.copyWith( text: '', @@ -318,6 +315,40 @@ class StreamMessageInputController extends ValueNotifier { message = Message(); } + /// The original message being edited, before any user changes. + /// + /// This is set by [editMessage] and cleared by [cancelEditMessage]. + /// Use this to display a stable preview of the original message while the + /// user is typing their edits. + Message? get editingOriginalMessage => _editingOriginalMessage; + Message? _editingOriginalMessage; + + Message? _preEditMessage; + + /// Sets the controller to edit an existing [message]. + /// + /// Stores a snapshot of [message] in [editingOriginalMessage] so the + /// original content stays visible while the user types. + /// Saves the current composer state so [cancelEditMessage] can restore it. + void editMessage(Message message) { + _preEditMessage = this.message; + _editingOriginalMessage = message; + this.message = message.copyWith(state: MessageState.updating); + } + + /// Cancels the current edit and restores the composer to the state it was + /// in before editing began. + void cancelEditMessage() { + _editingOriginalMessage = null; + if (_preEditMessage != null) { + message = _preEditMessage!; + _preEditMessage = null; + } else { + _initialMessage = Message(); + reset(); + } + } + /// Sets the [message] to the initial [Message] value. void reset({bool resetId = true}) { if (resetId) { @@ -347,14 +378,12 @@ class StreamMessageInputController extends ValueNotifier { /// the property will restore [StreamMessageInputController.message] /// to the value it had when the restoration data it is getting restored from /// was collected. -class StreamRestorableMessageInputController - extends RestorableChangeNotifier { +class StreamRestorableMessageInputController extends RestorableChangeNotifier { /// Creates a [StreamRestorableMessageInputController]. /// /// This constructor creates a default [Message] when no `message` argument /// is supplied. - StreamRestorableMessageInputController({Message? message}) - : _initialValue = message ?? Message(); + StreamRestorableMessageInputController({Message? message}) : _initialValue = message ?? Message(); /// Creates a [StreamRestorableMessageInputController] from an initial /// [text] value. @@ -364,8 +393,7 @@ class StreamRestorableMessageInputController final Message _initialValue; @override - StreamMessageInputController createDefaultValue() => - StreamMessageInputController(message: _initialValue); + StreamMessageInputController createDefaultValue() => StreamMessageInputController(message: _initialValue); @override StreamMessageInputController fromPrimitives(Object? data) { diff --git a/packages/stream_chat_flutter_core/lib/src/stream_message_reminder_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_message_reminder_list_controller.dart index a2fa4f156e..242bd9c4b1 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_message_reminder_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_message_reminder_list_controller.dart @@ -29,8 +29,7 @@ const _kDefaultBackendPaginationLimit = 30; /// This controller is typically used in conjunction with UI components /// to display and interact with a list of message reminders. /// {@endtemplate} -class StreamMessageReminderListController - extends PagedValueNotifier { +class StreamMessageReminderListController extends PagedValueNotifier { /// {@macro streamMessageReminderListController} StreamMessageReminderListController({ required this.client, @@ -38,10 +37,10 @@ class StreamMessageReminderListController this.filter, this.sort = defaultMessageReminderListSort, this.limit = defaultMessageReminderPagedLimit, - }) : _activeFilter = filter, - _activeSort = sort, - _eventHandler = eventHandler ?? StreamMessageReminderListEventHandler(), - super(const PagedValue.loading()); + }) : _activeFilter = filter, + _activeSort = sort, + _eventHandler = eventHandler ?? StreamMessageReminderListEventHandler(), + super(const PagedValue.loading()); /// Creates a [StreamMessageReminderListController] from the passed [value]. StreamMessageReminderListController.fromValue( @@ -51,9 +50,9 @@ class StreamMessageReminderListController this.filter, this.sort = defaultMessageReminderListSort, this.limit = defaultMessageReminderPagedLimit, - }) : _activeFilter = filter, - _activeSort = sort, - _eventHandler = eventHandler ?? StreamMessageReminderListEventHandler(); + }) : _activeFilter = filter, + _activeSort = sort, + _eventHandler = eventHandler ?? StreamMessageReminderListEventHandler(); /// The Stream client used to perform the queries. final StreamChatClient client; @@ -98,11 +97,11 @@ class StreamMessageReminderListController super.value = switch (_activeSort) { null => newValue, final reminderSort => newValue.maybeMap( - orElse: () => newValue, - (success) => success.copyWith( - items: success.items.sorted(reminderSort.compare), - ), + orElse: () => newValue, + (success) => success.copyWith( + items: success.items.sorted(reminderSort.compare), ), + ), }; } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart index 5f8b369c70..3666f7d0f4 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_message_search_list_controller.dart @@ -15,8 +15,7 @@ const _kDefaultBackendPaginationLimit = 30; /// * Load initial data. /// * Load more data using [loadMore]. /// * Replace the previously loaded users. -class StreamMessageSearchListController - extends PagedValueNotifier { +class StreamMessageSearchListController extends PagedValueNotifier { /// Creates a Stream user list controller. /// /// * `client` is the Stream chat client to use for the channels list. @@ -36,19 +35,19 @@ class StreamMessageSearchListController this.searchQuery, this.sort, this.limit = defaultMessageSearchPagedLimit, - }) : assert( - messageFilter != null || searchQuery != null, - 'Either messageFilter or searchQuery must be provided', - ), - assert( - messageFilter == null || searchQuery == null, - 'Only one of messageFilter or searchQuery can be provided', - ), - _activeFilter = filter, - _activeMessageFilter = messageFilter, - _activeSearchQuery = searchQuery, - _activeSort = sort, - super(const PagedValue.loading()); + }) : assert( + messageFilter != null || searchQuery != null, + 'Either messageFilter or searchQuery must be provided', + ), + assert( + messageFilter == null || searchQuery == null, + 'Only one of messageFilter or searchQuery can be provided', + ), + _activeFilter = filter, + _activeMessageFilter = messageFilter, + _activeSearchQuery = searchQuery, + _activeSort = sort, + super(const PagedValue.loading()); /// Creates a [StreamUserListController] from the passed [value]. StreamMessageSearchListController.fromValue( @@ -59,18 +58,18 @@ class StreamMessageSearchListController this.searchQuery, this.sort, this.limit = defaultMessageSearchPagedLimit, - }) : assert( - messageFilter != null || searchQuery != null, - 'Either messageFilter or searchQuery must be provided', - ), - assert( - messageFilter == null || searchQuery == null, - 'Only one of messageFilter or searchQuery can be provided', - ), - _activeFilter = filter, - _activeMessageFilter = messageFilter, - _activeSearchQuery = searchQuery, - _activeSort = sort; + }) : assert( + messageFilter != null || searchQuery != null, + 'Either messageFilter or searchQuery must be provided', + ), + assert( + messageFilter == null || searchQuery == null, + 'Only one of messageFilter or searchQuery can be provided', + ), + _activeFilter = filter, + _activeMessageFilter = messageFilter, + _activeSearchQuery = searchQuery, + _activeSort = sort; /// The client to use for the channels list. final StreamChatClient client; diff --git a/packages/stream_chat_flutter_core/lib/src/stream_poll_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_poll_controller.dart index 749fb77e0e..d63f7c3569 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_poll_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_poll_controller.dart @@ -53,11 +53,14 @@ class StreamPollController extends ValueNotifier { factory StreamPollController({ Poll? poll, PollConfig? config, - }) => - StreamPollController._( - config ?? const PollConfig(), - poll ?? Poll(name: '', options: const [PollOption(text: '')]), - ); + }) => StreamPollController._( + config ?? const PollConfig(), + poll ?? + Poll( + name: '', + options: const [PollOption(text: '')], + ), + ); StreamPollController._(this.config, super.poll) : _initialValue = poll; @@ -84,7 +87,7 @@ class StreamPollController extends ValueNotifier { // Remove the id from the new added options. return option.copyWith(id: null); - }) + }), ], ); } @@ -122,8 +125,7 @@ class StreamPollController extends ValueNotifier { final name = value.name; final (:min, :max) = nameRange; - if (min != null && name.length < min || - max != null && name.length > max) { + if (min != null && name.length < min || max != null && name.length > max) { invalidErrors.add( PollValidationError.nameRange(name, range: nameRange), ); @@ -147,8 +149,7 @@ class StreamPollController extends ValueNotifier { final nonEmptyOptions = [...options.where((it) => it.text.isNotEmpty)]; final (:min, :max) = optionsRange; - if (min != null && nonEmptyOptions.length < min || - max != null && nonEmptyOptions.length > max) { + if (min != null && nonEmptyOptions.length < min || max != null && nonEmptyOptions.length > max) { invalidErrors.add( PollValidationError.optionsRange(options, range: optionsRange), ); @@ -161,8 +162,7 @@ class StreamPollController extends ValueNotifier { if (config.allowedVotesRange case final allowedVotesRange?) { final (:min, :max) = allowedVotesRange; - if (min != null && maxVotesAllowed < min || - max != null && maxVotesAllowed > max) { + if (min != null && maxVotesAllowed < min || max != null && maxVotesAllowed > max) { invalidErrors.add( PollValidationError.maxVotesAllowed( maxVotesAllowed, @@ -292,19 +292,15 @@ extension PollValidationErrorPatternMatching on PollValidationError { TResult when({ required TResult Function(List options) duplicateOptions, required TResult Function(String name, Range range) nameRange, - required TResult Function(List options, Range range) - optionsRange, - required TResult Function(int maxVotesAllowed, Range range) - maxVotesAllowed, + required TResult Function(List options, Range range) optionsRange, + required TResult Function(int maxVotesAllowed, Range range) maxVotesAllowed, }) { final error = this; return switch (error) { _PollValidationErrorDuplicateOptions() => duplicateOptions(error.options), _PollValidationErrorNameRange() => nameRange(error.name, error.range), - _PollValidationErrorOptionsRange() => - optionsRange(error.options, error.range), - _PollValidationErrorMaxVotesAllowed() => - maxVotesAllowed(error.maxVotesAllowed, error.range), + _PollValidationErrorOptionsRange() => optionsRange(error.options, error.range), + _PollValidationErrorMaxVotesAllowed() => maxVotesAllowed(error.maxVotesAllowed, error.range), }; } @@ -318,14 +314,10 @@ extension PollValidationErrorPatternMatching on PollValidationError { }) { final error = this; return switch (error) { - _PollValidationErrorDuplicateOptions() => - duplicateOptions?.call(error.options), - _PollValidationErrorNameRange() => - nameRange?.call(error.name, error.range), - _PollValidationErrorOptionsRange() => - optionsRange?.call(error.options, error.range), - _PollValidationErrorMaxVotesAllowed() => - maxVotesAllowed?.call(error.maxVotesAllowed, error.range), + _PollValidationErrorDuplicateOptions() => duplicateOptions?.call(error.options), + _PollValidationErrorNameRange() => nameRange?.call(error.name, error.range), + _PollValidationErrorOptionsRange() => optionsRange?.call(error.options, error.range), + _PollValidationErrorMaxVotesAllowed() => maxVotesAllowed?.call(error.maxVotesAllowed, error.range), }; } @@ -340,14 +332,10 @@ extension PollValidationErrorPatternMatching on PollValidationError { }) { final error = this; final result = switch (error) { - _PollValidationErrorDuplicateOptions() => - duplicateOptions?.call(error.options), - _PollValidationErrorNameRange() => - nameRange?.call(error.name, error.range), - _PollValidationErrorOptionsRange() => - optionsRange?.call(error.options, error.range), - _PollValidationErrorMaxVotesAllowed() => - maxVotesAllowed?.call(error.maxVotesAllowed, error.range), + _PollValidationErrorDuplicateOptions() => duplicateOptions?.call(error.options), + _PollValidationErrorNameRange() => nameRange?.call(error.name, error.range), + _PollValidationErrorOptionsRange() => optionsRange?.call(error.options, error.range), + _PollValidationErrorMaxVotesAllowed() => maxVotesAllowed?.call(error.maxVotesAllowed, error.range), }; return result ?? orElse(); @@ -356,13 +344,10 @@ extension PollValidationErrorPatternMatching on PollValidationError { /// @nodoc @optionalTypeArgs TResult map({ - required TResult Function(_PollValidationErrorDuplicateOptions value) - duplicateOptions, + required TResult Function(_PollValidationErrorDuplicateOptions value) duplicateOptions, required TResult Function(_PollValidationErrorNameRange value) nameRange, - required TResult Function(_PollValidationErrorOptionsRange value) - optionsRange, - required TResult Function(_PollValidationErrorMaxVotesAllowed value) - maxVotesAllowed, + required TResult Function(_PollValidationErrorOptionsRange value) optionsRange, + required TResult Function(_PollValidationErrorMaxVotesAllowed value) maxVotesAllowed, }) { final error = this; return switch (error) { @@ -376,12 +361,10 @@ extension PollValidationErrorPatternMatching on PollValidationError { /// @nodoc @optionalTypeArgs TResult? mapOrNull({ - TResult? Function(_PollValidationErrorDuplicateOptions value)? - duplicateOptions, + TResult? Function(_PollValidationErrorDuplicateOptions value)? duplicateOptions, TResult? Function(_PollValidationErrorNameRange value)? nameRange, TResult? Function(_PollValidationErrorOptionsRange value)? optionsRange, - TResult? Function(_PollValidationErrorMaxVotesAllowed value)? - maxVotesAllowed, + TResult? Function(_PollValidationErrorMaxVotesAllowed value)? maxVotesAllowed, }) { final error = this; return switch (error) { @@ -395,12 +378,10 @@ extension PollValidationErrorPatternMatching on PollValidationError { /// @nodoc @optionalTypeArgs TResult maybeMap({ - TResult Function(_PollValidationErrorDuplicateOptions value)? - duplicateOptions, + TResult Function(_PollValidationErrorDuplicateOptions value)? duplicateOptions, TResult Function(_PollValidationErrorNameRange value)? nameRange, TResult Function(_PollValidationErrorOptionsRange value)? optionsRange, - TResult Function(_PollValidationErrorMaxVotesAllowed value)? - maxVotesAllowed, + TResult Function(_PollValidationErrorMaxVotesAllowed value)? maxVotesAllowed, required TResult orElse(), }) { final error = this; diff --git a/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart index 3b29d687dc..a55d48fbd5 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_poll_vote_list_controller.dart @@ -21,8 +21,7 @@ const _kDefaultBackendPaginationLimit = 30; /// * Load initial data. /// * Load more data using [loadMore]. /// * Replace the previously loaded poll votes. -class StreamPollVoteListController - extends PagedValueNotifier { +class StreamPollVoteListController extends PagedValueNotifier { /// Creates a Stream poll vote list controller. /// * `channel` is the Stream chat channel to use for the poll votes list. /// * `pollId` is the poll id to use for the poll votes list. @@ -36,10 +35,10 @@ class StreamPollVoteListController this.filter, this.sort = defaultPollVoteListSort, this.limit = defaultPollVotePagedLimit, - }) : _eventHandler = eventHandler ?? StreamPollVoteEventHandler(), - _activeFilter = filter, - _activeSort = sort, - super(const PagedValue.loading()); + }) : _eventHandler = eventHandler ?? StreamPollVoteEventHandler(), + _activeFilter = filter, + _activeSort = sort, + super(const PagedValue.loading()); /// Creates a [StreamPollVoteListController] from the passed [value]. StreamPollVoteListController.fromValue( @@ -50,9 +49,9 @@ class StreamPollVoteListController this.filter, this.sort = defaultPollVoteListSort, this.limit = defaultPollVotePagedLimit, - }) : _eventHandler = eventHandler ?? StreamPollVoteEventHandler(), - _activeFilter = filter, - _activeSort = sort; + }) : _eventHandler = eventHandler ?? StreamPollVoteEventHandler(), + _activeFilter = filter, + _activeSort = sort; /// The channel to use for the poll votes list. final Channel channel; @@ -106,11 +105,11 @@ class StreamPollVoteListController super.value = switch (_activeSort) { null => newValue, final pollVoteSort => newValue.maybeMap( - orElse: () => newValue, - (success) => success.copyWith( - items: success.items.sorted(pollVoteSort.compare), - ), + orElse: () => newValue, + (success) => success.copyWith( + items: success.items.sorted(pollVoteSort.compare), ), + ), }; } @@ -216,13 +215,11 @@ class StreamPollVoteListController if (eventListener?.call(event) ?? false) return; final eventType = event.type; - if (eventType == EventType.pollVoteCasted || - eventType == EventType.pollAnswerCasted) { + if (eventType == EventType.pollVoteCasted || eventType == EventType.pollAnswerCasted) { _eventHandler.onPollVoteCasted(event, this); } else if (eventType == EventType.pollVoteChanged) { _eventHandler.onPollVoteChanged(event, this); - } else if (eventType == EventType.pollVoteRemoved || - eventType == EventType.pollAnswerRemoved) { + } else if (eventType == EventType.pollVoteRemoved || eventType == EventType.pollAnswerRemoved) { _eventHandler.onPollVoteRemoved(event, this); } }); diff --git a/packages/stream_chat_flutter_core/lib/src/stream_reaction_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_reaction_list_controller.dart new file mode 100644 index 0000000000..2e7dc0dda6 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/stream_reaction_list_controller.dart @@ -0,0 +1,182 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; + +/// The default reaction list page limit to load. +const defaultReactionPagedLimit = 25; + +const _kDefaultBackendPaginationLimit = 30; + +/// {@template streamReactionListController} +/// A controller for managing and displaying a paginated list of reactions. +/// +/// The `StreamReactionListController` extends [PagedValueNotifier] to handle +/// paginated data for reactions. It provides functionality for querying +/// reactions and managing filters and sorting. +/// +/// This controller uses cursor-based pagination via the `queryReactions` API, +/// which supports filtering by reaction type, user ID, or creation date. +/// +/// This controller is typically used in conjunction with UI components +/// to display and interact with a list of reactions for a message. +/// {@endtemplate} +class StreamReactionListController extends PagedValueNotifier { + /// {@macro streamReactionListController} + StreamReactionListController({ + required this.client, + required this.messageId, + this.filter, + this.sort, + this.limit = defaultReactionPagedLimit, + }) : _activeFilter = filter, + _activeSort = sort, + super(const PagedValue.loading()); + + /// Creates a [StreamReactionListController] from the passed [value]. + StreamReactionListController.fromValue( + super.value, { + required this.client, + required this.messageId, + this.filter, + this.sort, + this.limit = defaultReactionPagedLimit, + }) : _activeFilter = filter, + _activeSort = sort; + + /// The Stream chat client used to query reactions. + final StreamChatClient client; + + /// The ID of the message whose reactions are being listed. + final String messageId; + + /// The query filters to use. + /// + /// Supported filter fields: `type`, `user_id`, `created_at`. + final Filter? filter; + Filter? _activeFilter; + + /// The sorting used for the reactions matching the filters. + /// + /// Sorting is based on field and direction. The only backend-supported sort + /// field is `created_at` (see [ReactionSortKey]). + /// + /// Direction can be ascending or descending. + final SortOrder? sort; + SortOrder? _activeSort; + + /// The limit to apply to the reaction list. + /// + /// The default is set to [defaultReactionPagedLimit]. + final int limit; + + /// Allows for the change of filters used for reaction queries. + /// + /// Use this if you need to support runtime filter changes, + /// such as switching between reaction type tabs. + /// + /// Note: This will not trigger a new query. Make sure to call + /// [doInitialLoad] or [refresh] after setting a new filter. + set filter(Filter? value) => _activeFilter = value; + + /// Allows for the change of the query sort used for reaction queries. + /// + /// Use this if you need to support runtime sort changes, + /// through custom sort UI. + /// + /// Note: This will not trigger a new query. Make sure to call + /// [doInitialLoad] or [refresh] after setting a new sort. + set sort(SortOrder? value) => _activeSort = value; + + @override + set value(PagedValue newValue) { + super.value = switch (_activeSort) { + null => newValue, + final reactionSort => newValue.maybeMap( + orElse: () => newValue, + (success) => success.copyWith( + items: success.items.sorted(reactionSort.compare), + ), + ), + }; + } + + @override + Future doInitialLoad() async { + final limit = min( + this.limit * defaultInitialPagedLimitMultiplier, + _kDefaultBackendPaginationLimit, + ); + try { + final response = await client.queryReactions( + messageId, + filter: _activeFilter, + sort: _activeSort, + pagination: PaginationParams(limit: limit), + ); + + final reactions = response.reactions; + final next = response.next; + final nextKey = next != null && next.isNotEmpty ? next : null; + value = PagedValue( + items: reactions, + nextPageKey: nextKey, + ); + } on StreamChatError catch (error) { + value = PagedValue.error(error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = PagedValue.error(chatError); + } + } + + @override + Future loadMore(String? nextPageKey) async { + final previousValue = value.asSuccess; + + try { + final response = await client.queryReactions( + messageId, + filter: _activeFilter, + sort: _activeSort, + pagination: PaginationParams(limit: limit, next: nextPageKey), + ); + + final reactions = response.reactions; + final previousItems = previousValue.items; + final newItems = previousItems + reactions; + final next = response.next; + final nextKey = next != null && next.isNotEmpty ? next : null; + value = PagedValue( + items: newItems, + nextPageKey: nextKey, + ); + } on StreamChatError catch (error) { + value = previousValue.copyWith(error: error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = previousValue.copyWith(error: chatError); + } + } + + @override + Future refresh({bool resetValue = true}) { + if (resetValue) { + _activeFilter = filter; + _activeSort = sort; + } + return super.refresh(resetValue: resetValue); + } + + /// Replaces the previously loaded reactions with [reactions]. + set reactions(List reactions) { + if (value.isSuccess) { + final currentValue = value.asSuccess; + value = currentValue.copyWith(items: reactions); + } else { + value = PagedValue(items: reactions); + } + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/stream_thread_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_thread_list_controller.dart index fe30762ed7..691c4e7f2b 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_thread_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_thread_list_controller.dart @@ -29,11 +29,11 @@ class StreamThreadListController extends PagedValueNotifier { this.sort, this.options = const ThreadOptions(), this.limit = defaultThreadsPagedLimit, - }) : _activeFilter = filter, - _activeSort = sort, - _activeOptions = options, - _eventHandler = eventHandler ?? StreamThreadListEventHandler(), - super(const PagedValue.loading()); + }) : _activeFilter = filter, + _activeSort = sort, + _activeOptions = options, + _eventHandler = eventHandler ?? StreamThreadListEventHandler(), + super(const PagedValue.loading()); /// Creates a [StreamThreadListController] from the passed [value]. StreamThreadListController.fromValue( @@ -44,10 +44,10 @@ class StreamThreadListController extends PagedValueNotifier { this.sort, this.options = const ThreadOptions(), this.limit = defaultThreadsPagedLimit, - }) : _activeFilter = filter, - _activeSort = sort, - _activeOptions = options, - _eventHandler = eventHandler ?? StreamThreadListEventHandler(); + }) : _activeFilter = filter, + _activeSort = sort, + _activeOptions = options, + _eventHandler = eventHandler ?? StreamThreadListEventHandler(); /// The Stream client used to perform the queries. final StreamChatClient client; @@ -129,11 +129,11 @@ class StreamThreadListController extends PagedValueNotifier { super.value = switch (_activeSort) { null => newValue, final threadSort => newValue.maybeMap( - orElse: () => newValue, - (success) => success.copyWith( - items: success.items.sorted(threadSort.compare), - ), + orElse: () => newValue, + (success) => success.copyWith( + items: success.items.sorted(threadSort.compare), ), + ), }; } @@ -338,11 +338,9 @@ class StreamThreadListController extends PagedValueNotifier { final handlerFunc = switch (event.type) { EventType.threadUpdated => _eventHandler.onThreadUpdated, EventType.connectionRecovered => _eventHandler.onConnectionRecovered, - EventType.notificationThreadMessageNew => - _eventHandler.onNotificationThreadMessageNew, + EventType.notificationThreadMessageNew => _eventHandler.onNotificationThreadMessageNew, EventType.messageRead => _eventHandler.onMessageRead, - EventType.notificationMarkUnread => - _eventHandler.onNotificationMarkUnread, + EventType.notificationMarkUnread => _eventHandler.onNotificationMarkUnread, EventType.channelDeleted => _eventHandler.onChannelDeleted, EventType.channelTruncated => _eventHandler.onChannelTruncated, EventType.messageNew => _eventHandler.onMessageNew, diff --git a/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart index 9dc25635eb..fa22bc49a1 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_user_list_controller.dart @@ -40,9 +40,9 @@ class StreamUserListController extends PagedValueNotifier { this.sort = defaultUserListSort, this.presence = true, this.limit = defaultUserPagedLimit, - }) : _activeFilter = filter, - _activeSort = sort, - super(const PagedValue.loading()); + }) : _activeFilter = filter, + _activeSort = sort, + super(const PagedValue.loading()); /// Creates a [StreamUserListController] from the passed [value]. StreamUserListController.fromValue( @@ -52,8 +52,8 @@ class StreamUserListController extends PagedValueNotifier { this.sort = defaultUserListSort, this.presence = true, this.limit = defaultUserPagedLimit, - }) : _activeFilter = filter, - _activeSort = sort; + }) : _activeFilter = filter, + _activeSort = sort; /// The client to use for the channels list. final StreamChatClient client; @@ -105,11 +105,11 @@ class StreamUserListController extends PagedValueNotifier { super.value = switch (_activeSort) { null => newValue, final userSort => newValue.maybeMap( - orElse: () => newValue, - (success) => success.copyWith( - items: success.items.sorted(userSort.compare), - ), + orElse: () => newValue, + (success) => success.copyWith( + items: success.items.sorted(userSort.compare), ), + ), }; } diff --git a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart index 444362bbb7..2bdd25a06c 100644 --- a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart +++ b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart @@ -8,11 +8,7 @@ export 'src/lazy_load_scroll_view.dart'; export 'src/message_list_core.dart' hide MessageListCoreState; export 'src/message_text_field_controller.dart'; export 'src/paged_value_notifier.dart' - show - PagedValueListenableBuilder, - PagedValue, - PagedValueNotifier, - PagedValuePatternMatching; + show PagedValueListenableBuilder, PagedValue, PagedValueNotifier, PagedValuePatternMatching; export 'src/paged_value_scroll_view.dart'; export 'src/stream_channel.dart'; export 'src/stream_channel_list_controller.dart'; @@ -27,6 +23,7 @@ export 'src/stream_message_reminder_list_event_handler.dart'; export 'src/stream_message_search_list_controller.dart'; export 'src/stream_poll_controller.dart'; export 'src/stream_poll_vote_list_controller.dart'; +export 'src/stream_reaction_list_controller.dart'; export 'src/stream_thread_list_controller.dart'; export 'src/stream_thread_list_event_handler.dart'; export 'src/stream_user_list_controller.dart'; diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index fc538ec6cb..e73c6c3537 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.23.0 +version: 10.0.0-beta.13 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -18,8 +18,8 @@ issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues # 2. Add it to the melos.yaml file for future updates. environment: - sdk: ^3.6.2 - flutter: ">=3.27.4" + sdk: ^3.10.0 + flutter: ">=3.38.1" dependencies: collection: ^1.17.2 @@ -31,7 +31,7 @@ dependencies: meta: ^1.9.1 package_info_plus: ">=8.3.0 <10.0.0" rxdart: ^0.28.0 - stream_chat: ^9.23.0 + stream_chat: ^10.0.0-beta.13 dev_dependencies: build_runner: ^2.4.9 diff --git a/packages/stream_chat_flutter_core/test/message_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_list_core_test.dart index d49a31e8f5..5392f7db16 100644 --- a/packages/stream_chat_flutter_core/test/message_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_list_core_test.dart @@ -14,14 +14,14 @@ void main() { int offset = 0, bool threads = false, }) { - final users = List.generate(count, (index) { - index = count + offset; + final users = List.generate(count, (i) { + final index = i + offset; return User(id: 'testUserId$index'); }); final messages = List.generate( count, - (index) { - index = index + offset; + (i) { + final index = i + offset; return Message( id: 'testMessageId$index', type: 'testType', @@ -39,8 +39,8 @@ void main() { ); final threadMessages = List.generate( count, - (index) { - index = index + offset; + (i) { + final index = i + offset; return Message( id: 'testThreadMessageId$index', type: 'testType', @@ -94,8 +94,7 @@ void main() { final mockChannel = MockChannel(); when(() => mockChannel.state.unreadCount).thenReturn(0); when(() => mockChannel.state.isUpToDate).thenReturn(true); - when(() => mockChannel.state.messagesStream) - .thenAnswer((_) => Stream.value([])); + when(() => mockChannel.state.messagesStream).thenAnswer((_) => Stream.value([])); when(() => mockChannel.state.messages).thenReturn([]); await tester.pumpWidget( @@ -131,8 +130,7 @@ void main() { when(() => mockChannel.state.isUpToDate).thenReturn(true); when(() => mockChannel.state.unreadCount).thenReturn(0); - when(() => mockChannel.state.messagesStream) - .thenAnswer((_) => Stream.value([])); + when(() => mockChannel.state.messagesStream).thenAnswer((_) => Stream.value([])); when(() => mockChannel.state.messages).thenReturn([]); await tester.pumpWidget( @@ -173,8 +171,7 @@ void main() { when(() => mockChannel.state.unreadCount).thenReturn(0); final messages = _generateMessages(); when(() => mockChannel.state.messages).thenReturn(messages); - when(() => mockChannel.state.messagesStream) - .thenAnswer((_) => Stream.value(messages)); + when(() => mockChannel.state.messagesStream).thenAnswer((_) => Stream.value(messages)); when(() => mockChannel.state.messages).thenReturn(messages); await tester.pumpWidget( @@ -193,13 +190,15 @@ void main() { await coreState.paginateData(); - verify(() => mockChannel.query( - messagesPagination: any( - named: 'messagesPagination', - that: wrapMatcher((it) => it.limit == paginationLimit), - ), - preferOffline: any(named: 'preferOffline'), - )).called(1); + verify( + () => mockChannel.query( + messagesPagination: any( + named: 'messagesPagination', + that: wrapMatcher((it) => it.limit == paginationLimit), + ), + preferOffline: any(named: 'preferOffline'), + ), + ).called(1); }, ); @@ -223,8 +222,7 @@ void main() { when(() => mockChannel.state.isUpToDate).thenReturn(true); const error = 'Error! Error! Error!'; - when(() => mockChannel.state.messagesStream) - .thenAnswer((_) => Stream.error(error)); + when(() => mockChannel.state.messagesStream).thenAnswer((_) => Stream.error(error)); when(() => mockChannel.state.messages).thenReturn([]); when(() => mockChannel.state.unreadCount).thenReturn(0); @@ -254,8 +252,7 @@ void main() { key: messageListCoreKey, messageListBuilder: (_, __) => const Offstage(), loadingBuilder: (BuildContext context) => const Offstage(), - emptyBuilder: (BuildContext context) => - const Offstage(key: emptyWidgetKey), + emptyBuilder: (BuildContext context) => const Offstage(key: emptyWidgetKey), errorBuilder: (BuildContext context, Object error) => const Offstage(), ); @@ -264,8 +261,7 @@ void main() { when(() => mockChannel.state.isUpToDate).thenReturn(true); const messages = []; - when(() => mockChannel.state.messagesStream) - .thenAnswer((_) => Stream.value(messages)); + when(() => mockChannel.state.messagesStream).thenAnswer((_) => Stream.value(messages)); when(() => mockChannel.state.messages).thenReturn(messages); when(() => mockChannel.state.unreadCount).thenReturn(0); @@ -302,19 +298,20 @@ void main() { final mockChannel = MockChannel(); when(() => mockChannel.state.isUpToDate).thenReturn(false); - when(() => mockChannel.query( - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - membersPagination: any(named: 'membersPagination'), - messagesPagination: any(named: 'messagesPagination'), - preferOffline: any(named: 'preferOffline'), - watchersPagination: any(named: 'watchersPagination'), - )).thenAnswer((_) async => const ChannelState()); + when( + () => mockChannel.query( + state: any(named: 'state'), + watch: any(named: 'watch'), + presence: any(named: 'presence'), + membersPagination: any(named: 'membersPagination'), + messagesPagination: any(named: 'messagesPagination'), + preferOffline: any(named: 'preferOffline'), + watchersPagination: any(named: 'watchersPagination'), + ), + ).thenAnswer((_) async => const ChannelState()); const messages = []; - when(() => mockChannel.state.messagesStream) - .thenAnswer((_) => Stream.value(messages)); + when(() => mockChannel.state.messagesStream).thenAnswer((_) => Stream.value(messages)); when(() => mockChannel.state.messages).thenReturn(messages); when(() => mockChannel.state.unreadCount).thenReturn(0); @@ -358,8 +355,7 @@ void main() { when(() => mockChannel.state.isUpToDate).thenReturn(true); final messages = _generateMessages(); - when(() => mockChannel.state.messagesStream) - .thenAnswer((_) => Stream.value(messages)); + when(() => mockChannel.state.messagesStream).thenAnswer((_) => Stream.value(messages)); when(() => mockChannel.state.messages).thenReturn(messages); when(() => mockChannel.state.unreadCount).thenReturn(0); @@ -408,8 +404,7 @@ void main() { final threads = {parentMessage.id: messages}; when(() => mockChannel.state.threads).thenReturn(threads); - when(() => mockChannel.state.threadsStream) - .thenAnswer((_) => Stream.value(threads)); + when(() => mockChannel.state.threadsStream).thenAnswer((_) => Stream.value(threads)); when(() => mockChannel.state.unreadCount).thenReturn(0); when( diff --git a/packages/stream_chat_flutter_core/test/mocks.dart b/packages/stream_chat_flutter_core/test/mocks.dart index 7235202250..cc705f9838 100644 --- a/packages/stream_chat_flutter_core/test/mocks.dart +++ b/packages/stream_chat_flutter_core/test/mocks.dart @@ -22,11 +22,11 @@ class MockClientState extends Mock implements ClientState { @override OwnUser get currentUser => _currentUser ??= OwnUser( - id: 'testUserId', - role: 'admin', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ); + id: 'testUserId', + role: 'admin', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); } class NonInitializedMockChannel extends Mock implements Channel { diff --git a/packages/stream_chat_flutter_core/test/paged_value_grid_view_test.dart b/packages/stream_chat_flutter_core/test/paged_value_grid_view_test.dart new file mode 100644 index 0000000000..c2877cdaf5 --- /dev/null +++ b/packages/stream_chat_flutter_core/test/paged_value_grid_view_test.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +class _TestController extends PagedValueNotifier { + _TestController(List items, {int? nextPageKey}) : super(PagedValue(items: items, nextPageKey: nextPageKey)); + + @override + Future doInitialLoad() async {} + + @override + Future loadMore(int nextPageKey) async {} +} + +Widget _wrap(Widget child) => MaterialApp(home: Scaffold(body: child)); + +PagedValueGridView _buildGrid( + _TestController controller, { + WidgetBuilder? leadingItemBuilder, + required List builtIndices, +}) { + return PagedValueGridView( + controller: controller, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), + leadingItemBuilder: leadingItemBuilder, + itemBuilder: (context, items, index) { + builtIndices.add(index); + return Text('item-$index'); + }, + emptyBuilder: (_) => const Text('empty'), + loadMoreErrorBuilder: (_, __) => const Text('load-more-error'), + loadMoreIndicatorBuilder: (_) => const Text('load-more-indicator'), + loadingBuilder: (_) => const Text('loading'), + errorBuilder: (_, __) => const Text('error'), + ); +} + +void main() { + group('PagedValueGridView without leadingItemBuilder', () { + testWidgets('renders items starting at index 0', (tester) async { + final controller = _TestController(['a', 'b', 'c']); + final builtIndices = []; + + await tester.pumpWidget(_wrap(_buildGrid(controller, builtIndices: builtIndices))); + await tester.pump(); + + expect(find.text('item-0'), findsOneWidget); + expect(find.text('item-1'), findsOneWidget); + expect(find.text('item-2'), findsOneWidget); + expect(builtIndices, [0, 1, 2]); + }); + + testWidgets('does not render a leading item', (tester) async { + final controller = _TestController(['a']); + final builtIndices = []; + + await tester.pumpWidget(_wrap(_buildGrid(controller, builtIndices: builtIndices))); + await tester.pump(); + + expect(find.text('leading'), findsNothing); + expect(builtIndices, [0]); + }); + }); + + group('PagedValueGridView with leadingItemBuilder', () { + testWidgets('renders the leading item before the paged items', (tester) async { + final controller = _TestController(['a', 'b', 'c']); + final builtIndices = []; + + await tester.pumpWidget( + _wrap( + _buildGrid( + controller, + leadingItemBuilder: (_) => const Text('leading'), + builtIndices: builtIndices, + ), + ), + ); + await tester.pump(); + + expect(find.text('leading'), findsOneWidget); + expect(find.text('item-0'), findsOneWidget); + expect(find.text('item-1'), findsOneWidget); + expect(find.text('item-2'), findsOneWidget); + }); + + testWidgets('itemBuilder receives item indices starting at 0, not offset', (tester) async { + final controller = _TestController(['a', 'b', 'c']); + final builtIndices = []; + + await tester.pumpWidget( + _wrap( + _buildGrid( + controller, + leadingItemBuilder: (_) => const Text('leading'), + builtIndices: builtIndices, + ), + ), + ); + await tester.pump(); + + expect(builtIndices, [0, 1, 2]); + }); + + testWidgets('renders leading item even with a single paged item', (tester) async { + final controller = _TestController(['a']); + final builtIndices = []; + + await tester.pumpWidget( + _wrap( + _buildGrid( + controller, + leadingItemBuilder: (_) => const Text('leading'), + builtIndices: builtIndices, + ), + ), + ); + await tester.pump(); + + expect(find.text('leading'), findsOneWidget); + expect(find.text('item-0'), findsOneWidget); + expect(builtIndices, [0]); + }); + + testWidgets('renders load-more indicator at correct position with leading item', (tester) async { + final controller = _TestController(['item-0', 'item-1'], nextPageKey: 1); + final builtIndices = []; + + await tester.pumpWidget( + _wrap( + _buildGrid( + controller, + leadingItemBuilder: (_) => const Text('leading'), + builtIndices: builtIndices, + ), + ), + ); + await tester.pump(); + + expect(find.text('leading'), findsOneWidget); + expect(find.text('item-0'), findsOneWidget); + expect(find.text('item-1'), findsOneWidget); + expect(find.text('load-more-indicator'), findsOneWidget); + expect(builtIndices, [0, 1]); + }); + }); +} diff --git a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart index 8f56ce1c1d..80c95ad425 100644 --- a/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_chat_core_test.dart @@ -145,8 +145,7 @@ void main() { when( mockClient.openConnection, ).thenAnswer((_) async => OwnUser(id: 'test-user')); - when(() => mockClient.wsConnectionStatus) - .thenReturn(ConnectionStatus.connected); + when(() => mockClient.wsConnectionStatus).thenReturn(ConnectionStatus.connected); }); tearDown(() { @@ -202,8 +201,7 @@ void main() { ).thenReturn(ConnectionStatus.disconnected); // Act - bring app to foreground - tester.binding - .handleAppLifecycleStateChanged(AppLifecycleState.resumed); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); await tester.pumpAndSettle(); // Assert @@ -251,8 +249,7 @@ void main() { ); // Act - tester.binding - .handleAppLifecycleStateChanged(AppLifecycleState.paused); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused); await tester.pumpAndSettle(); // Wait for timer to expire diff --git a/packages/stream_chat_flutter_core/test/stream_draft_list_controller_test.dart b/packages/stream_chat_flutter_core/test/stream_draft_list_controller_test.dart index ff07096332..87aa10cfa8 100644 --- a/packages/stream_chat_flutter_core/test/stream_draft_list_controller_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_draft_list_controller_test.dart @@ -36,9 +36,7 @@ List generateDrafts({ final baseId = startId ?? 123; return List.generate(count, (index) { - final text = texts != null && index < texts.length - ? texts[index] - : 'Draft ${index + 1}'; + final text = texts != null && index < texts.length ? texts[index] : 'Draft ${index + 1}'; return generateDraft( channelCid: 'messaging:${baseId + index}', @@ -86,22 +84,26 @@ void main() { ..drafts = drafts ..next = ''; - when(() => client.queryDrafts( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => response); + when( + () => client.queryDrafts( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => response); final controller = StreamDraftListController(client: client); await controller.doInitialLoad(); await pumpEventQueue(); - verify(() => client.queryDrafts( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).called(1); + verify( + () => client.queryDrafts( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).called(1); expect(controller.value, isA>()); expect(controller.value.asSuccess.items, equals(drafts)); @@ -109,11 +111,13 @@ void main() { test('handles API exceptions by transitioning to error state', () async { final exception = Exception('API unavailable'); - when(() => client.queryDrafts( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenThrow(exception); + when( + () => client.queryDrafts( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenThrow(exception); final controller = StreamDraftListController(client: client); @@ -142,11 +146,13 @@ void main() { ..drafts = additionalDrafts ..next = ''; - when(() => client.queryDrafts( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => response); + when( + () => client.queryDrafts( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => response); final controller = StreamDraftListController.fromValue( PagedValue( @@ -171,9 +177,9 @@ void main() { for (final draft in mergedDrafts) { expect( - controller.value.asSuccess.items.any((d) => - d.channelCid == draft.channelCid && - d.message.text == draft.message.text), + controller.value.asSuccess.items.any( + (d) => d.channelCid == draft.channelCid && d.message.text == draft.message.text, + ), isTrue, ); } @@ -181,17 +187,18 @@ void main() { expect(controller.value.asSuccess.nextPageKey, isNull); }); - test('loadMore preserves existing items when API throws exception', - () async { + test('loadMore preserves existing items when API throws exception', () async { const nextKey = 'next_page_token'; final existingDrafts = generateDrafts(); final exception = Exception('Network error'); - when(() => client.queryDrafts( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenThrow(exception); + when( + () => client.queryDrafts( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenThrow(exception); final controller = StreamDraftListController.fromValue( PagedValue( @@ -331,9 +338,7 @@ void main() { equals(allDrafts.length - 1), ); - final remainingDraftTexts = [ - ...controller.value.asSuccess.items.map((d) => d.message.text) - ]; + final remainingDraftTexts = [...controller.value.asSuccess.items.map((d) => d.message.text)]; expect(remainingDraftTexts, contains('Thread Draft 2')); expect(remainingDraftTexts, isNot(contains('Thread Draft 1'))); @@ -393,11 +398,13 @@ void main() { ..drafts = drafts ..next = ''; - when(() => client.queryDrafts( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => queryResponse); + when( + () => client.queryDrafts( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => queryResponse); final controller = StreamDraftListController.fromValue( PagedValue(items: drafts), @@ -432,11 +439,13 @@ void main() { ..drafts = drafts ..next = ''; - when(() => client.queryDrafts( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => queryResponse); + when( + () => client.queryDrafts( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => queryResponse); final controller = StreamDraftListController.fromValue( PagedValue(items: drafts), @@ -473,11 +482,13 @@ void main() { final drafts = generateDrafts(); var queryCallCount = 0; - when(() => client.queryDrafts( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async { + when( + () => client.queryDrafts( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async { queryCallCount++; return QueryDraftsResponse() ..drafts = drafts @@ -512,11 +523,13 @@ void main() { ..drafts = drafts ..next = ''; - when(() => client.queryDrafts( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => queryResponse); + when( + () => client.queryDrafts( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => queryResponse); final controller = StreamDraftListController.fromValue( PagedValue(items: drafts), @@ -569,11 +582,13 @@ void main() { ..drafts = drafts ..next = ''; - when(() => client.queryDrafts( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => response); + when( + () => client.queryDrafts( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => response); final controller = StreamDraftListController.fromValue( PagedValue(items: drafts), @@ -599,11 +614,13 @@ void main() { final apiCalls = >[]; - when(() => client.queryDrafts( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((invocation) async { + when( + () => client.queryDrafts( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((invocation) async { apiCalls.add({ 'filter': invocation.namedArguments[const Symbol('filter')], 'sort': invocation.namedArguments[const Symbol('sort')], @@ -647,18 +664,22 @@ void main() { final apiCalls = >[]; - when(() => client.queryDrafts( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((invocation) { + when( + () => client.queryDrafts( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((invocation) { apiCalls.add({ 'filter': invocation.namedArguments[const Symbol('filter')], 'sort': invocation.namedArguments[const Symbol('sort')], }); - return Future.value(QueryDraftsResponse() - ..drafts = drafts - ..next = ''); + return Future.value( + QueryDraftsResponse() + ..drafts = drafts + ..next = '', + ); }); final controller = StreamDraftListController( @@ -688,8 +709,7 @@ void main() { group('Disposal', () { test('dispose cancels subscriptions without errors', () { - final controller = StreamDraftListController(client: client) - ..doInitialLoad(); + final controller = StreamDraftListController(client: client)..doInitialLoad(); expect(controller.dispose, returnsNormally); }); diff --git a/packages/stream_chat_flutter_core/test/stream_message_input_controller_test.dart b/packages/stream_chat_flutter_core/test/stream_message_input_controller_test.dart index ba1eadfa9f..f3cae092ca 100644 --- a/packages/stream_chat_flutter_core/test/stream_message_input_controller_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_message_input_controller_test.dart @@ -241,12 +241,10 @@ void main() { }); test('setOGAttachment replaces existing OG attachment', () { - final oldOGAttachment = - Attachment(ogScrapeUrl: 'https://old.example.com'); + final oldOGAttachment = Attachment(ogScrapeUrl: 'https://old.example.com'); controller.addAttachment(oldOGAttachment); - final newOGAttachment = - Attachment(ogScrapeUrl: 'https://new.example.com'); + final newOGAttachment = Attachment(ogScrapeUrl: 'https://new.example.com'); controller.setOGAttachment(newOGAttachment); expect(controller.attachments.length, 1); @@ -382,6 +380,63 @@ void main() { }); }); + group('Edit Message', () { + test('editMessage sets state to MessageState.updating', () { + final existingMessage = Message(id: 'msg-1', text: 'Original text'); + controller.editMessage(existingMessage); + + expect(controller.message.state.isInitial, isFalse); + expect(controller.message.state.isUpdating, isTrue); + }); + + test('editMessage preserves the message id and text', () { + final existingMessage = Message(id: 'msg-1', text: 'Original text'); + controller.editMessage(existingMessage); + + expect(controller.message.id, 'msg-1'); + expect(controller.message.text, 'Original text'); + }); + + test('editMessage stores original in editingOriginalMessage', () { + final existingMessage = Message(id: 'msg-1', text: 'Original text'); + controller.editMessage(existingMessage); + + expect(controller.editingOriginalMessage, isNotNull); + expect(controller.editingOriginalMessage!.id, 'msg-1'); + expect(controller.editingOriginalMessage!.text, 'Original text'); + }); + + test('editingOriginalMessage text is not affected by subsequent typing', () { + final existingMessage = Message(id: 'msg-1', text: 'Original text'); + controller.editMessage(existingMessage); + + controller.text = 'Edited text'; + + expect(controller.editingOriginalMessage!.text, 'Original text'); + expect(controller.message.text, 'Edited text'); + }); + + test('cancelEditMessage clears editingOriginalMessage', () { + final existingMessage = Message(id: 'msg-1', text: 'Original text'); + controller.editMessage(existingMessage); + + controller.cancelEditMessage(); + + expect(controller.editingOriginalMessage, isNull); + }); + + test('cancelEditMessage resets controller to empty state, not the edited message', () { + final existingMessage = Message(id: 'msg-1', text: 'Original text'); + controller.editMessage(existingMessage); + controller.text = 'Edited text'; + + controller.cancelEditMessage(); + + expect(controller.text, isEmpty); + expect(controller.message.state.isInitial, isTrue); + }); + }); + group('Reset and Clear', () { test('clear resets the message to empty state', () { controller.text = 'Some text'; @@ -501,8 +556,7 @@ class _RestorableWidget extends StatefulWidget { State<_RestorableWidget> createState() => _RestorableWidgetState(); } -class _RestorableWidgetState extends State<_RestorableWidget> - with RestorationMixin { +class _RestorableWidgetState extends State<_RestorableWidget> with RestorationMixin { final controller = StreamRestorableMessageInputController(); @override diff --git a/packages/stream_chat_flutter_core/test/stream_message_reminder_list_controller_test.dart b/packages/stream_chat_flutter_core/test/stream_message_reminder_list_controller_test.dart index 86d6767df1..cbbd5924b6 100644 --- a/packages/stream_chat_flutter_core/test/stream_message_reminder_list_controller_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_message_reminder_list_controller_test.dart @@ -48,15 +48,9 @@ List generateMessageReminders({ final channelCid = channelCids != null && index < channelCids.length ? channelCids[index] : 'messaging:${baseId + index}'; - final messageId = messageIds != null && index < messageIds.length - ? messageIds[index] - : 'message_${baseId + index}'; - final userId = userIds != null && index < userIds.length - ? userIds[index] - : 'user_${baseId + index}'; - final text = texts != null && index < texts.length - ? texts[index] - : 'Reminder ${index + 1}'; + final messageId = messageIds != null && index < messageIds.length ? messageIds[index] : 'message_${baseId + index}'; + final userId = userIds != null && index < userIds.length ? userIds[index] : 'user_${baseId + index}'; + final text = texts != null && index < texts.length ? texts[index] : 'Reminder ${index + 1}'; return generateMessageReminder( channelCid: channelCid, @@ -110,22 +104,26 @@ void main() { ..reminders = reminders ..next = null; - when(() => client.queryReminders( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => response); + when( + () => client.queryReminders( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => response); final controller = StreamMessageReminderListController(client: client); await controller.doInitialLoad(); await pumpEventQueue(); - verify(() => client.queryReminders( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).called(1); + verify( + () => client.queryReminders( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).called(1); expect(controller.value, isA>()); expect(controller.value.asSuccess.items, equals(reminders)); @@ -133,11 +131,13 @@ void main() { test('handles StreamChatError exceptions properly', () async { const chatError = StreamChatError('Network error'); - when(() => client.queryReminders( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenThrow(chatError); + when( + () => client.queryReminders( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenThrow(chatError); final controller = StreamMessageReminderListController(client: client); @@ -166,11 +166,13 @@ void main() { ..reminders = additionalReminders ..next = null; - when(() => client.queryReminders( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenAnswer((_) async => response); + when( + () => client.queryReminders( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => response); final controller = StreamMessageReminderListController.fromValue( PagedValue( @@ -198,11 +200,13 @@ void main() { final existingReminders = generateMessageReminders(); const chatError = StreamChatError('Network error'); - when(() => client.queryReminders( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - pagination: any(named: 'pagination'), - )).thenThrow(chatError); + when( + () => client.queryReminders( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenThrow(chatError); final controller = StreamMessageReminderListController.fromValue( PagedValue( @@ -275,9 +279,7 @@ void main() { ); }); - test( - 'deleteReminder removes reminder and returns true when reminder exists', - () { + test('deleteReminder removes reminder and returns true when reminder exists', () { final reminders = generateMessageReminders(); final controller = StreamMessageReminderListController.fromValue( PagedValue(items: reminders), @@ -292,9 +294,9 @@ void main() { equals(reminders.length - 1), ); expect( - controller.value.asSuccess.items.any((r) => - r.messageId == reminders[0].messageId && - r.userId == reminders[0].userId), + controller.value.asSuccess.items.any( + (r) => r.messageId == reminders[0].messageId && r.userId == reminders[0].userId, + ), isFalse, ); }); @@ -438,9 +440,7 @@ void main() { expect( controller.value.asSuccess.items.any( - (r) => - r.messageId == initialReminders[0].messageId && - r.userId == initialReminders[0].userId, + (r) => r.messageId == initialReminders[0].messageId && r.userId == initialReminders[0].userId, ), isFalse, ); diff --git a/packages/stream_chat_flutter_core/test/stream_poll_controller_test.dart b/packages/stream_chat_flutter_core/test/stream_poll_controller_test.dart index ed5282eb63..408b4c9c29 100644 --- a/packages/stream_chat_flutter_core/test/stream_poll_controller_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_poll_controller_test.dart @@ -11,10 +11,13 @@ void main() { }); test('Initialization with Custom Poll and Config', () { - final poll = Poll(name: 'Initial Poll', options: const [ - PollOption(text: 'Option 1'), - PollOption(text: 'Option 2'), - ]); + final poll = Poll( + name: 'Initial Poll', + options: const [ + PollOption(text: 'Option 1'), + PollOption(text: 'Option 2'), + ], + ); const config = PollConfig(nameRange: (min: 2, max: 50)); final pollController = StreamPollController(poll: poll, config: config); @@ -101,10 +104,7 @@ void main() { final errors = pollController.validateGranularly(); expect(errors.isEmpty, isFalse); - final containsNameRangeError = errors - .map((e) => e.mapOrNull(nameRange: (e) => e)) - .nonNulls - .isNotEmpty; + final containsNameRangeError = errors.map((e) => e.mapOrNull(nameRange: (e) => e)).nonNulls.isNotEmpty; expect(containsNameRangeError, isTrue); }); @@ -117,10 +117,7 @@ void main() { final errors = pollController.validateGranularly(); expect(errors.isEmpty, isFalse); - final containsDuplicateOptions = errors - .map((e) => e.mapOrNull(duplicateOptions: (e) => e)) - .nonNulls - .isNotEmpty; + final containsDuplicateOptions = errors.map((e) => e.mapOrNull(duplicateOptions: (e) => e)).nonNulls.isNotEmpty; expect(containsDuplicateOptions, isTrue); }); @@ -130,10 +127,7 @@ void main() { final errors = pollController.validateGranularly(); expect(errors.isEmpty, isFalse); - final containsOptionsRangeError = errors - .map((e) => e.mapOrNull(optionsRange: (e) => e)) - .nonNulls - .isNotEmpty; + final containsOptionsRangeError = errors.map((e) => e.mapOrNull(optionsRange: (e) => e)).nonNulls.isNotEmpty; expect(containsOptionsRangeError, isTrue); }); @@ -238,10 +232,7 @@ void main() { )..question = 'A' * 200; final errors = pollController.validateGranularly(); - final containsNameRangeError = errors - .map((e) => e.mapOrNull(nameRange: (e) => e)) - .nonNulls - .isNotEmpty; + final containsNameRangeError = errors.map((e) => e.mapOrNull(nameRange: (e) => e)).nonNulls.isNotEmpty; expect(containsNameRangeError, isFalse); }); @@ -256,10 +247,7 @@ void main() { } final errors = pollController.validateGranularly(); - final containsOptionsRangeError = errors - .map((e) => e.mapOrNull(optionsRange: (e) => e)) - .nonNulls - .isNotEmpty; + final containsOptionsRangeError = errors.map((e) => e.mapOrNull(optionsRange: (e) => e)).nonNulls.isNotEmpty; expect(containsOptionsRangeError, isFalse); }); diff --git a/packages/stream_chat_flutter_core/test/stream_reaction_list_controller_test.dart b/packages/stream_chat_flutter_core/test/stream_reaction_list_controller_test.dart new file mode 100644 index 0000000000..983d82b09b --- /dev/null +++ b/packages/stream_chat_flutter_core/test/stream_reaction_list_controller_test.dart @@ -0,0 +1,579 @@ +// ignore_for_file: avoid_redundant_argument_values + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; +import 'package:stream_chat_flutter_core/src/stream_reaction_list_controller.dart'; + +import 'mocks.dart'; + +Reaction generateReaction({ + String? messageId, + String? type, + String? userId, + DateTime? createdAt, +}) { + return Reaction( + messageId: messageId ?? 'message_123', + type: type ?? 'like', + userId: userId ?? 'user_123', + createdAt: createdAt ?? DateTime.now(), + ); +} + +List generateReactions({ + int count = 2, + String? messageId, + List? types, + List? userIds, + int? startId, +}) { + final now = DateTime.now(); + final baseId = startId ?? 1; + + return List.generate(count, (index) { + final type = types != null && index < types.length ? types[index] : 'like'; + final userId = userIds != null && index < userIds.length ? userIds[index] : 'user_${baseId + index}'; + + return generateReaction( + messageId: messageId ?? 'message_123', + type: type, + userId: userId, + createdAt: now.subtract(Duration(minutes: index)), + ); + }); +} + +void main() { + const messageId = 'message_123'; + + final client = MockClient(); + + setUpAll(() { + registerFallbackValue(const PaginationParams()); + registerFallbackValue(Filter.equal('type', 'like')); + }); + + setUp(() { + when(client.on).thenAnswer((_) => const Stream.empty()); + }); + + tearDown(() { + reset(client); + }); + + group('Initialization', () { + test('should start in loading state when created with client', () { + final controller = StreamReactionListController( + client: client, + messageId: messageId, + ); + expect(controller.value, isA()); + }); + + test('should preserve provided value when created with fromValue', () { + final reactions = generateReactions(); + final value = PagedValue(items: reactions); + final controller = StreamReactionListController.fromValue( + value, + client: client, + messageId: messageId, + ); + + expect(controller.value, same(value)); + expect(controller.value.asSuccess.items, equals(reactions)); + }); + }); + + group('Initial loading', () { + test('successfully loads reactions from API', () async { + final reactions = generateReactions(); + final response = QueryReactionsResponse() + ..reactions = reactions + ..next = null; + + when( + () => client.queryReactions( + any(), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => response); + + final controller = StreamReactionListController( + client: client, + messageId: messageId, + ); + + await controller.doInitialLoad(); + await pumpEventQueue(); + + verify( + () => client.queryReactions( + messageId, + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).called(1); + + expect(controller.value, isA>()); + expect(controller.value.asSuccess.items, equals(reactions)); + }); + + test('sets next page key when API returns next cursor', () async { + const nextCursor = 'next_cursor_token'; + final reactions = generateReactions(); + final response = QueryReactionsResponse() + ..reactions = reactions + ..next = nextCursor; + + when( + () => client.queryReactions( + any(), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => response); + + final controller = StreamReactionListController( + client: client, + messageId: messageId, + ); + + await controller.doInitialLoad(); + await pumpEventQueue(); + + expect(controller.value.asSuccess.nextPageKey, equals(nextCursor)); + }); + + test('sets null next page key when API returns empty next cursor', () async { + final reactions = generateReactions(); + final response = QueryReactionsResponse() + ..reactions = reactions + ..next = ''; + + when( + () => client.queryReactions( + any(), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => response); + + final controller = StreamReactionListController( + client: client, + messageId: messageId, + ); + + await controller.doInitialLoad(); + await pumpEventQueue(); + + expect(controller.value.asSuccess.nextPageKey, isNull); + }); + + test('handles StreamChatError by transitioning to error state', () async { + const chatError = StreamChatError('Network error'); + + when( + () => client.queryReactions( + any(), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenThrow(chatError); + + final controller = StreamReactionListController( + client: client, + messageId: messageId, + ); + + await controller.doInitialLoad(); + await pumpEventQueue(); + + expect(controller.value, isA()); + expect((controller.value as Error).error, equals(chatError)); + }); + + test('wraps generic exceptions in StreamChatError', () async { + final exception = Exception('API unavailable'); + + when( + () => client.queryReactions( + any(), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenThrow(exception); + + final controller = StreamReactionListController( + client: client, + messageId: messageId, + ); + + await controller.doInitialLoad(); + await pumpEventQueue(); + + expect(controller.value, isA()); + expect( + (controller.value as Error).error.message, + contains('API unavailable'), + ); + }); + }); + + group('Pagination', () { + test('loadMore appends new reactions to existing items', () async { + const nextKey = 'next_page_token'; + final existingReactions = generateReactions(); + final additionalReactions = generateReactions( + count: 1, + userIds: ['user_999'], + startId: 999, + ); + + final response = QueryReactionsResponse() + ..reactions = additionalReactions + ..next = null; + + when( + () => client.queryReactions( + any(), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => response); + + final controller = StreamReactionListController.fromValue( + PagedValue( + items: existingReactions, + nextPageKey: nextKey, + ), + client: client, + messageId: messageId, + ); + + await controller.loadMore(nextKey); + await pumpEventQueue(); + + final mergedReactions = [...existingReactions, ...additionalReactions]; + + expect( + controller.value.asSuccess.items.length, + equals(mergedReactions.length), + ); + expect(controller.value.asSuccess.nextPageKey, isNull); + }); + + test('loadMore passes next cursor to API', () async { + const nextKey = 'cursor_page_2'; + final existingReactions = generateReactions(); + + final response = QueryReactionsResponse() + ..reactions = [] + ..next = null; + + PaginationParams? capturedPagination; + when( + () => client.queryReactions( + any(), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((invocation) async { + capturedPagination = invocation.namedArguments[const Symbol('pagination')] as PaginationParams?; + return response; + }); + + final controller = StreamReactionListController.fromValue( + PagedValue( + items: existingReactions, + nextPageKey: nextKey, + ), + client: client, + messageId: messageId, + ); + + await controller.loadMore(nextKey); + await pumpEventQueue(); + + expect(capturedPagination?.next, equals(nextKey)); + }); + + test('loadMore preserves existing items on StreamChatError', () async { + const nextKey = 'next_page_token'; + final existingReactions = generateReactions(); + const chatError = StreamChatError('Network error'); + + when( + () => client.queryReactions( + any(), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenThrow(chatError); + + final controller = StreamReactionListController.fromValue( + PagedValue( + items: existingReactions, + nextPageKey: nextKey, + ), + client: client, + messageId: messageId, + ); + + await controller.loadMore(nextKey); + await pumpEventQueue(); + + expect(controller.value.isSuccess, isTrue); + expect(controller.value.asSuccess.items, equals(existingReactions)); + expect(controller.value.asSuccess.error, equals(chatError)); + }); + + test('loadMore preserves existing items on generic error', () async { + const nextKey = 'next_page_token'; + final existingReactions = generateReactions(); + final exception = Exception('Network error'); + + when( + () => client.queryReactions( + any(), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenThrow(exception); + + final controller = StreamReactionListController.fromValue( + PagedValue( + items: existingReactions, + nextPageKey: nextKey, + ), + client: client, + messageId: messageId, + ); + + await controller.loadMore(nextKey); + await pumpEventQueue(); + + expect(controller.value.isSuccess, isTrue); + expect(controller.value.asSuccess.items, equals(existingReactions)); + expect(controller.value.asSuccess.error, isNotNull); + expect( + controller.value.asSuccess.error!.message, + contains('Network error'), + ); + }); + }); + + group('reactions setter', () { + test('replaces reactions when in success state', () { + final initial = generateReactions(count: 3); + final replacement = generateReactions(count: 1, userIds: ['user_new']); + + final controller = StreamReactionListController.fromValue( + PagedValue(items: initial), + client: client, + messageId: messageId, + ); + + expect(controller.value.isSuccess, isTrue); + expect(controller.value.asSuccess.items, equals(initial)); + + controller.reactions = replacement; + + expect(controller.value.asSuccess.items, equals(replacement)); + expect(controller.value.asSuccess.items.length, equals(1)); + }); + + test('creates new success value when not in success state', () { + final reactions = generateReactions(); + final controller = StreamReactionListController( + client: client, + messageId: messageId, + ); + + // Controller is in loading state + expect(controller.value.isNotSuccess, isTrue); + + controller.reactions = reactions; + + expect(controller.value.isSuccess, isTrue); + expect(controller.value.asSuccess.items, equals(reactions)); + }); + + test('preserves nextPageKey when replacing reactions', () { + const nextKey = 'next_cursor'; + final initial = generateReactions(); + final replacement = generateReactions(count: 1, userIds: ['user_new']); + + final controller = StreamReactionListController.fromValue( + PagedValue(items: initial, nextPageKey: nextKey), + client: client, + messageId: messageId, + ); + + expect(controller.value.isSuccess, isTrue); + expect(controller.value.asSuccess.items, equals(initial)); + expect(controller.value.asSuccess.nextPageKey, equals(nextKey)); + + controller.reactions = replacement; + + expect(controller.value.asSuccess.items, equals(replacement)); + expect(controller.value.asSuccess.nextPageKey, equals(nextKey)); + }); + }); + + group('Filtering and sorting', () { + test('refresh resets filter and sort to initial values', () async { + final reactions = generateReactions(); + final initialFilter = Filter.equal('type', 'like'); + final sort = [const SortOption.desc(ReactionSortKey.createdAt)]; + + final apiCalls = >[]; + + when( + () => client.queryReactions( + any(), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((invocation) async { + apiCalls.add({ + 'filter': invocation.namedArguments[const Symbol('filter')], + 'sort': invocation.namedArguments[const Symbol('sort')], + }); + return QueryReactionsResponse() + ..reactions = reactions + ..next = null; + }); + + final controller = StreamReactionListController( + client: client, + messageId: messageId, + filter: initialFilter, + sort: sort, + ); + + await controller.doInitialLoad(); + await pumpEventQueue(); + + // Change filter and sort at runtime + controller + ..filter = Filter.equal('type', 'love') + ..sort = [const SortOption.asc(ReactionSortKey.createdAt)]; + + await controller.refresh(); + await pumpEventQueue(); + + expect(apiCalls.length, equals(2)); + + final refreshCall = apiCalls.last; + expect(refreshCall['filter'], equals(initialFilter)); + expect(refreshCall['sort'], equals(sort)); + }); + + test('refresh with resetValue=false preserves current filter and sort', () async { + final reactions = generateReactions(); + final initialFilter = Filter.equal('type', 'like'); + final initialSort = [const SortOption.desc(ReactionSortKey.createdAt)]; + final newFilter = Filter.equal('type', 'love'); + final newSort = [const SortOption.asc(ReactionSortKey.createdAt)]; + + final apiCalls = >[]; + + when( + () => client.queryReactions( + any(), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((invocation) async { + apiCalls.add({ + 'filter': invocation.namedArguments[const Symbol('filter')], + 'sort': invocation.namedArguments[const Symbol('sort')], + }); + return QueryReactionsResponse() + ..reactions = reactions + ..next = null; + }); + + final controller = StreamReactionListController( + client: client, + messageId: messageId, + filter: initialFilter, + sort: initialSort, + ); + + await controller.doInitialLoad(); + await pumpEventQueue(); + + controller + ..filter = newFilter + ..sort = newSort; + + await controller.refresh(resetValue: false); + await pumpEventQueue(); + + expect(apiCalls.length, equals(2)); + + final refreshCall = apiCalls.last; + expect(refreshCall['filter'], equals(newFilter)); + expect(refreshCall['sort'], equals(newSort)); + }); + + test('value setter sorts items when sort is provided', () async { + final now = DateTime.now(); + final older = generateReaction(userId: 'user_1', createdAt: now.subtract(const Duration(hours: 1))); + final newer = generateReaction(userId: 'user_2', createdAt: now); + + final response = QueryReactionsResponse() + ..reactions = [older, newer] + ..next = null; + + when( + () => client.queryReactions( + any(), + filter: any(named: 'filter'), + sort: any(named: 'sort'), + pagination: any(named: 'pagination'), + ), + ).thenAnswer((_) async => response); + + final controller = StreamReactionListController( + client: client, + messageId: messageId, + sort: [const SortOption.desc(ReactionSortKey.createdAt)], + ); + + await controller.doInitialLoad(); + await pumpEventQueue(); + + // desc order: newer first + expect(controller.value.asSuccess.items.first.userId, equals('user_2')); + expect(controller.value.asSuccess.items.last.userId, equals('user_1')); + }); + }); + + group('Disposal', () { + test('dispose completes without errors', () { + final controller = StreamReactionListController( + client: client, + messageId: messageId, + )..doInitialLoad(); + + expect(controller.dispose, returnsNormally); + }); + }); +} diff --git a/packages/stream_chat_flutter_core/test/stream_thread_list_event_handler_test.dart b/packages/stream_chat_flutter_core/test/stream_thread_list_event_handler_test.dart index 2dd163d6df..f4608b16f1 100644 --- a/packages/stream_chat_flutter_core/test/stream_thread_list_event_handler_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_thread_list_event_handler_test.dart @@ -5,8 +5,7 @@ import 'package:stream_chat_flutter_core/src/stream_thread_list_controller.dart' import 'package:stream_chat_flutter_core/src/stream_thread_list_event_handler.dart'; // Mock classes -class MockStreamThreadListController extends Mock - implements StreamThreadListController {} +class MockStreamThreadListController extends Mock implements StreamThreadListController {} class MockEvent extends Mock implements Event {} @@ -83,8 +82,7 @@ void main() { () { when(() => mockEvent.message).thenReturn(mockMessage); when(() => mockMessage.parentId).thenReturn('parent-id'); - when(() => mockController.getThread( - parentMessageId: any(named: 'parentMessageId'))).thenReturn(null); + when(() => mockController.getThread(parentMessageId: any(named: 'parentMessageId'))).thenReturn(null); handler.onNotificationThreadMessageNew(mockEvent, mockController); verify(() => mockController.addUnseenThreadId('parent-id')); @@ -108,12 +106,10 @@ void main() { when(() => mockMessage.parentId).thenReturn(null); when(() => mockEvent.message).thenReturn(mockMessage); when(() => mockEvent.hardDelete).thenReturn(true); - when(() => mockController.deleteThread(parentMessageId: 'message-id')) - .thenReturn(true); + when(() => mockController.deleteThread(parentMessageId: 'message-id')).thenReturn(true); handler.onMessageDeleted(mockEvent, mockController); - verify( - () => mockController.deleteThread(parentMessageId: 'message-id')); + verify(() => mockController.deleteThread(parentMessageId: 'message-id')); verifyNever(() => mockController.deleteReply(any())); }, ); @@ -178,27 +174,23 @@ void main() { when(() => mockEvent.cid).thenReturn('channel-cid'); handler.onChannelDeleted(mockEvent, mockController); - verify(() => - mockController.deleteThreadByChannelCid(channelCid: 'channel-cid')); + verify(() => mockController.deleteThreadByChannelCid(channelCid: 'channel-cid')); }); test('onChannelTruncated deletes threads by channel cid', () { when(() => mockEvent.cid).thenReturn('channel-cid'); handler.onChannelTruncated(mockEvent, mockController); - verify(() => - mockController.deleteThreadByChannelCid(channelCid: 'channel-cid')); + verify(() => mockController.deleteThreadByChannelCid(channelCid: 'channel-cid')); }); test('onMessageRead marks thread as read', () { - when(() => mockThread.copyWith(read: any(named: 'read'))) - .thenReturn(mockThread); + when(() => mockThread.copyWith(read: any(named: 'read'))).thenReturn(mockThread); when(() => mockThread.parentMessageId).thenReturn('parent-id'); when(() => mockEvent.thread).thenReturn(mockThread); when(() => mockEvent.user).thenReturn(mockUser); when(() => mockEvent.createdAt).thenReturn(DateTime.now()); - when(() => mockController.getThread(parentMessageId: 'parent-id')) - .thenReturn(mockThread); + when(() => mockController.getThread(parentMessageId: 'parent-id')).thenReturn(mockThread); when(() => mockController.updateThread(mockThread)).thenReturn(true); handler.onMessageRead(mockEvent, mockController); @@ -208,14 +200,12 @@ void main() { }); test('onNotificationMarkUnread marks thread as unread', () { - when(() => mockThread.copyWith(read: any(named: 'read'))) - .thenReturn(mockThread); + when(() => mockThread.copyWith(read: any(named: 'read'))).thenReturn(mockThread); when(() => mockThread.parentMessageId).thenReturn('parent-id'); when(() => mockEvent.thread).thenReturn(mockThread); when(() => mockEvent.user).thenReturn(mockUser); when(() => mockEvent.createdAt).thenReturn(DateTime.now()); - when(() => mockController.getThread(parentMessageId: 'parent-id')) - .thenReturn(mockThread); + when(() => mockController.getThread(parentMessageId: 'parent-id')).thenReturn(mockThread); when(() => mockController.updateThread(mockThread)).thenReturn(true); handler.onNotificationMarkUnread(mockEvent, mockController); diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 025eebebff..e0694617f3 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,48 +1,104 @@ +## 10.0.0-beta.13 + +🛑️ Breaking +- SDK Redesign Changes. For more details, please refer to the [migration guide](https://github.com/GetStream/stream-chat-flutter/blob/210ff93f955be3f85c62e860309bd9aa240a5446/migrations). + The SDK redesign introduces a fresher default UI, but also better APIs for customization of the components. + +## 10.0.0-beta.12 + +- Included the changes from version [`9.23.0`](https://pub.dev/packages/stream_chat_localizations/changelog). + ## 9.23.0 - Fixed Italian translation for `unreadMessagesSeparatorText` (was incorrectly showing French text "Nouveaux messages" instead of Italian "Nuovi messaggi"). +## 10.0.0-beta.11 + +- Included the changes from version [`9.22.0`](https://pub.dev/packages/stream_chat_localizations/changelog). + ## 9.22.0 - Added translations for new `deletePollOptionLabel` label. - Added translations for new `deletePollOptionQuestion` text. +## 10.0.0-beta.10 + +- Included the changes from version [`9.21.0`](https://pub.dev/packages/stream_chat_localizations/changelog). + ## 9.21.0 - Updated `stream_chat_flutter` dependency to [`9.21.0`](https://pub.dev/packages/stream_chat_flutter/changelog). +## 10.0.0-beta.9 + +- Included the changes from version [`9.20.0`](https://pub.dev/packages/stream_chat_localizations/changelog). + ## 9.20.0 - Updated `stream_chat_flutter` dependency to [`9.20.0`](https://pub.dev/packages/stream_chat_flutter/changelog). +## 10.0.0-beta.8 + +- Included the changes from version [`9.19.0`](https://pub.dev/packages/stream_chat_localizations/changelog). + ## 9.19.0 - Updated `stream_chat_flutter` dependency to [`9.19.0`](https://pub.dev/packages/stream_chat_flutter/changelog). +## 10.0.0-beta.7 + +- Included the changes from version [`9.18.0`](https://pub.dev/packages/stream_chat_localizations/changelog). + ## 9.18.0 - Updated `stream_chat_flutter` dependency to [`9.18.0`](https://pub.dev/packages/stream_chat_flutter/changelog). +## 10.0.0-beta.6 + +- Included the changes from version [`9.17.0`](https://pub.dev/packages/stream_chat_localizations/changelog). + ## 9.17.0 - Updated `stream_chat_flutter` dependency to [`9.17.0`](https://pub.dev/packages/stream_chat_flutter/changelog). +## 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 ce38cf422e..027fa4823c 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -3,16 +3,14 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_localizations/stream_chat_localizations.dart'; -class _NnStreamChatLocalizationsDelegate - extends LocalizationsDelegate { +class _NnStreamChatLocalizationsDelegate extends LocalizationsDelegate { const _NnStreamChatLocalizationsDelegate(); @override bool isSupported(Locale locale) => locale.languageCode == 'nn'; @override - Future load(Locale locale) => - SynchronousFuture(const NnStreamChatLocalizations()); + Future load(Locale locale) => SynchronousFuture(const NnStreamChatLocalizations()); @override bool shouldReload(_NnStreamChatLocalizationsDelegate old) => false; @@ -73,8 +71,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - 'Uploading $remaining/$total ...'; + }) => 'Uploading $remaining/$total ...'; @override String pinnedByUserText({ @@ -87,8 +84,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { } @override - String get sendMessagePermissionError => - "You don't have permission to send messages"; + String get sendMessagePermissionError => "You don't have permission to send messages"; @override String get emptyMessagesText => 'There are no messages currently'; @@ -122,8 +118,8 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String threadSeparatorText(int replyCount) { - if (replyCount == 1) return '1 Reply'; - return '$replyCount Replies'; + if (replyCount == 1) return '1 reply'; + return '$replyCount replies'; } @override @@ -136,7 +132,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String get reconnectingLabel => 'Reconnecting...'; @override - String get alsoSendAsDirectMessageLabel => 'Also send as direct message'; + String get alsoSendAsDirectMessageLabel => 'Also send in Channel'; @override String get addACommentOrSendLabel => 'Add a comment or send'; @@ -161,8 +157,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { 'The file is too large to upload. The file size limit is $limitInMB MB.'; @override - String get couldNotReadBytesFromFileError => - 'Could not read bytes from file.'; + String get couldNotReadBytesFromFileError => 'Could not read bytes from file.'; @override String get addAFileLabel => 'Add a file'; @@ -189,7 +184,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Something went wrong'; @override - String get addMoreFilesLabel => 'Add more files'; + String get addMoreFilesLabel => 'Add more'; @override String get enablePhotoAndVideoAccessMessage => @@ -217,8 +212,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String get flagMessageSuccessfulLabel => 'Message flagged'; @override - String get flagMessageSuccessfulText => - 'The message has been reported to a moderator.'; + String get flagMessageSuccessfulText => 'The message has been reported to a moderator.'; @override String get deleteLabel => 'DELETE'; @@ -227,12 +221,10 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String get deleteMessageLabel => 'Delete Message'; @override - String get deleteMessageQuestion => - 'Are you sure you want to permanently delete this\nmessage?'; + String get deleteMessageQuestion => 'Are you sure you want to permanently delete this\nmessage?'; @override - String get operationCouldNotBeCompletedText => - "The operation couldn't be completed."; + String get operationCouldNotBeCompletedText => "The operation couldn't be completed."; @override String get replyLabel => 'Reply'; @@ -302,8 +294,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String get letsStartChattingLabel => 'Let’s start chatting!'; @override - String get sendingFirstMessageLabel => - 'How about sending your first message to a friend?'; + String get sendingFirstMessageLabel => 'How about sending your first message to a friend?'; @override String get startAChatLabel => 'Start a chat'; @@ -315,8 +306,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String get deleteConversationLabel => 'Delete Conversation'; @override - String get deleteConversationQuestion => - 'Are you sure you want to delete this conversation?'; + String get deleteConversationQuestion => 'Are you sure you want to delete this conversation?'; @override String get streamChatLabel => 'Stream Chat'; @@ -355,8 +345,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String get leaveConversationLabel => 'Leave conversation'; @override - String get leaveConversationQuestion => - 'Are you sure you want to leave this conversation?'; + String get leaveConversationQuestion => 'Are you sure you want to leave this conversation?'; @override String get showInChatLabel => 'Show in Chat'; @@ -392,8 +381,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '$currentPage of $totalPages'; + }) => '$currentPage of $totalPages'; @override String get fileText => 'File'; @@ -402,8 +390,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'Reply to Message'; @override - String attachmentLimitExceedError(int limit) => - 'Attachment limit exceeded, limit: $limit'; + String attachmentLimitExceedError(int limit) => 'Attachment limit exceeded, limit: $limit'; @override String get slowModeOnLabel => 'Slow mode ON'; @@ -457,8 +444,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { } @override - String get linkDisabledDetails => - 'Sending links is not allowed in this conversation.'; + String get linkDisabledDetails => 'Sending links is not allowed in this conversation.'; @override String get linkDisabledError => 'Links are disabled'; @@ -578,15 +564,13 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String get enterYourCommentLabel => 'Enter your comment'; @override - String get endVoteConfirmationText => - 'Are you sure you want to end the poll?'; + String get endVoteConfirmationText => 'Are you sure you want to end the poll?'; @override String get deletePollOptionLabel => 'Delete Option'; @override - String get deletePollOptionQuestion => - 'Are you sure you want to delete this option?'; + String get deletePollOptionQuestion => 'Are you sure you want to delete this option?'; @override String get createLabel => 'Create'; @@ -630,10 +614,10 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String voteCountLabel({int? count}) => switch (count) { - null || < 1 => '0 votes', - 1 => '1 vote', - _ => '$count votes', - }; + null || < 1 => '0 votes', + 1 => '1 vote', + _ => '$count votes', + }; @override String get noPollVotesLabel => 'There are no poll votes currently'; @@ -660,8 +644,7 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String get sendAnywayLabel => 'Send Anyway'; @override - String get moderatedMessageBlockedText => - 'Message was blocked by moderation policies'; + String get moderatedMessageBlockedText => 'Message was blocked by moderation policies'; @override String get moderationReviewModalTitle => 'Are you sure?'; @@ -699,6 +682,75 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String get draftLabel => 'Draft'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return 'Live Location'; + return 'Location'; + } + + @override + String get fileAttachmentText => 'File'; + + @override + String filesAttachmentCountText(int count) { + return count == 1 ? 'File' : '$count files'; + } + + @override + String photosAttachmentCountText(int count) { + return count == 1 ? 'Photo' : '$count photos'; + } + + @override + String videosAttachmentCountText(int count) { + return count == 1 ? 'Video' : '$count videos'; + } + + @override + String get noConversationsYetText => 'No conversations yet'; + + @override + String get replyToStartThreadText => 'Reply to a message to start a thread'; + + @override + String get sendMessageToStartConversationText => 'Send a message to start the conversation'; + + @override + String get savedForLaterLabel => 'Saved for later'; + + @override + String get repliedToThreadAnnotationLabel => 'Replied to a thread'; + + @override + String get alsoSentInChannelAnnotationLabel => 'Also sent in channel'; + + @override + String get viewLabel => 'View'; + + @override + String get reminderSetLabel => 'Reminder set'; + + @override + String reminderAtText(String time) => 'Today at $time'; + + @override + String get createPollPromptLabel => 'Create a poll and let everyone vote!'; + + @override + String get takePhotoAndShareLabel => 'Take a photo and share'; + + @override + String get takeVideoAndShareLabel => 'Take a video and share'; + + @override + String get openCameraLabel => 'Open camera'; + + @override + String get selectFilesToShareLabel => 'Select files to share'; + + @override + String get openFilesLabel => 'Open files'; } void main() async { diff --git a/packages/stream_chat_localizations/example/lib/main.dart b/packages/stream_chat_localizations/example/lib/main.dart index 22abb65fab..0d1b4fbd23 100644 --- a/packages/stream_chat_localizations/example/lib/main.dart +++ b/packages/stream_chat_localizations/example/lib/main.dart @@ -64,32 +64,32 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - // Add all the supported locales - supportedLocales: const [ - Locale('en'), - Locale('hi'), - Locale('fr'), - Locale('it'), - Locale('es'), - Locale('ja'), - Locale('ko'), - Locale('pt'), - ], - // Add GlobalStreamChatLocalizations.delegates - localizationsDelegates: GlobalStreamChatLocalizations.delegates, - // Programatically set the locale (this is a global change) - locale: const Locale('fr'), - builder: (context, widget) => StreamChat( - client: client, - child: widget, - ), - home: StreamChannel( - channel: channel, - child: const ChannelPage(), - ), - ); + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + // Add all the supported locales + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('it'), + Locale('es'), + Locale('ja'), + Locale('ko'), + Locale('pt'), + ], + // Add GlobalStreamChatLocalizations.delegates + localizationsDelegates: GlobalStreamChatLocalizations.delegates, + // Programatically set the locale (this is a global change) + locale: const Locale('fr'), + builder: (context, widget) => StreamChat( + client: client, + child: widget, + ), + home: StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ); } /// A list of messages sent in the current channel. diff --git a/packages/stream_chat_localizations/example/lib/override_lang.dart b/packages/stream_chat_localizations/example/lib/override_lang.dart index 1dc05f9100..9cd76c9849 100644 --- a/packages/stream_chat_localizations/example/lib/override_lang.dart +++ b/packages/stream_chat_localizations/example/lib/override_lang.dart @@ -3,16 +3,14 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_localizations/stream_chat_localizations.dart'; -class _CustomStreamChatLocalizationsDelegate - extends LocalizationsDelegate { +class _CustomStreamChatLocalizationsDelegate extends LocalizationsDelegate { const _CustomStreamChatLocalizationsDelegate(); @override bool isSupported(Locale locale) => locale.languageCode == 'en'; @override - Future load(Locale locale) => - SynchronousFuture(CustomStreamChatLocalizationsEn()); + Future load(Locale locale) => SynchronousFuture(CustomStreamChatLocalizationsEn()); @override bool shouldReload(_CustomStreamChatLocalizationsDelegate old) => false; @@ -89,34 +87,34 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) => MaterialApp( - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - // Add all the supported locales - supportedLocales: const [ - Locale('en'), - Locale('hi'), - Locale('fr'), - Locale('it'), - Locale('es'), - Locale('ja'), - Locale('ko'), - Locale('pt'), - ], - // Add overridden "CustomStreamChatLocalizationsEn.delegate" along with - // "GlobalStreamChatLocalizations.delegates" - localizationsDelegates: const [ - CustomStreamChatLocalizationsEn.delegate, - ...GlobalStreamChatLocalizations.delegates, - ], - builder: (context, widget) => StreamChat( - client: client, - child: widget, - ), - home: StreamChannel( - channel: channel, - child: const ChannelPage(), - ), - ); + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + // Add all the supported locales + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('it'), + Locale('es'), + Locale('ja'), + Locale('ko'), + Locale('pt'), + ], + // Add overridden "CustomStreamChatLocalizationsEn.delegate" along with + // "GlobalStreamChatLocalizations.delegates" + localizationsDelegates: const [ + CustomStreamChatLocalizationsEn.delegate, + ...GlobalStreamChatLocalizations.delegates, + ], + builder: (context, widget) => StreamChat( + client: client, + child: widget, + ), + home: StreamChannel( + channel: channel, + child: const ChannelPage(), + ), + ); } /// A list of messages sent in the current channel. diff --git a/packages/stream_chat_localizations/example/pubspec.yaml b/packages/stream_chat_localizations/example/pubspec.yaml index f7a62ec748..053eb15726 100644 --- a/packages/stream_chat_localizations/example/pubspec.yaml +++ b/packages/stream_chat_localizations/example/pubspec.yaml @@ -17,15 +17,15 @@ version: 1.0.0+1 # 2. Add it to the melos.yaml file for future updates. environment: - sdk: ^3.6.2 - flutter: ">=3.27.4" + sdk: ^3.10.0 + flutter: ">=3.38.1" dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat_flutter: ^9.23.0 - stream_chat_localizations: ^9.23.0 + stream_chat_flutter: ^10.0.0-beta.13 + stream_chat_localizations: ^10.0.0-beta.13 flutter: uses-material-design: true \ No newline at end of file diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index 62248df818..956b906cb4 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -112,8 +112,7 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { /// ) /// ``` /// -abstract class GlobalStreamChatLocalizations - implements StreamChatLocalizations { +abstract class GlobalStreamChatLocalizations implements StreamChatLocalizations { /// Initializes an object that defines the StreamChat widget's localized /// strings for the given `localeName`. const GlobalStreamChatLocalizations({ @@ -129,8 +128,7 @@ abstract class GlobalStreamChatLocalizations /// [GlobalStreamChatLocalizations.delegates] as the value of /// [MaterialApp.localizationsDelegates] to include the localizations for both /// the flutter and stream chat widget libraries. - static const LocalizationsDelegate delegate = - _StreamChatLocalizationsDelegate(); + static const LocalizationsDelegate delegate = _StreamChatLocalizationsDelegate(); /// A value for [MaterialApp.localizationsDelegates] that's typically used by /// internationalized apps. @@ -160,16 +158,13 @@ abstract class GlobalStreamChatLocalizations ]; } -class _StreamChatLocalizationsDelegate - extends LocalizationsDelegate { +class _StreamChatLocalizationsDelegate extends LocalizationsDelegate { const _StreamChatLocalizationsDelegate(); @override - bool isSupported(Locale locale) => - kStreamChatSupportedLanguages.contains(locale.languageCode); + bool isSupported(Locale locale) => kStreamChatSupportedLanguages.contains(locale.languageCode); - static final _loadedTranslations = - >{}; + static final _loadedTranslations = >{}; @override Future load(Locale locale) { @@ -186,6 +181,7 @@ class _StreamChatLocalizationsDelegate bool shouldReload(_StreamChatLocalizationsDelegate old) => false; @override - String toString() => 'GlobalStreamChatLocalizations.delegate(' + String toString() => + 'GlobalStreamChatLocalizations.delegate(' '${kStreamChatSupportedLanguages.length} locales)'; } 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 a71ec593a2..928d3a069d 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`). @@ -49,8 +51,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - 'Transferència en curs $remaining/$total ...'; + }) => 'Transferència en curs $remaining/$total ...'; @override String pinnedByUserText({ @@ -63,8 +64,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { } @override - String get sendMessagePermissionError => - 'No tens permís per enviar missatges'; + String get sendMessagePermissionError => 'No tens permís per enviar missatges'; @override String get emptyMessagesText => 'Actualment no hi ha missatges'; @@ -73,8 +73,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String get genericErrorText => 'Hi ha hagut un problema'; @override - String get loadingMessagesError => - 'Hi ha hagut un error mentre carregava el missatge'; + String get loadingMessagesError => 'Hi ha hagut un error mentre carregava el missatge'; @override String resultCountText(int count) => '$count resultats'; @@ -113,8 +112,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String get reconnectingLabel => 'Reconnectant...'; @override - String get alsoSendAsDirectMessageLabel => - 'Enviar també com a missatge directe'; + String get alsoSendAsDirectMessageLabel => 'Enviar també com a missatge directe'; @override String get addACommentOrSendLabel => 'Afegir un comentari o enviar'; @@ -140,8 +138,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { 'La mida màxima del fitxer és de $limitInMB MB.'; @override - String get couldNotReadBytesFromFileError => - "No s'han pogut llegir els bytes del fitxer."; + String get couldNotReadBytesFromFileError => "No s'han pogut llegir els bytes del fitxer."; @override String get addAFileLabel => 'Afegeix un fitxer'; @@ -168,12 +165,11 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Alguna cosa ha anat malament'; @override - String get addMoreFilesLabel => 'Afegir més fitxers'; + String get addMoreFilesLabel => 'Afegir més'; @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 +179,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'; @@ -196,8 +191,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String get flagMessageSuccessfulLabel => 'Missatge reportat'; @override - String get flagMessageSuccessfulText => - 'Aquest missatge ha estat reportat a un moderador'; + String get flagMessageSuccessfulText => 'Aquest missatge ha estat reportat a un moderador'; @override String get deleteLabel => 'ESBORRA'; @@ -206,12 +200,10 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String get deleteMessageLabel => 'Esborra el missatge'; @override - String get deleteMessageQuestion => - 'Estàs segur que vols esborrar aquest\nmissatge de forma permanent?'; + String get deleteMessageQuestion => 'Estàs segur que vols esborrar aquest missatge de forma permanent?'; @override - String get operationCouldNotBeCompletedText => - "L'operació no s'ha pogut completar"; + String get operationCouldNotBeCompletedText => "L'operació no s'ha pogut completar"; @override String get replyLabel => 'Respondre'; @@ -281,8 +273,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String get letsStartChattingLabel => 'Comencem a parlar!'; @override - String get sendingFirstMessageLabel => - 'Què et sembla enviar el teu primer missatge?'; + String get sendingFirstMessageLabel => 'Què et sembla enviar el teu primer missatge?'; @override String get startAChatLabel => 'Inicia una conversa'; @@ -294,11 +285,10 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String get deleteConversationLabel => 'Esborra la conversa'; @override - String get deleteConversationQuestion => - 'Estàs segur que vols esborrar aquesta conversa?'; + String get deleteConversationQuestion => 'Estàs segur que vols esborrar aquesta conversa?'; @override - String get streamChatLabel => 'Stream Chat'; + String get streamChatLabel => 'Converses'; @override String get searchingForNetworkText => 'Cercant xarxa'; @@ -334,8 +324,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String get leaveConversationLabel => 'Surt de la conversa'; @override - String get leaveConversationQuestion => - "Estàs segur que vols sortir d'aquesta conversa?"; + String get leaveConversationQuestion => "Estàs segur que vols sortir d'aquesta conversa?"; @override String get showInChatLabel => 'Mostra al xat'; @@ -371,8 +360,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '${currentPage + 1} de $totalPages'; + }) => '${currentPage + 1} de $totalPages'; @override String get fileText => 'Fitxer'; @@ -381,8 +369,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'Respondre al missatge'; @override - String attachmentLimitExceedError(int limit) => - 'No és possible afegir més de $limit fitxers adjunts'; + String attachmentLimitExceedError(int limit) => 'No és possible afegir més de $limit fitxers adjunts'; @override String get viewLibrary => 'Veure llibreria'; @@ -439,8 +426,7 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { } @override - String get linkDisabledDetails => - 'No es permet enviar enllaços a aquesta conversa'; + String get linkDisabledDetails => 'No es permet enviar enllaços a aquesta conversa'; @override String get linkDisabledError => 'Els enllaços estan deshabilitats'; @@ -449,8 +435,7 @@ 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"; @@ -559,15 +544,13 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String get enterYourCommentLabel => 'Introdueix el teu comentari'; @override - String get endVoteConfirmationText => - 'Estàs segur que vols finalitzar la votació?'; + String get endVoteConfirmationText => 'Estàs segur que vols finalitzar la votació?'; @override String get deletePollOptionLabel => 'Eliminar opció'; @override - String get deletePollOptionQuestion => - 'Estàs segur que vols eliminar aquesta opció?'; + String get deletePollOptionQuestion => 'Estàs segur que vols eliminar aquesta opció?'; @override String get createLabel => 'Crear'; @@ -611,10 +594,10 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { @override String voteCountLabel({int? count}) => switch (count) { - null || < 1 => '0 vots', - 1 => '1 vot', - _ => '$count vots', - }; + null || < 1 => '0 vots', + 1 => '1 vot', + _ => '$count vots', + }; @override String get noPollVotesLabel => 'No hi ha vots en aquest moment'; @@ -635,15 +618,13 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { String get slideToCancelLabel => 'Llisca per cancel·lar'; @override - String get holdToRecordLabel => - 'Mantén premut per gravar, deixa anar per enviar'; + String get holdToRecordLabel => 'Mantén premut per gravar, deixa anar per enviar'; @override String get sendAnywayLabel => 'Enviar igualment'; @override - String get moderatedMessageBlockedText => - 'Missatge bloquejat per les polítiques de moderació'; + String get moderatedMessageBlockedText => 'Missatge bloquejat per les polítiques de moderació'; @override String get moderationReviewModalTitle => 'Estàs segur?'; @@ -667,6 +648,18 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { @override String get videoAttachmentText => 'Vídeo'; + @override + String get fileAttachmentText => 'Fitxer'; + + @override + String filesAttachmentCountText(int count) => count == 1 ? 'Fitxer' : '$count fitxers'; + + @override + String photosAttachmentCountText(int count) => count == 1 ? 'Foto' : '$count fotos'; + + @override + String videosAttachmentCountText(int count) => count == 1 ? 'Vídeo' : '$count vídeos'; + @override String get pollYouVotedText => 'Has votat'; @@ -681,4 +674,55 @@ class StreamChatLocalizationsCa extends GlobalStreamChatLocalizations { @override String get draftLabel => 'Esborrany'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return 'Ubicació en directe'; + return 'Ubicació'; + } + + @override + String get noConversationsYetText => 'Encara no hi ha converses'; + + @override + String get replyToStartThreadText => 'Respon a un missatge per iniciar un fil'; + + @override + String get sendMessageToStartConversationText => 'Envia un missatge per iniciar la conversa'; + + @override + String get savedForLaterLabel => 'Desat per a més tard'; + + @override + String get repliedToThreadAnnotationLabel => 'Ha respost a un fil'; + + @override + String get alsoSentInChannelAnnotationLabel => 'També enviat al canal'; + + @override + String get viewLabel => 'Veure'; + + @override + String get reminderSetLabel => 'Recordatori establert'; + + @override + String reminderAtText(String time) => 'Avui a les $time'; + + @override + String get createPollPromptLabel => 'Crea una enquesta i deixa que tothom voti!'; + + @override + String get takePhotoAndShareLabel => 'Fes una foto i comparteix'; + + @override + String get takeVideoAndShareLabel => 'Grava un vídeo i comparteix'; + + @override + String get openCameraLabel => 'Obrir càmera'; + + @override + String get selectFilesToShareLabel => 'Seleccioneu fitxers per compartir'; + + @override + String get openFilesLabel => 'Obrir fitxers'; } 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 6ca4153625..51a6edd030 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`). @@ -49,8 +51,7 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - 'Hochladen $remaining/$total ...'; + }) => 'Hochladen $remaining/$total ...'; @override String pinnedByUserText({ @@ -158,12 +159,11 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Etwas ist schief gelaufen'; @override - String get addMoreFilesLabel => 'Weitere Dateien hinzufügen'; + String get addMoreFilesLabel => 'Mehr hinzufügen'; @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 +173,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'; @@ -186,8 +185,7 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String get flagMessageSuccessfulLabel => 'Nachricht gemeldet'; @override - String get flagMessageSuccessfulText => - 'Die Nachricht wurde an einen Moderator weitergeleitet.'; + String get flagMessageSuccessfulText => 'Die Nachricht wurde an einen Moderator weitergeleitet.'; @override String get deleteLabel => 'LÖSCHEN'; @@ -196,12 +194,10 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String get deleteMessageLabel => 'Nachricht löschen'; @override - String get deleteMessageQuestion => - 'Sind Sie sicher, dass Sie diese Nachricht endgültig löschen wollen?'; + String get deleteMessageQuestion => 'Sind Sie sicher, dass Sie diese Nachricht endgültig löschen wollen?'; @override - String get operationCouldNotBeCompletedText => - 'Die Operation konnte nicht abgeschlossen werden.'; + String get operationCouldNotBeCompletedText => 'Die Operation konnte nicht abgeschlossen werden.'; @override String get replyLabel => 'Antwort'; @@ -271,7 +267,8 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String get letsStartChattingLabel => 'Lass uns anfangen zu chatten!'; @override - String get sendingFirstMessageLabel => 'Wie wäre es, wenn Sie Ihre erste ' + String get sendingFirstMessageLabel => + 'Wie wäre es, wenn Sie Ihre erste ' 'Nachricht an einen Freund senden würden?'; @override @@ -284,11 +281,10 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String get deleteConversationLabel => 'Unterhaltung löschen'; @override - String get deleteConversationQuestion => - 'Sind Sie sicher, dass Sie diese Unterhaltung löschen wollen?'; + String get deleteConversationQuestion => 'Sind Sie sicher, dass Sie diese Unterhaltung löschen wollen?'; @override - String get streamChatLabel => 'Stream Chat'; + String get streamChatLabel => 'Unterhaltungen'; @override String get searchingForNetworkText => 'Netzwerk wird gesucht'; @@ -323,8 +319,7 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String get leaveConversationLabel => 'Unterhaltung verlassen'; @override - String get leaveConversationQuestion => - 'Sind Sie sicher, dass Sie diese Unterhaltung verlassen wollen?'; + String get leaveConversationQuestion => 'Sind Sie sicher, dass Sie diese Unterhaltung verlassen wollen?'; @override String get showInChatLabel => 'Im Chat anzeigen'; @@ -360,8 +355,7 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '${currentPage + 1} von $totalPages'; + }) => '${currentPage + 1} von $totalPages'; @override String get fileText => 'Datei'; @@ -370,26 +364,22 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'Auf Nachricht antworten'; @override - String attachmentLimitExceedError(int limit) => - 'Dateigröße überschritten, Grenze: $limit'; + String attachmentLimitExceedError(int limit) => 'Dateigröße überschritten, Grenze: $limit'; @override String get slowModeOnLabel => 'Langsamer Modus: EIN'; @override - String get linkDisabledDetails => - 'Das Senden von Links ist in dieser Konversation nicht erlaubt.'; + String get linkDisabledDetails => 'Das Senden von Links ist in dieser Konversation nicht erlaubt.'; @override String get linkDisabledError => 'Verknüpfungen sind deaktiviert'; @override - String get sendMessagePermissionError => - 'Sie sind nicht berechtigt Nachrichten zu senden'; + String get sendMessagePermissionError => 'Sie sind nicht berechtigt Nachrichten zu senden'; @override - String get couldNotReadBytesFromFileError => - 'Kan bytes niet uit bestand lezen.'; + String get couldNotReadBytesFromFileError => 'Kan bytes niet uit bestand lezen.'; @override String get downloadLabel => 'Downloaden'; @@ -443,8 +433,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'; @@ -553,15 +542,13 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String get enterYourCommentLabel => 'Geben Sie Ihren Kommentar ein'; @override - String get endVoteConfirmationText => - 'Sind Sie sicher, dass Sie die Abstimmung beenden möchten?'; + String get endVoteConfirmationText => 'Sind Sie sicher, dass Sie die Abstimmung beenden möchten?'; @override String get deletePollOptionLabel => 'Option löschen'; @override - String get deletePollOptionQuestion => - 'Sind Sie sicher, dass Sie diese Option löschen möchten?'; + String get deletePollOptionQuestion => 'Sind Sie sicher, dass Sie diese Option löschen möchten?'; @override String get createLabel => 'Erstellen'; @@ -605,10 +592,10 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { @override String voteCountLabel({int? count}) => switch (count) { - null || < 1 => '0 Stimmen', - 1 => '1 Stimme', - _ => '$count Stimmen', - }; + null || < 1 => '0 Stimmen', + 1 => '1 Stimme', + _ => '$count Stimmen', + }; @override String get noPollVotesLabel => 'Derzeit keine Umfrage-Stimmen'; @@ -635,8 +622,7 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { String get sendAnywayLabel => 'Trotzdem senden'; @override - String get moderatedMessageBlockedText => - 'Nachricht wurde durch Moderationsrichtlinien blockiert'; + String get moderatedMessageBlockedText => 'Nachricht wurde durch Moderationsrichtlinien blockiert'; @override String get moderationReviewModalTitle => 'Bist du sicher?'; @@ -660,6 +646,18 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { @override String get videoAttachmentText => 'Video'; + @override + String get fileAttachmentText => 'Datei'; + + @override + String filesAttachmentCountText(int count) => count == 1 ? 'Datei' : '$count Dateien'; + + @override + String photosAttachmentCountText(int count) => count == 1 ? 'Foto' : '$count Fotos'; + + @override + String videosAttachmentCountText(int count) => count == 1 ? 'Video' : '$count Videos'; + @override String get pollYouVotedText => 'Du hast abgestimmt'; @@ -674,4 +672,55 @@ class StreamChatLocalizationsDe extends GlobalStreamChatLocalizations { @override String get draftLabel => 'Entwurf'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return 'Live-Standort'; + return 'Standort'; + } + + @override + String get noConversationsYetText => 'Noch keine Unterhaltungen'; + + @override + String get replyToStartThreadText => 'Antworten Sie auf eine Nachricht, um einen Thread zu starten'; + + @override + String get sendMessageToStartConversationText => 'Senden Sie eine Nachricht, um die Unterhaltung zu starten'; + + @override + String get savedForLaterLabel => 'Für später gespeichert'; + + @override + String get repliedToThreadAnnotationLabel => 'Auf einen Thread geantwortet'; + + @override + String get alsoSentInChannelAnnotationLabel => 'Auch im Kanal gesendet'; + + @override + String get viewLabel => 'Anzeigen'; + + @override + String get reminderSetLabel => 'Erinnerung gesetzt'; + + @override + String reminderAtText(String time) => 'Heute um $time'; + + @override + String get createPollPromptLabel => 'Erstelle eine Umfrage und lass alle abstimmen!'; + + @override + String get takePhotoAndShareLabel => 'Foto aufnehmen und teilen'; + + @override + String get takeVideoAndShareLabel => 'Video aufnehmen und teilen'; + + @override + String get openCameraLabel => 'Kamera öffnen'; + + @override + String get selectFilesToShareLabel => 'Dateien zum Teilen auswählen'; + + @override + String get openFilesLabel => 'Dateien öffnen'; } 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 f26fd20a4e..85cc7f795b 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`). @@ -37,20 +39,19 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { } @override - String get threadReplyLabel => 'Thread Reply'; + String get threadReplyLabel => 'Thread'; @override String get onlyVisibleToYouText => 'Only visible to you'; @override - String threadReplyCountText(int count) => '$count Thread Replies'; + String threadReplyCountText(int count) => count == 1 ? '1 reply' : '$count replies'; @override String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - 'Uploading $remaining/$total ...'; + }) => 'Uploading $remaining/$total ...'; @override String pinnedByUserText({ @@ -63,8 +64,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { } @override - String get sendMessagePermissionError => - "You don't have permission to send messages"; + String get sendMessagePermissionError => "You don't have permission to send messages"; @override String get emptyMessagesText => 'There are no messages currently'; @@ -98,8 +98,8 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String threadSeparatorText(int replyCount) { - if (replyCount == 1) return '1 Reply'; - return '$replyCount Replies'; + if (replyCount == 1) return '1 reply'; + return '$replyCount replies'; } @override @@ -112,7 +112,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String get reconnectingLabel => 'Reconnecting...'; @override - String get alsoSendAsDirectMessageLabel => 'Also send as direct message'; + String get alsoSendAsDirectMessageLabel => 'Also send in Channel'; @override String get addACommentOrSendLabel => 'Add a comment or send'; @@ -137,8 +137,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { 'The file is too large to upload. The file size limit is $limitInMB MB.'; @override - String get couldNotReadBytesFromFileError => - 'Could not read bytes from file.'; + String get couldNotReadBytesFromFileError => 'Could not read bytes from file.'; @override String get addAFileLabel => 'Add a file'; @@ -165,12 +164,11 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Something went wrong'; @override - String get addMoreFilesLabel => 'Add more files'; + String get addMoreFilesLabel => 'Add more'; @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 +178,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'; @@ -193,8 +190,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String get flagMessageSuccessfulLabel => 'Message flagged'; @override - String get flagMessageSuccessfulText => - 'The message has been reported to a moderator.'; + String get flagMessageSuccessfulText => 'The message has been reported to a moderator.'; @override String get deleteLabel => 'DELETE'; @@ -203,12 +199,10 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String get deleteMessageLabel => 'Delete Message'; @override - String get deleteMessageQuestion => - 'Are you sure you want to permanently delete this\nmessage?'; + String get deleteMessageQuestion => 'Are you sure you want to permanently delete this message?'; @override - String get operationCouldNotBeCompletedText => - "The operation couldn't be completed."; + String get operationCouldNotBeCompletedText => "The operation couldn't be completed."; @override String get replyLabel => 'Reply'; @@ -278,8 +272,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String get letsStartChattingLabel => 'Let’s start chatting!'; @override - String get sendingFirstMessageLabel => - 'How about sending your first message to a friend?'; + String get sendingFirstMessageLabel => 'How about sending your first message to a friend?'; @override String get startAChatLabel => 'Start a chat'; @@ -291,11 +284,10 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String get deleteConversationLabel => 'Delete Conversation'; @override - String get deleteConversationQuestion => - 'Are you sure you want to delete this conversation?'; + String get deleteConversationQuestion => 'Are you sure you want to delete this conversation?'; @override - String get streamChatLabel => 'Stream Chat'; + String get streamChatLabel => 'Chats'; @override String get searchingForNetworkText => 'Searching for Network'; @@ -331,8 +323,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String get leaveConversationLabel => 'Leave conversation'; @override - String get leaveConversationQuestion => - 'Are you sure you want to leave this conversation?'; + String get leaveConversationQuestion => 'Are you sure you want to leave this conversation?'; @override String get showInChatLabel => 'Show in Chat'; @@ -368,8 +359,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '${currentPage + 1} of $totalPages'; + }) => '${currentPage + 1} of $totalPages'; @override String get fileText => 'File'; @@ -378,8 +368,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'Reply to Message'; @override - String attachmentLimitExceedError(int limit) => - 'Attachment limit exceeded, limit: $limit'; + String attachmentLimitExceedError(int limit) => 'Attachment limit exceeded, limit: $limit'; @override String get slowModeOnLabel => 'Slow mode ON'; @@ -433,8 +422,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { } @override - String get linkDisabledDetails => - 'Sending links is not allowed in this conversation.'; + String get linkDisabledDetails => 'Sending links is not allowed in this conversation.'; @override String get linkDisabledError => 'Links are disabled'; @@ -446,8 +434,7 @@ 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'; @@ -555,15 +542,13 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String get enterYourCommentLabel => 'Enter your comment'; @override - String get endVoteConfirmationText => - 'Are you sure you want to end the vote?'; + String get endVoteConfirmationText => 'Are you sure you want to end the vote?'; @override String get deletePollOptionLabel => 'Delete Option'; @override - String get deletePollOptionQuestion => - 'Are you sure you want to delete this option?'; + String get deletePollOptionQuestion => 'Are you sure you want to delete this option?'; @override String get createLabel => 'Create'; @@ -607,10 +592,10 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String voteCountLabel({int? count}) => switch (count) { - null || < 1 => '0 votes', - 1 => '1 vote', - _ => '$count votes', - }; + null || < 1 => '0 votes', + 1 => '1 vote', + _ => '$count votes', + }; @override String get noPollVotesLabel => 'There are no poll votes currently'; @@ -637,8 +622,7 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { String get sendAnywayLabel => 'Send Anyway'; @override - String get moderatedMessageBlockedText => - 'Message was blocked by moderation policies'; + String get moderatedMessageBlockedText => 'Message was blocked by moderation policies'; @override String get moderationReviewModalTitle => 'Are you sure?'; @@ -662,6 +646,18 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get videoAttachmentText => 'Video'; + @override + String get fileAttachmentText => 'File'; + + @override + String filesAttachmentCountText(int count) => count == 1 ? 'File' : '$count files'; + + @override + String photosAttachmentCountText(int count) => count == 1 ? 'Photo' : '$count photos'; + + @override + String videosAttachmentCountText(int count) => count == 1 ? 'Video' : '$count videos'; + @override String get pollYouVotedText => 'You voted'; @@ -676,4 +672,55 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get draftLabel => 'Draft'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return 'Live Location'; + return 'Location'; + } + + @override + String get noConversationsYetText => 'No conversations yet'; + + @override + String get replyToStartThreadText => 'Reply to a message to start a thread'; + + @override + String get sendMessageToStartConversationText => 'Send a message to start the conversation'; + + @override + String get savedForLaterLabel => 'Saved for later'; + + @override + String get repliedToThreadAnnotationLabel => 'Replied to a thread'; + + @override + String get alsoSentInChannelAnnotationLabel => 'Also sent in channel'; + + @override + String get viewLabel => 'View'; + + @override + String get reminderSetLabel => 'Reminder set'; + + @override + String reminderAtText(String time) => 'Today at $time'; + + @override + String get createPollPromptLabel => 'Create a poll and let everyone vote!'; + + @override + String get takePhotoAndShareLabel => 'Take a photo and share'; + + @override + String get takeVideoAndShareLabel => 'Take a video and share'; + + @override + String get openCameraLabel => 'Open camera'; + + @override + String get selectFilesToShareLabel => 'Select files to share'; + + @override + String get openFilesLabel => 'Open files'; } 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 e1587597c7..387d7c4cde 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`). @@ -43,15 +45,13 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get onlyVisibleToYouText => 'Sólo visible para usted'; @override - String threadReplyCountText(int count) => - '$count respuestas al hilo de discusión'; + String threadReplyCountText(int count) => '$count respuestas al hilo de discusión'; @override String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - 'Transferencia en curso $remaining/$total ...'; + }) => 'Transferencia en curso $remaining/$total ...'; @override String pinnedByUserText({ @@ -64,8 +64,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { } @override - String get sendMessagePermissionError => - 'No tienes permiso para enviar mensajes'; + String get sendMessagePermissionError => 'No tienes permiso para enviar mensajes'; @override String get emptyMessagesText => 'Actualmente no hay mensajes'; @@ -74,8 +73,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get genericErrorText => 'Hubo un problema'; @override - String get loadingMessagesError => - 'Hubo un error mientras se cargaba el mensaje'; + String get loadingMessagesError => 'Hubo un error mientras se cargaba el mensaje'; @override String resultCountText(int count) => '$count resultados'; @@ -114,8 +112,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get reconnectingLabel => 'Reconectando...'; @override - String get alsoSendAsDirectMessageLabel => - 'Enviar también como mensaje directo'; + String get alsoSendAsDirectMessageLabel => 'Enviar también como mensaje directo'; @override String get addACommentOrSendLabel => 'Añadir un comentario o enviar'; @@ -141,8 +138,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { 'El límite de tamaño de los archivos es de $limitInMB MB.'; @override - String get couldNotReadBytesFromFileError => - 'No se pudieron leer los bytes del archivo.'; + String get couldNotReadBytesFromFileError => 'No se pudieron leer los bytes del archivo.'; @override String get addAFileLabel => 'Añadir un archivo'; @@ -169,12 +165,11 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Algo ha salido mal'; @override - String get addMoreFilesLabel => 'Añadir más archivos'; + String get addMoreFilesLabel => 'Añadir más'; @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 +179,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'; @@ -197,8 +191,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get flagMessageSuccessfulLabel => 'Mensaje reportado'; @override - String get flagMessageSuccessfulText => - 'Este mensaje ha sido reportado a un moderador.'; + String get flagMessageSuccessfulText => 'Este mensaje ha sido reportado a un moderador.'; @override String get deleteLabel => 'BORRAR'; @@ -207,12 +200,10 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get deleteMessageLabel => 'Borrar el mensaje'; @override - String get deleteMessageQuestion => - '¿Estás seguro de que quieres borrar este\nmensaje de forma permanente?'; + String get deleteMessageQuestion => '¿Estás seguro de que quieres borrar este mensaje de forma permanente?'; @override - String get operationCouldNotBeCompletedText => - 'La operación no pudo completarse.'; + String get operationCouldNotBeCompletedText => 'La operación no pudo completarse.'; @override String get replyLabel => 'Responder'; @@ -282,8 +273,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get letsStartChattingLabel => '¡Empecemos a charlar!'; @override - String get sendingFirstMessageLabel => - '¿Qué le parece enviar su primer mensaje a un amigo?'; + String get sendingFirstMessageLabel => '¿Qué le parece enviar su primer mensaje a un amigo?'; @override String get startAChatLabel => 'Iniciar una conversación'; @@ -295,11 +285,10 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get deleteConversationLabel => 'Borrar la conversación'; @override - String get deleteConversationQuestion => - '¿Estás seguro de que quieres borrar esta conversación?'; + String get deleteConversationQuestion => '¿Estás seguro de que quieres borrar esta conversación?'; @override - String get streamChatLabel => 'Stream Chat'; + String get streamChatLabel => 'Conversaciones'; @override String get searchingForNetworkText => 'Buscando red'; @@ -335,8 +324,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get leaveConversationLabel => 'Salir de la conversación'; @override - String get leaveConversationQuestion => - '¿Estás seguro de que quiere salir de esta conversación?'; + String get leaveConversationQuestion => '¿Estás seguro de que quiere salir de esta conversación?'; @override String get showInChatLabel => 'Mostrar en el chat'; @@ -372,8 +360,7 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '${currentPage + 1} de $totalPages'; + }) => '${currentPage + 1} de $totalPages'; @override String get fileText => 'Archivo'; @@ -382,7 +369,8 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'Responder al Mensaje'; @override - String attachmentLimitExceedError(int limit) => ''' + String attachmentLimitExceedError(int limit) => + ''' No es posible añadir más de $limit archivos adjuntos '''; @@ -441,8 +429,7 @@ No es posible añadir más de $limit archivos adjuntos } @override - String get linkDisabledDetails => - 'No se permite enviar enlaces en esta conversación.'; + String get linkDisabledDetails => 'No se permite enviar enlaces en esta conversación.'; @override String get linkDisabledError => 'Los enlaces están deshabilitados'; @@ -451,8 +438,7 @@ 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'; @@ -560,15 +546,13 @@ No es posible añadir más de $limit archivos adjuntos String get enterYourCommentLabel => 'Ingresa tu comentario'; @override - String get endVoteConfirmationText => - '¿Estás seguro de que quieres finalizar la votación?'; + String get endVoteConfirmationText => '¿Estás seguro de que quieres finalizar la votación?'; @override String get deletePollOptionLabel => 'Eliminar opción'; @override - String get deletePollOptionQuestion => - '¿Estás seguro de que quieres eliminar esta opción?'; + String get deletePollOptionQuestion => '¿Estás seguro de que quieres eliminar esta opción?'; @override String get createLabel => 'Crear'; @@ -612,17 +596,16 @@ No es posible añadir más de $limit archivos adjuntos @override String voteCountLabel({int? count}) => switch (count) { - null || < 1 => '0 votos', - 1 => '1 voto', - _ => '$count votos', - }; + null || < 1 => '0 votos', + 1 => '1 voto', + _ => '$count votos', + }; @override String get noPollVotesLabel => 'No hay votos en la encuesta actualmente'; @override - String get loadingPollVotesError => - 'Error al cargar los votos de la encuesta'; + String get loadingPollVotesError => 'Error al cargar los votos de la encuesta'; @override String get repliedToLabel => 'respondido a:'; @@ -637,15 +620,13 @@ No es posible añadir más de $limit archivos adjuntos String get slideToCancelLabel => 'Desliza para cancelar'; @override - String get holdToRecordLabel => - 'Mantén pulsado para grabar, suelta para enviar'; + String get holdToRecordLabel => 'Mantén pulsado para grabar, suelta para enviar'; @override String get sendAnywayLabel => 'Enviar de todos modos'; @override - String get moderatedMessageBlockedText => - 'Mensaje bloqueado por políticas de moderación'; + String get moderatedMessageBlockedText => 'Mensaje bloqueado por políticas de moderación'; @override String get moderationReviewModalTitle => '¿Estás seguro?'; @@ -669,6 +650,18 @@ No es posible añadir más de $limit archivos adjuntos @override String get videoAttachmentText => 'Video'; + @override + String get fileAttachmentText => 'Archivo'; + + @override + String filesAttachmentCountText(int count) => count == 1 ? 'Archivo' : '$count archivos'; + + @override + String photosAttachmentCountText(int count) => count == 1 ? 'Foto' : '$count fotos'; + + @override + String videosAttachmentCountText(int count) => count == 1 ? 'Vídeo' : '$count vídeos'; + @override String get pollYouVotedText => 'Has votado'; @@ -683,4 +676,55 @@ 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'; + } + + @override + String get noConversationsYetText => 'Aún no hay conversaciones'; + + @override + String get replyToStartThreadText => 'Responde a un mensaje para iniciar un hilo'; + + @override + String get sendMessageToStartConversationText => 'Envía un mensaje para iniciar la conversación'; + + @override + String get savedForLaterLabel => 'Guardado para después'; + + @override + String get repliedToThreadAnnotationLabel => 'Respondió a un hilo'; + + @override + String get alsoSentInChannelAnnotationLabel => 'También enviado en el canal'; + + @override + String get viewLabel => 'Ver'; + + @override + String get reminderSetLabel => 'Recordatorio establecido'; + + @override + String reminderAtText(String time) => 'Hoy a las $time'; + + @override + String get createPollPromptLabel => '¡Crea una encuesta y deja que todos voten!'; + + @override + String get takePhotoAndShareLabel => 'Toma una foto y comparte'; + + @override + String get takeVideoAndShareLabel => 'Graba un video y comparte'; + + @override + String get openCameraLabel => 'Abrir cámara'; + + @override + String get selectFilesToShareLabel => 'Selecciona archivos para compartir'; + + @override + String get openFilesLabel => 'Abrir archivos'; } 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 7d210044ed..c67308f155 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`). @@ -43,15 +45,13 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get onlyVisibleToYouText => 'Seulement visible par vous'; @override - String threadReplyCountText(int count) => - '$count Réponses au fil de discussion'; + String threadReplyCountText(int count) => '$count Réponses au fil de discussion'; @override String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - 'Transfert en cours $remaining/$total ...'; + }) => 'Transfert en cours $remaining/$total ...'; @override String pinnedByUserText({ @@ -64,8 +64,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { } @override - String get sendMessagePermissionError => - "Vous n'êtes pas autorisé à envoyer des messages"; + String get sendMessagePermissionError => "Vous n'êtes pas autorisé à envoyer des messages"; @override String get emptyMessagesText => "Il n'y a pas de messages actuellement"; @@ -113,8 +112,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get reconnectingLabel => 'Reconnexion...'; @override - String get alsoSendAsDirectMessageLabel => - 'Envoyer aussi comme message direct'; + String get alsoSendAsDirectMessageLabel => 'Envoyer aussi comme message direct'; @override String get addACommentOrSendLabel => 'Ajouter un commentaire ou envoyer'; @@ -140,8 +138,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { 'La taille limite du fichier est de $limitInMB Mo.'; @override - String get couldNotReadBytesFromFileError => - 'Impossible de lire les octets du fichier.'; + String get couldNotReadBytesFromFileError => 'Impossible de lire les octets du fichier.'; @override String get addAFileLabel => 'Ajouter un fichier'; @@ -168,12 +165,11 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Quelque chose a mal tourné'; @override - String get addMoreFilesLabel => "Ajouter d'autres fichiers"; + String get addMoreFilesLabel => 'Ajouter plus'; @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 +179,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'; @@ -196,8 +191,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get flagMessageSuccessfulLabel => 'Message signalé'; @override - String get flagMessageSuccessfulText => - 'Ce message a été signalé à un modérateur.'; + String get flagMessageSuccessfulText => 'Ce message a été signalé à un modérateur.'; @override String get deleteLabel => 'SUPPRIMER'; @@ -206,12 +200,10 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get deleteMessageLabel => 'Supprimer le message'; @override - String get deleteMessageQuestion => - 'Êtes-vous sûr de vouloir supprimer définitivement ce\nmessage ?'; + String get deleteMessageQuestion => 'Êtes-vous sûr de vouloir supprimer définitivement ce message ?'; @override - String get operationCouldNotBeCompletedText => - "L'opération n'a pas pu être terminée."; + String get operationCouldNotBeCompletedText => "L'opération n'a pas pu être terminée."; @override String get replyLabel => 'Répondre'; @@ -281,8 +273,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get letsStartChattingLabel => 'Commençons à discuter !'; @override - String get sendingFirstMessageLabel => - "Que diriez-vous d'envoyer votre premier message à un ami ?"; + String get sendingFirstMessageLabel => "Que diriez-vous d'envoyer votre premier message à un ami ?"; @override String get startAChatLabel => 'Commencer une discussion'; @@ -294,11 +285,10 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get deleteConversationLabel => 'Supprimer la conversation'; @override - String get deleteConversationQuestion => - 'Vous êtes sûr de vouloir supprimer cette conversation ?'; + String get deleteConversationQuestion => 'Vous êtes sûr de vouloir supprimer cette conversation ?'; @override - String get streamChatLabel => 'Stream Chat'; + String get streamChatLabel => 'Conversations'; @override String get searchingForNetworkText => 'Recherche de réseau'; @@ -334,8 +324,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get leaveConversationLabel => 'Quitter la conversation'; @override - String get leaveConversationQuestion => - 'Etes-vous sûr de vouloir quitter cette conversation ?'; + String get leaveConversationQuestion => 'Etes-vous sûr de vouloir quitter cette conversation ?'; @override String get showInChatLabel => 'Montrer dans la Discussion'; @@ -371,8 +360,7 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '${currentPage + 1} de $totalPages'; + }) => '${currentPage + 1} de $totalPages'; @override String get fileText => 'Fichier'; @@ -381,7 +369,8 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'Répondre au Message'; @override - String attachmentLimitExceedError(int limit) => ''' + String attachmentLimitExceedError(int limit) => + ''' Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $limit pièces jointes '''; @@ -440,8 +429,7 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ } @override - String get linkDisabledDetails => - "L'envoi de liens n'est pas autorisé dans cette conversation."; + String get linkDisabledDetails => "L'envoi de liens n'est pas autorisé dans cette conversation."; @override String get linkDisabledError => 'Les liens sont désactivés'; @@ -451,8 +439,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"; @@ -519,8 +506,7 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ String get multipleAnswersLabel => 'Réponses multiples'; @override - String get maximumVotesPerPersonLabel => - 'Nombre maximum de votes par personne'; + String get maximumVotesPerPersonLabel => 'Nombre maximum de votes par personne'; @override String? maxVotesPerPersonValidationError(int votes, Range range) { @@ -562,15 +548,13 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ String get enterYourCommentLabel => 'Entrez votre commentaire'; @override - String get endVoteConfirmationText => - 'Êtes-vous sûr de vouloir terminer le vote?'; + String get endVoteConfirmationText => 'Êtes-vous sûr de vouloir terminer le vote?'; @override String get deletePollOptionLabel => "Supprimer l'option"; @override - String get deletePollOptionQuestion => - 'Êtes-vous sûr de vouloir supprimer cette option ?'; + String get deletePollOptionQuestion => 'Êtes-vous sûr de vouloir supprimer cette option ?'; @override String get createLabel => 'Créer'; @@ -614,18 +598,16 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ @override String voteCountLabel({int? count}) => switch (count) { - null || < 1 => '0 vote', - 1 => '1 vote', - _ => '$count votes', - }; + null || < 1 => '0 vote', + 1 => '1 vote', + _ => '$count votes', + }; @override - String get noPollVotesLabel => - "Il n'y a pas de votes de sondage actuellement"; + String get noPollVotesLabel => "Il n'y a pas de votes de sondage actuellement"; @override - String get loadingPollVotesError => - 'Erreur de chargement des votes du sondage'; + String get loadingPollVotesError => 'Erreur de chargement des votes du sondage'; @override String get repliedToLabel => 'répondu à:'; @@ -640,15 +622,13 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ String get slideToCancelLabel => 'Glissez pour annuler'; @override - String get holdToRecordLabel => - 'Maintenez pour enregistrer, relâchez pour envoyer'; + String get holdToRecordLabel => 'Maintenez pour enregistrer, relâchez pour envoyer'; @override String get sendAnywayLabel => 'Envoyer quand même'; @override - String get moderatedMessageBlockedText => - 'Message bloqué par les politiques de modération'; + String get moderatedMessageBlockedText => 'Message bloqué par les politiques de modération'; @override String get moderationReviewModalTitle => 'Êtes-vous sûr ?'; @@ -672,6 +652,18 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ @override String get videoAttachmentText => 'Vidéo'; + @override + String get fileAttachmentText => 'Fichier'; + + @override + String filesAttachmentCountText(int count) => count == 1 ? 'Fichier' : '$count fichiers'; + + @override + String photosAttachmentCountText(int count) => count == 1 ? 'Photo' : '$count photos'; + + @override + String videosAttachmentCountText(int count) => count == 1 ? 'Vidéo' : '$count vidéos'; + @override String get pollYouVotedText => 'Vous avez voté'; @@ -686,4 +678,55 @@ 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'; + } + + @override + String get noConversationsYetText => 'Pas encore de conversations'; + + @override + String get replyToStartThreadText => 'Répondez à un message pour démarrer un fil'; + + @override + String get sendMessageToStartConversationText => 'Envoyez un message pour démarrer la conversation'; + + @override + String get savedForLaterLabel => 'Enregistré pour plus tard'; + + @override + String get repliedToThreadAnnotationLabel => 'A répondu à un fil'; + + @override + String get alsoSentInChannelAnnotationLabel => 'Également envoyé dans le canal'; + + @override + String get viewLabel => 'Voir'; + + @override + String get reminderSetLabel => 'Rappel défini'; + + @override + String reminderAtText(String time) => "Aujourd'hui à $time"; + + @override + String get createPollPromptLabel => 'Créez un sondage et laissez tout le monde voter !'; + + @override + String get takePhotoAndShareLabel => 'Prendre une photo et partager'; + + @override + String get takeVideoAndShareLabel => 'Prendre une vidéo et partager'; + + @override + String get openCameraLabel => 'Ouvrir la caméra'; + + @override + String get selectFilesToShareLabel => 'Sélectionnez des fichiers à partager'; + + @override + String get openFilesLabel => 'Ouvrir des fichiers'; } 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 e4120b3782..5829f937d8 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`). @@ -49,8 +51,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - 'अपलोडिंग $remaining/$total ...'; + }) => 'अपलोडिंग $remaining/$total ...'; @override String pinnedByUserText({ @@ -163,12 +164,11 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'लोड करने में समस्या'; @override - String get addMoreFilesLabel => 'और फ़ाइलें जोड़ें'; + String get addMoreFilesLabel => 'और जोड़ें'; @override String get enablePhotoAndVideoAccessMessage => - 'कृपया अपने फ़ोटो और वीडियो तक पहुंच सक्षम करें' - '\nताकि आप उन्हें मित्रों के साथ साझा कर सकें।'; + 'कृपया अपने फ़ोटो और वीडियो तक पहुंच सक्षम करे ताकि आप उन्हें मित्रों के साथ साझा कर सकें।'; @override String get allowGalleryAccessMessage => 'अपनी गैलरी तक पहुंच की अनुमति दें'; @@ -177,8 +177,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get flagMessageLabel => 'फ्लैग संदेश'; @override - String get flagMessageQuestion => 'क्या आप आगे की जांच के लिए इस संदेश की' - '\nएक प्रति मॉडरेटर को भेजना चाहते हैं?'; + String get flagMessageQuestion => 'क्या आप आगे की जांच के लिए इस संदेश की एक प्रति मॉडरेटर को भेजना चाहते हैं?'; @override String get flagLabel => 'फ्लैग'; @@ -190,8 +189,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get flagMessageSuccessfulLabel => 'संदेश फ्लैग हो गया'; @override - String get flagMessageSuccessfulText => - 'संदेश की रिपोर्ट एक मॉडरेटर को कर दी गई है।'; + String get flagMessageSuccessfulText => 'संदेश की रिपोर्ट एक मॉडरेटर को कर दी गई है।'; @override String get deleteLabel => 'हटाएँ'; @@ -200,12 +198,10 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get deleteMessageLabel => 'संदेश हटाएं'; @override - String get deleteMessageQuestion => - 'क्या आप वाकई इस संदेश को स्थायी रूप से\nहटाना चाहते हैं?'; + String get deleteMessageQuestion => 'क्या आप वाकई इस संदेश को स्थायी रूप से हटाना चाहते हैं?'; @override - String get operationCouldNotBeCompletedText => - 'कार्रवाई पूरी नहीं की जा सकी.'; + String get operationCouldNotBeCompletedText => 'कार्रवाई पूरी नहीं की जा सकी.'; @override String get replyLabel => 'जवाब दें'; @@ -275,8 +271,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get letsStartChattingLabel => 'चलो चैट करना शुरू करें!'; @override - String get sendingFirstMessageLabel => - 'किसी मित्र को अपना पहला संदेश भेजने के बारे में क्या विचार है?'; + String get sendingFirstMessageLabel => 'किसी मित्र को अपना पहला संदेश भेजने के बारे में क्या विचार है?'; @override String get startAChatLabel => 'चैट शुरू करें'; @@ -288,11 +283,10 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get deleteConversationLabel => 'वार्तालाप हटाए'; @override - String get deleteConversationQuestion => - 'क्या आप वाकई इस वार्तालाप को हटाना चाहते हैं?'; + String get deleteConversationQuestion => 'क्या आप वाकई इस वार्तालाप को हटाना चाहते हैं?'; @override - String get streamChatLabel => 'स्ट्रीम चैट'; + String get streamChatLabel => 'चैट'; @override String get searchingForNetworkText => 'नेटवर्क खोज रहे हैं'; @@ -328,8 +322,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get leaveConversationLabel => 'वार्तालाप छोड़े'; @override - String get leaveConversationQuestion => - 'क्या आप वाकई इस बातचीत को छोड़ना चाहते हैं?'; + String get leaveConversationQuestion => 'क्या आप वाकई इस बातचीत को छोड़ना चाहते हैं?'; @override String get showInChatLabel => 'चैट में दिखाएं'; @@ -365,8 +358,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '${currentPage + 1} ऑफ़ $totalPages'; + }) => '${currentPage + 1} ऑफ़ $totalPages'; @override String get fileText => 'फ़ाइल'; @@ -375,7 +367,8 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'संदेश का जवाब'; @override - String attachmentLimitExceedError(int limit) => ''' + String attachmentLimitExceedError(int limit) => + ''' अटैचमेंट लिमिट: $limit अटैचमेंट से अधिक जोड़ना संभव नहीं है '''; @@ -434,8 +427,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { } @override - String get linkDisabledDetails => - 'इस बातचीत में लिंक भेजने की अनुमति नहीं है.'; + String get linkDisabledDetails => 'इस बातचीत में लिंक भेजने की अनुमति नहीं है.'; @override String get linkDisabledError => 'लिंक भेजना प्रतिबंधित'; @@ -444,8 +436,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String unreadMessagesSeparatorText() => 'नए संदेश।'; @override - String get enableFileAccessMessage => 'कृपया फ़ाइलों तक पहुंच सक्षम करें ताकि' - '\nआप उन्हें मित्रों के साथ साझा कर सकें।'; + String get enableFileAccessMessage => 'कृपया फ़ाइलों तक पहुंच सक्षम करें ताकि आप उन्हें मित्रों के साथ साझा कर सकें।'; @override String get allowFileAccessMessage => 'फाइलों तक पहुंच की अनुमति दें'; @@ -547,15 +538,13 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get enterYourCommentLabel => 'अपनी टिप्पणी दर्ज करें'; @override - String get endVoteConfirmationText => - 'क्या आप वाकई मतदान समाप्त करना चाहते हैं?'; + String get endVoteConfirmationText => 'क्या आप वाकई मतदान समाप्त करना चाहते हैं?'; @override String get deletePollOptionLabel => 'विकल्प हटाएं'; @override - String get deletePollOptionQuestion => - 'क्या आप वाकई इस विकल्प को हटाना चाहते हैं?'; + String get deletePollOptionQuestion => 'क्या आप वाकई इस विकल्प को हटाना चाहते हैं?'; @override String get endLabel => 'समाप्त'; @@ -631,15 +620,13 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get slideToCancelLabel => 'रद्द करने के लिए स्लाइड करें'; @override - String get holdToRecordLabel => - 'रिकॉर्ड करने के लिए दबाए रखें, भेजने के लिए छोड़ें'; + String get holdToRecordLabel => 'रिकॉर्ड करने के लिए दबाए रखें, भेजने के लिए छोड़ें'; @override String get sendAnywayLabel => 'फिर भी भेजें'; @override - String get moderatedMessageBlockedText => - 'मॉडरेशन नीतियों द्वारा संदेश अवरुद्ध किया गया'; + String get moderatedMessageBlockedText => 'मॉडरेशन नीतियों द्वारा संदेश अवरुद्ध किया गया'; @override String get moderationReviewModalTitle => 'क्या आप निश्चित हैं?'; @@ -663,6 +650,18 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get videoAttachmentText => 'वीडियो'; + @override + String get fileAttachmentText => 'फ़ाइल'; + + @override + String filesAttachmentCountText(int count) => count == 1 ? 'फ़ाइल' : '$count फ़ाइलें'; + + @override + String photosAttachmentCountText(int count) => count == 1 ? 'फ़ोटो' : '$count फ़ोटो'; + + @override + String videosAttachmentCountText(int count) => count == 1 ? 'वीडियो' : '$count वीडियो'; + @override String get pollYouVotedText => 'आपने वोट दिया'; @@ -677,4 +676,55 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get draftLabel => 'ड्राफ्ट'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return 'लाइव लोकेशन'; + return 'लोकेशन'; + } + + @override + String get noConversationsYetText => 'अभी तक कोई बातचीत नहीं'; + + @override + String get replyToStartThreadText => 'थ्रेड शुरू करने के लिए किसी संदेश का जवाब दें'; + + @override + String get sendMessageToStartConversationText => 'बातचीत शुरू करने के लिए एक संदेश भेजें'; + + @override + String get savedForLaterLabel => 'बाद के लिए सहेजा गया'; + + @override + String get repliedToThreadAnnotationLabel => 'एक थ्रेड का जवाब दिया'; + + @override + String get alsoSentInChannelAnnotationLabel => 'चैनल में भी भेजा गया'; + + @override + String get viewLabel => 'देखें'; + + @override + String get reminderSetLabel => 'रिमाइंडर सेट'; + + @override + String reminderAtText(String time) => 'आज $time पर'; + + @override + String get createPollPromptLabel => 'पोल बनाएं और सबको वोट करने दें!'; + + @override + String get takePhotoAndShareLabel => 'फ़ोटो लें और साझा करें'; + + @override + String get takeVideoAndShareLabel => 'वीडियो लें और साझा करें'; + + @override + String get openCameraLabel => 'कैमरा खोलें'; + + @override + String get selectFilesToShareLabel => 'साझा करने के लिए फ़ाइलें चुनें'; + + @override + String get openFilesLabel => 'फ़ाइलें खोलें'; } 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 878a2a6b8a..c39086d7ed 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`). @@ -54,8 +56,7 @@ class StreamChatLocalizationsIt extends GlobalStreamChatLocalizations { String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - 'Caricamento $remaining/$total ...'; + }) => 'Caricamento $remaining/$total ...'; @override String pinnedByUserText({ @@ -68,8 +69,7 @@ class StreamChatLocalizationsIt extends GlobalStreamChatLocalizations { } @override - String get sendMessagePermissionError => - "Non hai l'autorizzazione per inviare messaggi"; + String get sendMessagePermissionError => "Non hai l'autorizzazione per inviare messaggi"; @override String get emptyMessagesText => "Non c'é nessun messaggio al momento"; @@ -78,8 +78,7 @@ class StreamChatLocalizationsIt extends GlobalStreamChatLocalizations { String get genericErrorText => 'Qualcosa è andato storto'; @override - String get loadingMessagesError => - 'Errore durante il caricamento dei messaggi'; + String get loadingMessagesError => 'Errore durante il caricamento dei messaggi'; @override String resultCountText(int count) => '$count risultati'; @@ -118,8 +117,7 @@ class StreamChatLocalizationsIt extends GlobalStreamChatLocalizations { String get reconnectingLabel => 'Riconnessione in corso...'; @override - String get alsoSendAsDirectMessageLabel => - 'Manda anche come messaggio diretto'; + String get alsoSendAsDirectMessageLabel => 'Manda anche come messaggio diretto'; @override String get addACommentOrSendLabel => 'Aggiungi un commento o invia'; @@ -144,8 +142,7 @@ class StreamChatLocalizationsIt extends GlobalStreamChatLocalizations { Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; @override - String get couldNotReadBytesFromFileError => - 'Impossibile leggere i byte dal file.'; + String get couldNotReadBytesFromFileError => 'Impossibile leggere i byte dal file.'; @override String get addAFileLabel => 'Aggiungi un file'; @@ -172,12 +169,11 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get somethingWentWrongError => 'Qualcosa è andato storto'; @override - String get addMoreFilesLabel => 'Aggiungi altri file'; + String get addMoreFilesLabel => 'Aggiungi altri'; @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 +182,7 @@ 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'; @@ -199,8 +194,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get flagMessageSuccessfulLabel => 'Messaggio segnalato'; @override - String get flagMessageSuccessfulText => - 'Questo messaggio è stato segnalato ad un moderatore.'; + String get flagMessageSuccessfulText => 'Questo messaggio è stato segnalato ad un moderatore.'; @override String get deleteLabel => 'CANCELLA'; @@ -209,12 +203,10 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get deleteMessageLabel => 'Cancella messaggio'; @override - String get deleteMessageQuestion => - 'Sei sicuro di voler definitivamente cancellare questo\nmessaggio?'; + String get deleteMessageQuestion => 'Sei sicuro di voler definitivamente cancellare questo messaggio?'; @override - String get operationCouldNotBeCompletedText => - 'Non è stato possibile completare questa operazione.'; + String get operationCouldNotBeCompletedText => 'Non è stato possibile completare questa operazione.'; @override String get replyLabel => 'Rispondi'; @@ -284,8 +276,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get letsStartChattingLabel => 'Inizia una conversazione!'; @override - String get sendingFirstMessageLabel => - 'Che ne dici di mandare il tuo primo messaggio ad un amico?'; + String get sendingFirstMessageLabel => 'Che ne dici di mandare il tuo primo messaggio ad un amico?'; @override String get startAChatLabel => 'Inizia una conversazione'; @@ -297,11 +288,10 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get deleteConversationLabel => 'Elimina conversazione'; @override - String get deleteConversationQuestion => - 'Sei sicuro di voler eliminare questa conversazione?'; + String get deleteConversationQuestion => 'Sei sicuro di voler eliminare questa conversazione?'; @override - String get streamChatLabel => 'Stream Chat'; + String get streamChatLabel => 'Conversazioni'; @override String get searchingForNetworkText => 'Cercando una connessione'; @@ -337,8 +327,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get leaveConversationLabel => 'Esci dalla conversazione'; @override - String get leaveConversationQuestion => - 'Sei sicuro di voler lasciare questa conversazione?'; + String get leaveConversationQuestion => 'Sei sicuro di voler lasciare questa conversazione?'; @override String get showInChatLabel => 'Mostra nella chat'; @@ -374,8 +363,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '${currentPage + 1} di $totalPages'; + }) => '${currentPage + 1} di $totalPages'; @override String get fileText => 'file'; @@ -384,7 +372,8 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.'''; String get replyToMessageLabel => 'Rispondi al messaggio'; @override - String attachmentLimitExceedError(int limit) => ''' + String attachmentLimitExceedError(int limit) => + ''' Attenzione: il limite massimo di $limit file è stato superato. '''; @@ -443,8 +432,7 @@ Attenzione: il limite massimo di $limit file è stato superato. } @override - String get linkDisabledDetails => - 'Non è permesso condividere link in questa convesazione.'; + String get linkDisabledDetails => 'Non è permesso condividere link in questa convesazione.'; @override String get linkDisabledError => 'I links sono disattivati'; @@ -453,8 +441,8 @@ Attenzione: il limite massimo di $limit file è stato superato. String unreadMessagesSeparatorText() => 'Nuovi messaggi'; @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"; @@ -563,15 +551,13 @@ Attenzione: il limite massimo di $limit file è stato superato. String get enterYourCommentLabel => 'Inserisci il tuo commento'; @override - String get endVoteConfirmationText => - 'Sei sicuro di voler terminare il voto?'; + String get endVoteConfirmationText => 'Sei sicuro di voler terminare il voto?'; @override String get deletePollOptionLabel => "Elimina l'opzione"; @override - String get deletePollOptionQuestion => - 'Sei sicuro di voler eliminare questa opzione?'; + String get deletePollOptionQuestion => 'Sei sicuro di voler eliminare questa opzione?'; @override String get createLabel => 'Crea'; @@ -615,17 +601,16 @@ Attenzione: il limite massimo di $limit file è stato superato. @override String voteCountLabel({int? count}) => switch (count) { - null || < 1 => '0 voti', - 1 => '1 voto', - _ => '$count voti', - }; + null || < 1 => '0 voti', + 1 => '1 voto', + _ => '$count voti', + }; @override String get noPollVotesLabel => 'Attualmente non ci sono voti nel sondaggio'; @override - String get loadingPollVotesError => - 'Errore durante il caricamento dei voti del sondaggio'; + String get loadingPollVotesError => 'Errore durante il caricamento dei voti del sondaggio'; @override String get repliedToLabel => 'risposto a:'; @@ -640,15 +625,13 @@ Attenzione: il limite massimo di $limit file è stato superato. String get slideToCancelLabel => 'Scorri per annullare'; @override - String get holdToRecordLabel => - 'Tieni premuto per registrare, rilascia per inviare'; + String get holdToRecordLabel => 'Tieni premuto per registrare, rilascia per inviare'; @override String get sendAnywayLabel => 'Invia comunque'; @override - String get moderatedMessageBlockedText => - 'Messaggio bloccato dalle politiche di moderazione'; + String get moderatedMessageBlockedText => 'Messaggio bloccato dalle politiche di moderazione'; @override String get moderationReviewModalTitle => 'Sei sicuro?'; @@ -672,6 +655,18 @@ Attenzione: il limite massimo di $limit file è stato superato. @override String get videoAttachmentText => 'Video'; + @override + String get fileAttachmentText => 'File'; + + @override + String filesAttachmentCountText(int count) => count == 1 ? 'File' : '$count file'; + + @override + String photosAttachmentCountText(int count) => count == 1 ? 'Foto' : '$count foto'; + + @override + String videosAttachmentCountText(int count) => count == 1 ? 'Video' : '$count video'; + @override String get pollYouVotedText => 'Hai votato'; @@ -686,4 +681,55 @@ 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'; + } + + @override + String get noConversationsYetText => 'Ancora nessuna conversazione'; + + @override + String get replyToStartThreadText => 'Rispondi a un messaggio per avviare un thread'; + + @override + String get sendMessageToStartConversationText => 'Invia un messaggio per iniziare la conversazione'; + + @override + String get savedForLaterLabel => 'Salvato per dopo'; + + @override + String get repliedToThreadAnnotationLabel => 'Ha risposto a un thread'; + + @override + String get alsoSentInChannelAnnotationLabel => 'Inviato anche nel canale'; + + @override + String get viewLabel => 'Visualizza'; + + @override + String get reminderSetLabel => 'Promemoria impostato'; + + @override + String reminderAtText(String time) => 'Oggi alle $time'; + + @override + String get createPollPromptLabel => 'Crea un sondaggio e fai votare tutti!'; + + @override + String get takePhotoAndShareLabel => 'Scatta una foto e condividi'; + + @override + String get takeVideoAndShareLabel => 'Registra un video e condividi'; + + @override + String get openCameraLabel => 'Apri fotocamera'; + + @override + String get selectFilesToShareLabel => 'Seleziona i file da condividere'; + + @override + String get openFilesLabel => 'Apri file'; } 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 fea76688a1..e889aace52 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 @@ -49,8 +49,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - '$remaining/${total}mbのアップロード中…'; + }) => '$remaining/${total}mbのアップロード中…'; @override String pinnedByUserText({ @@ -129,8 +128,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { '圧縮を試しましたがサイズをオーバーしました'; @override - String fileTooLargeError(double limitInMB) => - 'ファイルが大きすぎてアップロードできません。ファイルサイズの制限は${limitInMB}MBです。'; + String fileTooLargeError(double limitInMB) => 'ファイルが大きすぎてアップロードできません。ファイルサイズの制限は${limitInMB}MBです。'; @override String get couldNotReadBytesFromFileError => 'ファイルからバイトを読み取れませんでした'; @@ -160,11 +158,10 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'エラーが発生しました'; @override - String get addMoreFilesLabel => 'ファイルの追加'; + String get addMoreFilesLabel => 'さらに追加'; @override - String get enablePhotoAndVideoAccessMessage => 'お友達と共有できるように、写真' - '\nやビデオへのアクセスを有効にしてください。'; + String get enablePhotoAndVideoAccessMessage => 'お友達と共有できるように、写真やビデオへのアクセスを有効にしてください。'; @override String get allowGalleryAccessMessage => 'ギャラリーへのアクセスを許可する'; @@ -172,8 +169,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get flagMessageLabel => 'メッセージをフラグする'; @override - String get flagMessageQuestion => 'このメッセージのコピーを' - '\nモデレーターに送って、さらに調査してもらいますか?'; + String get flagMessageQuestion => 'このメッセージのコピーをモデレーターに送って、さらに調査してもらいますか?'; @override String get flagLabel => 'フラグする'; @@ -194,8 +190,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get deleteMessageLabel => 'メッセージを削除する'; @override - String get deleteMessageQuestion => 'このメッセージ' - '\nを完全に削除してもよろしいですか?'; + String get deleteMessageQuestion => 'このメッセージを完全に削除してもよろしいですか?'; @override String get operationCouldNotBeCompletedText => '操作を完了できませんでした。'; @@ -283,7 +278,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get deleteConversationQuestion => '本当に会話を削除しますか?'; @override - String get streamChatLabel => 'ストリームチャット'; + String get streamChatLabel => 'チャット'; @override String get searchingForNetworkText => 'ネットワークを検索中'; @@ -351,8 +346,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '${currentPage + 1} / $totalPages'; + }) => '${currentPage + 1} / $totalPages'; @override String get fileText => 'ファイル'; @@ -367,7 +361,8 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get viewLibrary => 'ライブラリを表示'; @override - String attachmentLimitExceedError(int limit) => ''' + String attachmentLimitExceedError(int limit) => + ''' 添付ファイルの制限を超えました:$limit個のファイル以上を添付することはできません '''; @@ -429,8 +424,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String unreadMessagesSeparatorText() => '新しいメッセージ。'; @override - String get enableFileAccessMessage => - '友達と共有できるように、' '\nファイルへのアクセスを有効にしてください。'; + String get enableFileAccessMessage => '友達と共有できるように、ファイルへのアクセスを有効にしてください。'; @override String get allowFileAccessMessage => 'ファイルへのアクセスを許可する'; @@ -444,8 +438,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { } @override - String get markUnreadError => - 'メッセージを未読にする際にエラーが発生しました。最新の100件のチャンネルメッセージより古い未読メッセージはマークできません。'; + String get markUnreadError => 'メッセージを未読にする際にエラーが発生しました。最新の100件のチャンネルメッセージより古い未読メッセージはマークできません。'; @override String createPollLabel({bool isNew = false}) { @@ -587,10 +580,10 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String voteCountLabel({int? count}) => switch (count) { - null || < 1 => '0 票', - 1 => '1 票', - _ => '$count 票', - }; + null || < 1 => '0 票', + 1 => '1 票', + _ => '$count 票', + }; @override String get noPollVotesLabel => '現在投票はありません'; @@ -622,8 +615,7 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String get moderationReviewModalTitle => 'よろしいですか?'; @override - String get moderationReviewModalDescription => - '''あなたのコメントが他の人にどのような影響を与えるかを考え、コミュニティガイドラインに従ってください。'''; + String get moderationReviewModalDescription => '''あなたのコメントが他の人にどのような影響を与えるかを考え、コミュニティガイドラインに従ってください。'''; @override String get emptyMessagePreviewText => ''; @@ -640,6 +632,18 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String get videoAttachmentText => '動画'; + @override + String get fileAttachmentText => 'ファイル'; + + @override + String filesAttachmentCountText(int count) => count == 1 ? 'ファイル' : '$count件のファイル'; + + @override + String photosAttachmentCountText(int count) => count == 1 ? '写真' : '$count枚の写真'; + + @override + String videosAttachmentCountText(int count) => count == 1 ? '動画' : '$count本の動画'; + @override String get pollYouVotedText => '投票しました'; @@ -654,4 +658,55 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { @override String get draftLabel => '下書き'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return 'ライブ位置情報'; + return '位置情報'; + } + + @override + String get noConversationsYetText => 'まだ会話がありません'; + + @override + String get replyToStartThreadText => 'スレッドを開始するにはメッセージに返信してください'; + + @override + String get sendMessageToStartConversationText => '会話を始めるにはメッセージを送信してください'; + + @override + String get savedForLaterLabel => '後で確認'; + + @override + String get repliedToThreadAnnotationLabel => 'スレッドに返信しました'; + + @override + String get alsoSentInChannelAnnotationLabel => 'チャンネルにも送信されました'; + + @override + String get viewLabel => '表示'; + + @override + String get reminderSetLabel => 'リマインダー設定済み'; + + @override + String reminderAtText(String time) => '今日 $time'; + + @override + String get createPollPromptLabel => '投票を作成してみんなに投票してもらおう!'; + + @override + String get takePhotoAndShareLabel => '写真を撮って共有'; + + @override + String get takeVideoAndShareLabel => '動画を撮って共有'; + + @override + String get openCameraLabel => 'カメラを開く'; + + @override + String get selectFilesToShareLabel => '共有するファイルを選択'; + + @override + String get openFilesLabel => 'ファイルを開く'; } 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 9d4156d2d8..bdb83d04cb 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 @@ -49,8 +49,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - '$remaining/${total}mb를 업로드중...'; + }) => '$remaining/${total}mb를 업로드중...'; @override String pinnedByUserText({ @@ -129,8 +128,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { '우리는 압축해 보았지만 충분하지 않았습니다.'; @override - String fileTooLargeError(double limitInMB) => - '파일이 너무 커서 업로드할 수 없습니다. 파일 크기 제한은 ${limitInMB}MB입니다.'; + String fileTooLargeError(double limitInMB) => '파일이 너무 커서 업로드할 수 없습니다. 파일 크기 제한은 ${limitInMB}MB입니다.'; @override String get couldNotReadBytesFromFileError => '파일에서 바이트를 읽을 수 없습니다.'; @@ -160,11 +158,10 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get somethingWentWrongError => '뭔가 잘못됐습느다'; @override - String get addMoreFilesLabel => '파일을 추가함'; + String get addMoreFilesLabel => '더 추가'; @override - String get enablePhotoAndVideoAccessMessage => '친구와 공유할 수 있도록 사진과' - '\n동영상에 액세스할 수 있도록 설정하십시오.'; + String get enablePhotoAndVideoAccessMessage => '친구와 공유할 수 있도록 사진과 동영상에 액세스할 수 있도록 설정하십시오.'; @override String get allowGalleryAccessMessage => '갤러리에 대한 액세스를 허용합니다'; @@ -282,7 +279,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get deleteConversationQuestion => '대화를 삭제하시겠습니까?'; @override - String get streamChatLabel => '스트림 채팅'; + String get streamChatLabel => '채팅'; @override String get searchingForNetworkText => '네트워크를 검색하는 중입니다.'; @@ -350,8 +347,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '${currentPage + 1} / $totalPages'; + }) => '${currentPage + 1} / $totalPages'; //3 / 11 @@ -369,8 +365,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get viewLibrary => '라이브러리 보기'; @override - String attachmentLimitExceedError(int limit) => - '첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다'; + String attachmentLimitExceedError(int limit) => '첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다'; @override String get downloadLabel => '다운로드'; @@ -588,10 +583,10 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String voteCountLabel({int? count}) => switch (count) { - null || < 1 => '0 표', - 1 => '1 표', - _ => '$count 표', - }; + null || < 1 => '0 표', + 1 => '1 표', + _ => '$count 표', + }; @override String get noPollVotesLabel => '현재 투표가 없습니다'; @@ -623,8 +618,7 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { String get moderationReviewModalTitle => '확실합니까?'; @override - String get moderationReviewModalDescription => - '''귀하의 댓글이 다른 사람들에게 어떤 영향을 미칠 수 있는지 고려하고 커뮤니티 가이드라인을 준수하세요.'''; + String get moderationReviewModalDescription => '''귀하의 댓글이 다른 사람들에게 어떤 영향을 미칠 수 있는지 고려하고 커뮤니티 가이드라인을 준수하세요.'''; @override String get emptyMessagePreviewText => ''; @@ -641,6 +635,18 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String get videoAttachmentText => '비디오'; + @override + String get fileAttachmentText => '파일'; + + @override + String filesAttachmentCountText(int count) => count == 1 ? '파일' : '파일 $count개'; + + @override + String photosAttachmentCountText(int count) => count == 1 ? '사진' : '사진 $count장'; + + @override + String videosAttachmentCountText(int count) => count == 1 ? '동영상' : '동영상 $count개'; + @override String get pollYouVotedText => '투표했습니다'; @@ -655,4 +661,55 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String get draftLabel => '임시글'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return '실시간 위치'; + return '위치'; + } + + @override + String get noConversationsYetText => '아직 대화가 없습니다'; + + @override + String get replyToStartThreadText => '스레드를 시작하려면 메시지에 답장하세요'; + + @override + String get sendMessageToStartConversationText => '대화를 시작하려면 메시지를 보내세요'; + + @override + String get savedForLaterLabel => '나중을 위해 저장됨'; + + @override + String get repliedToThreadAnnotationLabel => '스레드에 답장함'; + + @override + String get alsoSentInChannelAnnotationLabel => '채널에도 전송됨'; + + @override + String get viewLabel => '보기'; + + @override + String get reminderSetLabel => '리마인더 설정됨'; + + @override + String reminderAtText(String time) => '오늘 $time'; + + @override + String get createPollPromptLabel => '투표를 만들고 모두에게 투표하게 하세요!'; + + @override + String get takePhotoAndShareLabel => '사진을 찍고 공유'; + + @override + String get takeVideoAndShareLabel => '동영상을 찍고 공유'; + + @override + String get openCameraLabel => '카메라 열기'; + + @override + String get selectFilesToShareLabel => '공유할 파일 선택'; + + @override + String get openFilesLabel => '파일 열기'; } 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 814867f8d8..4046576945 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`). @@ -49,8 +51,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - 'Laster opp $remaining/$total ...'; + }) => 'Laster opp $remaining/$total ...'; @override String pinnedByUserText({ @@ -63,8 +64,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { } @override - String get sendMessagePermissionError => - 'Du har ikke tillatelse til å sende meldinger'; + String get sendMessagePermissionError => 'Du har ikke tillatelse til å sende meldinger'; @override String get emptyMessagesText => 'Det er ingen meldinger akkurat nå'; @@ -133,8 +133,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { 'Vi prøvde å komprimere den, men det hjalp ikke.'; @override - String fileTooLargeError(double limitInMB) => - 'Filen er for stor til å laste opp. Filgrense er $limitInMB MB.'; + String fileTooLargeError(double limitInMB) => 'Filen er for stor til å laste opp. Filgrense er $limitInMB MB.'; @override String get addAFileLabel => 'Legg til en fil'; @@ -161,12 +160,11 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Noe gikk galt'; @override - String get addMoreFilesLabel => 'Legg til flere filer'; + String get addMoreFilesLabel => 'Legg til flere'; @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 +174,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'; @@ -189,8 +186,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String get flagMessageSuccessfulLabel => 'Melding rapportert'; @override - String get flagMessageSuccessfulText => - 'Meldingen har blitt rapportert til en moderator.'; + String get flagMessageSuccessfulText => 'Meldingen har blitt rapportert til en moderator.'; @override String get deleteLabel => 'SLETT'; @@ -199,12 +195,10 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String get deleteMessageLabel => 'Slett melding'; @override - String get deleteMessageQuestion => - 'Er du sikker på at du ønsker å slette denne meldingen permanent?'; + String get deleteMessageQuestion => 'Er du sikker på at du ønsker å slette denne meldingen permanent?'; @override - String get operationCouldNotBeCompletedText => - 'Denne handlingen kunne ikke bli gjennomført.'; + String get operationCouldNotBeCompletedText => 'Denne handlingen kunne ikke bli gjennomført.'; @override String get replyLabel => 'Svar'; @@ -274,8 +268,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String get letsStartChattingLabel => 'La oss starte å chatte!'; @override - String get sendingFirstMessageLabel => - 'Hva med å sende din første melding til en venn?'; + String get sendingFirstMessageLabel => 'Hva med å sende din første melding til en venn?'; @override String get startAChatLabel => 'Start en chat'; @@ -287,11 +280,10 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String get deleteConversationLabel => 'Slett samtale'; @override - String get deleteConversationQuestion => - 'Er du sikker på at du ønsker å slette denne samtalen?'; + String get deleteConversationQuestion => 'Er du sikker på at du ønsker å slette denne samtalen?'; @override - String get streamChatLabel => 'Stream Chat'; + String get streamChatLabel => 'Samtaler'; @override String get searchingForNetworkText => 'Søker etter nettverk'; @@ -327,8 +319,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String get leaveConversationLabel => 'Forlat samtale'; @override - String get leaveConversationQuestion => - 'Er du sikker på at du ønsker å forlate denne samtalen?'; + String get leaveConversationQuestion => 'Er du sikker på at du ønsker å forlate denne samtalen?'; @override String get showInChatLabel => 'Se i chat'; @@ -364,8 +355,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '${currentPage + 1} of $totalPages'; + }) => '${currentPage + 1} of $totalPages'; @override String get fileText => 'Fil'; @@ -374,15 +364,13 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'Svar på melding'; @override - String attachmentLimitExceedError(int limit) => - 'Antall vedlegg oversteget, maks antall: $limit'; + String attachmentLimitExceedError(int limit) => 'Antall vedlegg oversteget, maks antall: $limit'; @override String get slowModeOnLabel => 'Sakte modus PÅ'; @override - String get linkDisabledDetails => - 'Sende lenker er ikke lov i denne samtalen.'; + String get linkDisabledDetails => 'Sende lenker er ikke lov i denne samtalen.'; @override String get linkDisabledError => 'Lenker er deaktivert'; @@ -394,8 +382,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String unreadMessagesSeparatorText() => 'Nye meldinger.'; @override - String get couldNotReadBytesFromFileError => - 'Kunne ikke lese bytes fra filen.'; + String get couldNotReadBytesFromFileError => 'Kunne ikke lese bytes fra filen.'; @override String get downloadLabel => 'Nedlasting'; @@ -423,7 +410,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?'; @@ -436,8 +422,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { } @override - String get enableFileAccessMessage => - 'Aktiver tilgang til filer slik' '\nat du kan dele dem med venner.'; + String get enableFileAccessMessage => 'Aktiver tilgang til filer slik at du kan dele dem med venner.'; @override String get allowFileAccessMessage => 'Gi tilgang til filer'; @@ -503,8 +488,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String get multipleAnswersLabel => 'Flere svar'; @override - String get maximumVotesPerPersonLabel => - 'Maksimalt antall stemmer per person'; + String get maximumVotesPerPersonLabel => 'Maksimalt antall stemmer per person'; @override String? maxVotesPerPersonValidationError(int votes, Range range) { @@ -546,15 +530,13 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String get enterYourCommentLabel => 'Skriv inn kommentaren din'; @override - String get endVoteConfirmationText => - 'Er du sikker på at du vil avslutte avstemningen?'; + String get endVoteConfirmationText => 'Er du sikker på at du vil avslutte avstemningen?'; @override String get deletePollOptionLabel => 'Slett alternativ'; @override - String get deletePollOptionQuestion => - 'Er du sikker på at du vil slette dette alternativet?'; + String get deletePollOptionQuestion => 'Er du sikker på at du vil slette dette alternativet?'; @override String get createLabel => 'Opprett'; @@ -598,10 +580,10 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { @override String voteCountLabel({int? count}) => switch (count) { - null || < 1 => '0 stemmer', - 1 => '1 stemme', - _ => '$count stemmer', - }; + null || < 1 => '0 stemmer', + 1 => '1 stemme', + _ => '$count stemmer', + }; @override String get noPollVotesLabel => 'Det er ingen stemmer for øyeblikket'; @@ -628,8 +610,7 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { String get sendAnywayLabel => 'Send likevel'; @override - String get moderatedMessageBlockedText => - 'Meldingen ble blokkert av modereringsregler'; + String get moderatedMessageBlockedText => 'Meldingen ble blokkert av modereringsregler'; @override String get moderationReviewModalTitle => 'Er du sikker?'; @@ -653,6 +634,18 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { @override String get videoAttachmentText => 'Video'; + @override + String get fileAttachmentText => 'Fil'; + + @override + String filesAttachmentCountText(int count) => count == 1 ? 'Fil' : '$count filer'; + + @override + String photosAttachmentCountText(int count) => count == 1 ? 'Bilde' : '$count bilder'; + + @override + String videosAttachmentCountText(int count) => count == 1 ? 'Video' : '$count videoer'; + @override String get pollYouVotedText => 'Du stemte'; @@ -667,4 +660,55 @@ class StreamChatLocalizationsNo extends GlobalStreamChatLocalizations { @override String get draftLabel => 'Utkast'; + + @override + String locationLabel({bool isLive = false}) { + if (isLive) return 'Direkte posisjon'; + return 'Posisjon'; + } + + @override + String get noConversationsYetText => 'Ingen samtaler ennå'; + + @override + String get replyToStartThreadText => 'Svar på en melding for å starte en tråd'; + + @override + String get sendMessageToStartConversationText => 'Send en melding for å starte samtalen'; + + @override + String get savedForLaterLabel => 'Lagret til senere'; + + @override + String get repliedToThreadAnnotationLabel => 'Svarte i en tråd'; + + @override + String get alsoSentInChannelAnnotationLabel => 'Også sendt i kanalen'; + + @override + String get viewLabel => 'Vis'; + + @override + String get reminderSetLabel => 'Påminnelse satt'; + + @override + String reminderAtText(String time) => 'I dag kl. $time'; + + @override + String get createPollPromptLabel => 'Lag en avstemning og la alle stemme!'; + + @override + String get takePhotoAndShareLabel => 'Ta et bilde og del'; + + @override + String get takeVideoAndShareLabel => 'Ta en video og del'; + + @override + String get openCameraLabel => 'Åpne kamera'; + + @override + String get selectFilesToShareLabel => 'Velg filer å dele'; + + @override + String get openFilesLabel => 'Åpne filer'; } 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 ddc189736f..f8e314915f 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`). @@ -49,8 +51,7 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String attachmentsUploadProgressText({ required int remaining, required int total, - }) => - 'Tranferência em andamento $remaining/$total ...'; + }) => 'Tranferência em andamento $remaining/$total ...'; @override String pinnedByUserText({ @@ -69,8 +70,7 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String get genericErrorText => 'Ocorreu um problema'; @override - String get loadingMessagesError => - 'Ocorreu um problema ao carregar a mensagem'; + String get loadingMessagesError => 'Ocorreu um problema ao carregar a mensagem'; @override String resultCountText(int count) => '$count resultados'; @@ -109,8 +109,7 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String get reconnectingLabel => 'Reconectando...'; @override - String get alsoSendAsDirectMessageLabel => - 'Enviar também como mensagem direta'; + String get alsoSendAsDirectMessageLabel => 'Enviar também como mensagem direta'; @override String get addACommentOrSendLabel => 'Adicionar um comnetário ou enviar'; @@ -136,8 +135,7 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { 'O tamanho máximo dos arquivos é de $limitInMB MB.'; @override - String get couldNotReadBytesFromFileError => - 'Não foi possível ler os bytes do arquivo.'; + String get couldNotReadBytesFromFileError => 'Não foi possível ler os bytes do arquivo.'; @override String get addAFileLabel => 'Adicionar um arquivo'; @@ -164,12 +162,11 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String get somethingWentWrongError => 'Algo deu errado'; @override - String get addMoreFilesLabel => 'Adicionar mais arquivos'; + String get addMoreFilesLabel => 'Adicionar mais'; @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 +175,7 @@ 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'; @@ -191,8 +187,7 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String get flagMessageSuccessfulLabel => 'Mensagem denunciada'; @override - String get flagMessageSuccessfulText => - 'Esta mensagem foi enviada a um moderador.'; + String get flagMessageSuccessfulText => 'Esta mensagem foi enviada a um moderador.'; @override String get deleteLabel => 'APAGAR'; @@ -201,12 +196,10 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String get deleteMessageLabel => 'Apagar mensagem'; @override - String get deleteMessageQuestion => - 'Você tem certeza que deseja apagar essa\nmensagem permanentemente?'; + String get deleteMessageQuestion => 'Você tem certeza que deseja apagar essa mensagem permanentemente?'; @override - String get operationCouldNotBeCompletedText => - 'A operação não pode ser completada.'; + String get operationCouldNotBeCompletedText => 'A operação não pode ser completada.'; @override String get replyLabel => 'Resposta'; @@ -276,8 +269,7 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String get letsStartChattingLabel => 'Vamos começar a conversar!'; @override - String get sendingFirstMessageLabel => - 'Que tal enviar sua primeira mensagem a um amigo?'; + String get sendingFirstMessageLabel => 'Que tal enviar sua primeira mensagem a um amigo?'; @override String get startAChatLabel => 'Iniciar uma conversa'; @@ -289,11 +281,10 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String get deleteConversationLabel => 'Apagar a conversa'; @override - String get deleteConversationQuestion => - 'Tem certeza que deseja apagar essa conversa?'; + String get deleteConversationQuestion => 'Tem certeza que deseja apagar essa conversa?'; @override - String get streamChatLabel => 'Stream Chat'; + String get streamChatLabel => 'Conversas'; @override String get searchingForNetworkText => 'Pesquisando rede'; @@ -329,8 +320,7 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String get leaveConversationLabel => 'Sair da conversa'; @override - String get leaveConversationQuestion => - 'Tem certeza que deseja sair dessa conversa?'; + String get leaveConversationQuestion => 'Tem certeza que deseja sair dessa conversa?'; @override String get showInChatLabel => 'Mostrar no chat'; @@ -366,8 +356,7 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String galleryPaginationText({ required int currentPage, required int totalPages, - }) => - '${currentPage + 1} de $totalPages'; + }) => '${currentPage + 1} de $totalPages'; @override String get fileText => 'Arquivo'; @@ -376,7 +365,8 @@ class StreamChatLocalizationsPt extends GlobalStreamChatLocalizations { String get replyToMessageLabel => 'Responder à mensagem'; @override - String attachmentLimitExceedError(int limit) => ''' + String attachmentLimitExceedError(int limit) => + ''' Não é possível adicionar mais de $limit arquivos de uma vez '''; @@ -432,15 +422,13 @@ Não é possível adicionar mais de $limit arquivos de uma vez } @override - String get linkDisabledDetails => - 'O envio de links não é permitido nesta conversa.'; + String get linkDisabledDetails => 'O envio de links não é permitido nesta conversa.'; @override String get linkDisabledError => 'Os links estão desativados'; @override - String get sendMessagePermissionError => - 'Você não tem permissão para enviar mensagens'; + String get sendMessagePermissionError => 'Você não tem permissão para enviar mensagens'; @override String get viewLibrary => 'Ver biblioteca'; @@ -449,8 +437,7 @@ Não é possível adicionar mais de $limit arquivos de uma vez String unreadMessagesSeparatorText() => 'Novas mensagens'; @override - String get enableFileAccessMessage => - 'Ative o acesso aos arquivos' '\npara poder compartilhá-los com amigos.'; + String get enableFileAccessMessage => 'Ative o acesso aos arquivos para poder compartilhá-los com amigos.'; @override String get allowFileAccessMessage => 'Permitir acesso aos arquivos'; @@ -558,15 +545,13 @@ Não é possível adicionar mais de $limit arquivos de uma vez String get enterYourCommentLabel => 'Inserir seu comentário'; @override - String get endVoteConfirmationText => - 'Tem certeza de que deseja encerrar a votação?'; + String get endVoteConfirmationText => 'Tem certeza de que deseja encerrar a votação?'; @override String get deletePollOptionLabel => 'Excluir opção'; @override - String get deletePollOptionQuestion => - 'Tem certeza de que deseja excluir esta opção?'; + String get deletePollOptionQuestion => 'Tem certeza de que deseja excluir esta opção?'; @override String get createLabel => 'Criar'; @@ -610,10 +595,10 @@ Não é possível adicionar mais de $limit arquivos de uma vez @override String voteCountLabel({int? count}) => switch (count) { - null || < 1 => '0 votos', - 1 => '1 voto', - _ => '$count votos', - }; + null || < 1 => '0 votos', + 1 => '1 voto', + _ => '$count votos', + }; @override String get noPollVotesLabel => 'Não há votos no momento'; @@ -634,15 +619,13 @@ Não é possível adicionar mais de $limit arquivos de uma vez String get slideToCancelLabel => 'Deslize para cancelar'; @override - String get holdToRecordLabel => - 'Mantenha pressionado para gravar, solte para enviar'; + String get holdToRecordLabel => 'Mantenha pressionado para gravar, solte para enviar'; @override String get sendAnywayLabel => 'Enviar mesmo assim'; @override - String get moderatedMessageBlockedText => - 'Mensagem bloqueada pelas políticas de moderação'; + String get moderatedMessageBlockedText => 'Mensagem bloqueada pelas políticas de moderação'; @override String get moderationReviewModalTitle => 'Tem certeza?'; @@ -666,6 +649,18 @@ Não é possível adicionar mais de $limit arquivos de uma vez @override String get videoAttachmentText => 'Vídeo'; + @override + String get fileAttachmentText => 'Arquivo'; + + @override + String filesAttachmentCountText(int count) => count == 1 ? 'Arquivo' : '$count arquivos'; + + @override + String photosAttachmentCountText(int count) => count == 1 ? 'Foto' : '$count fotos'; + + @override + String videosAttachmentCountText(int count) => count == 1 ? 'Vídeo' : '$count vídeos'; + @override String get pollYouVotedText => 'Você votou'; @@ -680,4 +675,55 @@ 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'; + } + + @override + String get noConversationsYetText => 'Ainda não há conversas'; + + @override + String get replyToStartThreadText => 'Responda a uma mensagem para iniciar uma thread'; + + @override + String get sendMessageToStartConversationText => 'Envie uma mensagem para iniciar a conversa'; + + @override + String get savedForLaterLabel => 'Guardado para depois'; + + @override + String get repliedToThreadAnnotationLabel => 'Respondeu a uma thread'; + + @override + String get alsoSentInChannelAnnotationLabel => 'Também enviado no canal'; + + @override + String get viewLabel => 'Ver'; + + @override + String get reminderSetLabel => 'Lembrete definido'; + + @override + String reminderAtText(String time) => 'Hoje às $time'; + + @override + String get createPollPromptLabel => 'Crie uma enquete e deixe todos votarem!'; + + @override + String get takePhotoAndShareLabel => 'Tire uma foto e compartilhe'; + + @override + String get takeVideoAndShareLabel => 'Grave um vídeo e compartilhe'; + + @override + String get openCameraLabel => 'Abrir câmera'; + + @override + String get selectFilesToShareLabel => 'Selecione arquivos para compartilhar'; + + @override + String get openFilesLabel => 'Abrir arquivos'; } diff --git a/packages/stream_chat_localizations/lib/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/stream_chat_localizations.dart index 27c018806a..852e8e2f54 100644 --- a/packages/stream_chat_localizations/lib/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/stream_chat_localizations.dart @@ -2,8 +2,5 @@ library stream_chat_localizations; export 'package:flutter_localizations/flutter_localizations.dart' - show - GlobalCupertinoLocalizations, - GlobalMaterialLocalizations, - GlobalWidgetsLocalizations; + show GlobalCupertinoLocalizations, GlobalMaterialLocalizations, GlobalWidgetsLocalizations; export 'src/stream_chat_localizations.dart' hide getStreamChatTranslation; diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index 8ea640d9b4..e95ffeb03f 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.23.0 +version: 10.0.0-beta.13 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 @@ -18,15 +18,15 @@ issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues # 2. Add it to the melos.yaml file for future updates. environment: - sdk: ^3.6.2 - flutter: ">=3.27.4" + sdk: ^3.10.0 + flutter: ">=3.38.1" dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter - stream_chat_flutter: ^9.23.0 + stream_chat_flutter: ^10.0.0-beta.13 dev_dependencies: flutter_test: diff --git a/packages/stream_chat_localizations/test/basics_test.dart b/packages/stream_chat_localizations/test/basics_test.dart index a05877f43c..f973c08d45 100644 --- a/packages/stream_chat_localizations/test/basics_test.dart +++ b/packages/stream_chat_localizations/test/basics_test.dart @@ -6,28 +6,32 @@ import 'package:stream_chat_localizations/stream_chat_localizations.dart'; void main() { testWidgets('Nested Localizations', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - theme: ThemeData( - useMaterial3: false, + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + useMaterial3: false, + ), + // Creates the outer Localizations widget. + home: ListView( + children: [ + const LocalizationTracker(key: ValueKey('outer')), + Localizations( + locale: const Locale('hi'), + delegates: GlobalStreamChatLocalizations.delegates, + child: const LocalizationTracker(key: ValueKey('inner')), + ), + ], + ), ), - // Creates the outer Localizations widget. - home: ListView( - children: [ - const LocalizationTracker(key: ValueKey('outer')), - Localizations( - locale: const Locale('hi'), - delegates: GlobalStreamChatLocalizations.delegates, - child: const LocalizationTracker(key: ValueKey('inner')), - ), - ], - ), - )); + ); final LocalizationTrackerState outerTracker = tester.state( - find.byKey(const ValueKey('outer'), skipOffstage: false)); + find.byKey(const ValueKey('outer'), skipOffstage: false), + ); expect(outerTracker.captionFontSize, 12.0); final LocalizationTrackerState innerTracker = tester.state( - find.byKey(const ValueKey('inner'), skipOffstage: false)); + find.byKey(const ValueKey('inner'), skipOffstage: false), + ); expect(innerTracker.captionFontSize, 13.0); }); @@ -36,19 +40,21 @@ void main() { 'during didChangeDependencies', (WidgetTester tester) async { // PageView calls ScrollPosition.dispose() during didChangeDependencies. - await tester.pumpWidget(MaterialApp( - supportedLocales: const [ - Locale('en', 'US'), - Locale('hi', 'IN'), - ], - localizationsDelegates: const [ - DummyLocalizations.delegate, - GlobalStreamChatLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - home: PageView(), - )); + await tester.pumpWidget( + MaterialApp( + supportedLocales: const [ + Locale('en', 'US'), + Locale('hi', 'IN'), + ], + localizationsDelegates: const [ + DummyLocalizations.delegate, + GlobalStreamChatLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + home: PageView(), + ), + ); await tester.binding.setLocale('hi', 'IN'); await tester.pump(); @@ -58,14 +64,16 @@ void main() { testWidgets('Locale without countryCode', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/16782 - await tester.pumpWidget(MaterialApp( - localizationsDelegates: GlobalStreamChatLocalizations.delegates, - supportedLocales: const [ - Locale('en', 'US'), - Locale('hi'), - ], - home: Container(), - )); + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: GlobalStreamChatLocalizations.delegates, + supportedLocales: const [ + Locale('en', 'US'), + Locale('hi'), + ], + home: Container(), + ), + ); await tester.binding.setLocale('hi', ''); await tester.pump(); @@ -76,8 +84,7 @@ void main() { /// A localizations delegate that does not contain any useful data, and is only /// used to trigger didChangeDependencies upon locale change. -class _DummyLocalizationsDelegate - extends LocalizationsDelegate { +class _DummyLocalizationsDelegate extends LocalizationsDelegate { const _DummyLocalizationsDelegate(); @override diff --git a/packages/stream_chat_localizations/test/override_test.dart b/packages/stream_chat_localizations/test/override_test.dart index 6b775de167..7714fa5f5a 100644 --- a/packages/stream_chat_localizations/test/override_test.dart +++ b/packages/stream_chat_localizations/test/override_test.dart @@ -16,8 +16,7 @@ class FooStreamChatLocalizations extends StreamChatLocalizationsEn { final String launchUrlError; } -class FooStreamChatLocalizationsDelegate - extends LocalizationsDelegate { +class FooStreamChatLocalizationsDelegate extends LocalizationsDelegate { const FooStreamChatLocalizationsDelegate({ this.supportedLanguage = 'en', this.launchUrlError = 'foo', @@ -27,15 +26,12 @@ class FooStreamChatLocalizationsDelegate final String launchUrlError; @override - bool isSupported(Locale locale) => - supportedLanguage == 'allLanguages' || - locale.languageCode == supportedLanguage; + bool isSupported(Locale locale) => supportedLanguage == 'allLanguages' || locale.languageCode == supportedLanguage; @override - Future load(Locale locale) => - SynchronousFuture( - FooStreamChatLocalizations(locale, launchUrlError), - ); + Future load(Locale locale) => SynchronousFuture( + FooStreamChatLocalizations(locale, launchUrlError), + ); @override bool shouldReload(FooStreamChatLocalizationsDelegate old) => false; @@ -43,24 +39,22 @@ class FooStreamChatLocalizationsDelegate Widget buildFrame({ Locale? locale, - Iterable delegates = - GlobalStreamChatLocalizations.delegates, + Iterable delegates = GlobalStreamChatLocalizations.delegates, required WidgetBuilder buildContent, LocaleResolutionCallback? localeResolutionCallback, Iterable supportedLocales = const [ Locale('en', 'US'), Locale('hi', 'IN'), ], -}) => - MaterialApp( - color: const Color(0xFFFFFFFF), - locale: locale, - supportedLocales: supportedLocales, - localizationsDelegates: delegates, - localeResolutionCallback: localeResolutionCallback, - onGenerateRoute: (RouteSettings settings) => MaterialPageRoute( - builder: (BuildContext context) => buildContent(context)), - ); +}) => MaterialApp( + color: const Color(0xFFFFFFFF), + locale: locale, + supportedLocales: supportedLocales, + localizationsDelegates: delegates, + localeResolutionCallback: localeResolutionCallback, + onGenerateRoute: (RouteSettings settings) => + MaterialPageRoute(builder: (BuildContext context) => buildContent(context)), +); void main() { testWidgets( @@ -104,23 +98,23 @@ void main() { "Localizations.override widget tracks parent's locale", (WidgetTester tester) async { Widget buildLocaleFrame(Locale locale) => buildFrame( - locale: locale, - supportedLocales: [locale], - buildContent: (BuildContext context) => Localizations.override( - context: context, - child: Builder( - builder: (BuildContext context) { - // No StreamChatLocalizations are defined for the first - // Localizations ancestor, so we should get the values from - // the default one, i.e. the one created by WidgetsApp via - // the LocalizationsDelegate provided by MaterialApp. - return Text( - StreamChatLocalizations.of(context)!.launchUrlError, - ); - }, - ), - ), - ); + locale: locale, + supportedLocales: [locale], + buildContent: (BuildContext context) => Localizations.override( + context: context, + child: Builder( + builder: (BuildContext context) { + // No StreamChatLocalizations are defined for the first + // Localizations ancestor, so we should get the values from + // the default one, i.e. the one created by WidgetsApp via + // the LocalizationsDelegate provided by MaterialApp. + return Text( + StreamChatLocalizations.of(context)!.launchUrlError, + ); + }, + ), + ), + ); await tester.pumpWidget(buildLocaleFrame(const Locale('en', 'US'))); expect(find.text('Cannot launch the url'), findsOneWidget); @@ -130,28 +124,27 @@ void main() { }, ); - testWidgets('Localizations.override widget with hardwired locale', - (WidgetTester tester) async { + testWidgets('Localizations.override widget with hardwired locale', (WidgetTester tester) async { Widget buildLocaleFrame(Locale locale) => buildFrame( - locale: locale, - buildContent: (BuildContext context) { - return Localizations.override( - context: context, - locale: const Locale('en', 'US'), - child: Builder( - builder: (BuildContext context) { - // No StreamChatLocalizations are defined for the first - // Localizations ancestor, so we should get the values from - // the default one, i.e. the one created by WidgetsApp via - // the LocalizationsDelegate provided by MaterialApp. - return Text( - StreamChatLocalizations.of(context)!.launchUrlError, - ); - }, - ), - ); - }, + locale: locale, + buildContent: (BuildContext context) { + return Localizations.override( + context: context, + locale: const Locale('en', 'US'), + child: Builder( + builder: (BuildContext context) { + // No StreamChatLocalizations are defined for the first + // Localizations ancestor, so we should get the values from + // the default one, i.e. the one created by WidgetsApp via + // the LocalizationsDelegate provided by MaterialApp. + return Text( + StreamChatLocalizations.of(context)!.launchUrlError, + ); + }, + ), ); + }, + ); await tester.pumpWidget(buildLocaleFrame(const Locale('en', 'US'))); expect(find.text('Cannot launch the url'), findsOneWidget); @@ -165,30 +158,32 @@ void main() { (WidgetTester tester) async { final Key textKey = UniqueKey(); - await tester.pumpWidget(buildFrame( - delegates: [ - ...GlobalStreamChatLocalizations.delegates, - const FooStreamChatLocalizationsDelegate( - supportedLanguage: 'fr', - launchUrlError: "Impossible de lancer l'url", - ), - const FooStreamChatLocalizationsDelegate( - supportedLanguage: 'uz', - launchUrlError: 'test', + await tester.pumpWidget( + buildFrame( + delegates: [ + ...GlobalStreamChatLocalizations.delegates, + const FooStreamChatLocalizationsDelegate( + supportedLanguage: 'fr', + launchUrlError: "Impossible de lancer l'url", + ), + const FooStreamChatLocalizationsDelegate( + supportedLanguage: 'uz', + launchUrlError: 'test', + ), + ], + supportedLocales: const [ + Locale('en'), + Locale('hi'), + Locale('fr'), + Locale('de'), + Locale('uz'), + ], + buildContent: (BuildContext context) => Text( + StreamChatLocalizations.of(context)!.launchUrlError, + key: textKey, ), - ], - supportedLocales: const [ - Locale('en'), - Locale('hi'), - Locale('fr'), - Locale('de'), - Locale('uz'), - ], - buildContent: (BuildContext context) => Text( - StreamChatLocalizations.of(context)!.launchUrlError, - key: textKey, ), - )); + ); expect( tester.widget(find.byKey(textKey)).data, @@ -214,24 +209,25 @@ void main() { (WidgetTester tester) async { final Key textKey = UniqueKey(); - await tester.pumpWidget(buildFrame( - // Accept whatever locale we're given - localeResolutionCallback: - (Locale? locale, Iterable supportedLocales) => locale, - delegates: [ - const FooStreamChatLocalizationsDelegate( - supportedLanguage: 'allLanguages', - ), - ...GlobalStreamChatLocalizations.delegates, - ], - buildContent: (BuildContext context) { - // Should always be 'foo', no matter what the locale is - return Text( - StreamChatLocalizations.of(context)!.launchUrlError, - key: textKey, - ); - }, - )); + await tester.pumpWidget( + buildFrame( + // Accept whatever locale we're given + localeResolutionCallback: (Locale? locale, Iterable supportedLocales) => locale, + delegates: [ + const FooStreamChatLocalizationsDelegate( + supportedLanguage: 'allLanguages', + ), + ...GlobalStreamChatLocalizations.delegates, + ], + buildContent: (BuildContext context) { + // Should always be 'foo', no matter what the locale is + return Text( + StreamChatLocalizations.of(context)!.launchUrlError, + key: textKey, + ); + }, + ), + ); expect(tester.widget(find.byKey(textKey)).data, 'foo'); @@ -250,16 +246,18 @@ void main() { (WidgetTester tester) async { final Key textKey = UniqueKey(); - await tester.pumpWidget(buildFrame( - delegates: [ - const FooStreamChatLocalizationsDelegate(), - ], - // supportedLocales not specified, so all locales resolve to 'en' - buildContent: (BuildContext context) => Text( - StreamChatLocalizations.of(context)!.launchUrlError, - key: textKey, + await tester.pumpWidget( + buildFrame( + delegates: [ + const FooStreamChatLocalizationsDelegate(), + ], + // supportedLocales not specified, so all locales resolve to 'en' + buildContent: (BuildContext context) => Text( + StreamChatLocalizations.of(context)!.launchUrlError, + key: textKey, + ), ), - )); + ); // Unsupported locale '_' (the widget tester's default) resolves to 'en'. expect(tester.widget(find.byKey(textKey)).data, 'foo'); diff --git a/packages/stream_chat_localizations/test/translations_test.dart b/packages/stream_chat_localizations/test/translations_test.dart index 8c043e3787..f43cf3ddf5 100644 --- a/packages/stream_chat_localizations/test/translations_test.dart +++ b/packages/stream_chat_localizations/test/translations_test.dart @@ -7,10 +7,8 @@ void main() { for (final language in kStreamChatSupportedLanguages) { test('translations exist for $language', () async { final locale = Locale(language); - expect( - GlobalStreamChatLocalizations.delegate.isSupported(locale), isTrue); - final localizations = - await GlobalStreamChatLocalizations.delegate.load(locale); + expect(GlobalStreamChatLocalizations.delegate.isSupported(locale), isTrue); + final localizations = await GlobalStreamChatLocalizations.delegate.load(locale); expect(localizations.launchUrlError, isNotNull); expect(localizations.loadingUsersError, isNotNull); expect(localizations.noUsersLabel, isNotNull); @@ -194,18 +192,15 @@ void main() { expect(localizations.couldNotReadBytesFromFileError, isNotNull); expect(localizations.toggleMuteUnmuteAction(isMuted: false), isNotNull); expect(localizations.downloadLabel, isNotNull); - expect(localizations.toggleMuteUnmuteGroupQuestion(isMuted: true), - isNotNull); + expect(localizations.toggleMuteUnmuteGroupQuestion(isMuted: true), isNotNull); expect(localizations.toggleMuteUnmuteGroupText(isMuted: true), isNotNull); - expect( - localizations.toggleMuteUnmuteUserQuestion(isMuted: true), isNotNull); + expect(localizations.toggleMuteUnmuteUserQuestion(isMuted: true), isNotNull); expect(localizations.toggleMuteUnmuteUserText(isMuted: true), isNotNull); expect(localizations.viewLibrary, isNotNull); expect(localizations.unreadMessagesSeparatorText(), isNotNull); expect(localizations.enableFileAccessMessage, isNotNull); expect(localizations.allowFileAccessMessage, isNotNull); - expect( - localizations.unreadCountIndicatorLabel(unreadCount: 2), isNotNull); + expect(localizations.unreadCountIndicatorLabel(unreadCount: 2), isNotNull); expect(localizations.unreadMessagesSeparatorText(), isNotNull); expect(localizations.markUnreadError, isNotNull); expect(localizations.markAsUnreadLabel, isNotNull); @@ -306,11 +301,32 @@ void main() { expect(localizations.audioAttachmentText, isNotNull); expect(localizations.imageAttachmentText, isNotNull); expect(localizations.videoAttachmentText, isNotNull); + expect(localizations.fileAttachmentText, isNotNull); + expect(localizations.filesAttachmentCountText(3), isNotNull); + expect(localizations.photosAttachmentCountText(3), isNotNull); + expect(localizations.videosAttachmentCountText(3), isNotNull); expect(localizations.pollYouVotedText, isNotNull); expect(localizations.pollSomeoneVotedText('TestUser'), isNotNull); expect(localizations.pollYouCreatedText, isNotNull); expect(localizations.pollSomeoneCreatedText('TestUser'), isNotNull); expect(localizations.systemMessageLabel, isNotNull); + expect(localizations.draftLabel, isNotNull); + expect(localizations.locationLabel(), isNotNull); + expect(localizations.noConversationsYetText, isNotNull); + expect(localizations.replyToStartThreadText, isNotNull); + expect(localizations.sendMessageToStartConversationText, isNotNull); + expect(localizations.savedForLaterLabel, isNotNull); + expect(localizations.repliedToThreadAnnotationLabel, isNotNull); + expect(localizations.alsoSentInChannelAnnotationLabel, isNotNull); + expect(localizations.viewLabel, isNotNull); + expect(localizations.reminderSetLabel, isNotNull); + expect(localizations.reminderAtText('3:00 PM'), isNotNull); + expect(localizations.createPollPromptLabel, isNotNull); + expect(localizations.takePhotoAndShareLabel, isNotNull); + expect(localizations.takeVideoAndShareLabel, isNotNull); + expect(localizations.openCameraLabel, isNotNull); + expect(localizations.selectFilesToShareLabel, isNotNull); + expect(localizations.openFilesLabel, isNotNull); }); } diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index dee30e6112..67501c0c58 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,17 +1,39 @@ +## 10.0.0-beta.13 + +🛑️ Breaking +- SDK Redesign Changes. For more details, please refer to the [migration guide](https://github.com/GetStream/stream-chat-flutter/blob/210ff93f955be3f85c62e860309bd9aa240a5446/migrations). + The SDK redesign introduces a fresher default UI, but also better APIs for customization of the components. + +## 10.0.0-beta.12 + +- Included the changes from version [`9.23.0`](https://pub.dev/packages/stream_chat_persistence/changelog). + ## 9.23.0 - Updated `stream_chat` dependency to [`9.23.0`](https://pub.dev/packages/stream_chat/changelog). +## 10.0.0-beta.11 + +- Included the changes from version [`9.22.0`](https://pub.dev/packages/stream_chat_persistence/changelog). + ## 9.22.0 ✅ Added - Added support for `ChannelModel.filterTags` field. +## 10.0.0-beta.10 + +- Included the changes from version [`9.21.0`](https://pub.dev/packages/stream_chat_persistence/changelog). + ## 9.21.0 - Updated `stream_chat` dependency to [`9.21.0`](https://pub.dev/packages/stream_chat/changelog). +## 10.0.0-beta.9 + +- Included the changes from version [`9.20.0`](https://pub.dev/packages/stream_chat_persistence/changelog). + ## 9.20.0 ✅ Added @@ -19,10 +41,28 @@ - Added support for `Read.lastDeliveredAt` and `Read.lastDeliveredMessageId` fields to track message delivery receipts. +## 10.0.0-beta.8 + +✅ Added + +- Added a new `StreamChatPersistenceClient.deleteMessagesFromUser()` method to delete + all messages from a specific user across all channels. +- Added a new `messageLimit` parameter to the `getChannelStates` method + to limit the number of messages fetched per channel. + +- Included the changes from version [`9.19.0`](https://pub.dev/packages/stream_chat_persistence/changelog). + ## 9.19.0 - Updated `stream_chat` dependency to [`9.19.0`](https://pub.dev/packages/stream_chat/changelog). +## 10.0.0-beta.7 + +- Added support for `Messages.deletedForMe`, `PinnedMessages.deletedForMe`, and + `Members.deletedMessages` fields. + +- Included the changes from version [`9.18.0`](https://pub.dev/packages/stream_chat_persistence/changelog). + ## 9.18.0 ✅ Added @@ -31,14 +71,29 @@ - Added support for `client.flush()` method to clear database. - Added support for `Channel.messageCount` field. +## 10.0.0-beta.6 + +- Included the changes from version [`9.17.0`](https://pub.dev/packages/stream_chat_persistence/changelog). + ## 9.17.0 - Updated `stream_chat` dependency to [`9.17.0`](https://pub.dev/packages/stream_chat/changelog). +## 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 @@ -50,14 +105,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/lib/main.dart b/packages/stream_chat_persistence/example/lib/main.dart index 6e71a555da..f82143fbbf 100644 --- a/packages/stream_chat_persistence/example/lib/main.dart +++ b/packages/stream_chat_persistence/example/lib/main.dart @@ -22,8 +22,7 @@ Future main() async { await client.connectUser( User( id: 'cool-shadow-7', - image: - 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', + image: 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', ), 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.' 'gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo', @@ -95,31 +94,32 @@ class HomeScreen extends StatelessWidget { body: SafeArea( child: StreamBuilder( stream: messages, - builder: ( - BuildContext context, - AsyncSnapshot snapshot, - ) { - if (snapshot.hasData && snapshot.data != null) { - final _messages = snapshot.data!.messages ?? []; - return MessageView( - messages: _messages.reversed.toList(), - channel: channel, - ); - } else if (snapshot.hasError) { - return const Center( - child: Text( - 'There was an error loading messages. Please see logs.', - ), - ); - } - return const Center( - child: SizedBox( - width: 100, - height: 100, - child: CircularProgressIndicator(), - ), - ); - }, + builder: + ( + BuildContext context, + AsyncSnapshot snapshot, + ) { + if (snapshot.hasData && snapshot.data != null) { + final _messages = snapshot.data!.messages ?? []; + return MessageView( + messages: _messages.reversed.toList(), + channel: channel, + ); + } else if (snapshot.hasError) { + return const Center( + child: Text( + 'There was an error loading messages. Please see logs.', + ), + ); + } + return const Center( + child: SizedBox( + width: 100, + height: 100, + child: CircularProgressIndicator(), + ), + ); + }, ), ), ); diff --git a/packages/stream_chat_persistence/example/linux/flutter/generated_plugins.cmake b/packages/stream_chat_persistence/example/linux/flutter/generated_plugins.cmake index 7ea2a80150..22f82029d5 100644 --- a/packages/stream_chat_persistence/example/linux/flutter/generated_plugins.cmake +++ b/packages/stream_chat_persistence/example/linux/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/packages/stream_chat_persistence/example/pubspec.yaml b/packages/stream_chat_persistence/example/pubspec.yaml index adee937525..dc5b0577e5 100644 --- a/packages/stream_chat_persistence/example/pubspec.yaml +++ b/packages/stream_chat_persistence/example/pubspec.yaml @@ -16,15 +16,15 @@ version: 1.0.0+1 # 2. Add it to the melos.yaml file for future updates. environment: - sdk: ^3.6.2 - flutter: ">=3.27.4" + sdk: ^3.10.0 + flutter: ">=3.38.1" dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat: ^9.23.0 - stream_chat_persistence: ^9.23.0 + stream_chat: ^10.0.0-beta.13 + stream_chat_persistence: ^10.0.0-beta.13 flutter: uses-material-design: true \ No newline at end of file diff --git a/packages/stream_chat_persistence/example/windows/flutter/generated_plugins.cmake b/packages/stream_chat_persistence/example/windows/flutter/generated_plugins.cmake index 8abff9572e..2703737743 100644 --- a/packages/stream_chat_persistence/example/windows/flutter/generated_plugins.cmake +++ b/packages/stream_chat_persistence/example/windows/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/packages/stream_chat_persistence/lib/src/converter/voting_visibility_converter.dart b/packages/stream_chat_persistence/lib/src/converter/voting_visibility_converter.dart index 2d5786d558..047be71a46 100644 --- a/packages/stream_chat_persistence/lib/src/converter/voting_visibility_converter.dart +++ b/packages/stream_chat_persistence/lib/src/converter/voting_visibility_converter.dart @@ -2,8 +2,7 @@ import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; /// A [TypeConverter] that serializes [VotingVisibility] to a [String] column. -class VotingVisibilityConverter - extends TypeConverter { +class VotingVisibilityConverter extends TypeConverter { /// Constant default constructor. const VotingVisibilityConverter(); diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart index f3a3de415a..88754425c6 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_dao.dart @@ -9,20 +9,21 @@ part 'channel_dao.g.dart'; /// The Data Access Object for operations in [Channels] table. @DriftAccessor(tables: [Channels, Users]) -class ChannelDao extends DatabaseAccessor - with _$ChannelDaoMixin { +class ChannelDao extends DatabaseAccessor with _$ChannelDaoMixin { /// Creates a new channel dao instance ChannelDao(super.db); /// Get channel by cid - Future getChannelByCid(String cid) async => - (select(channels)..where((c) => c.cid.equals(cid))).join([ + Future getChannelByCid(String cid) async => (select(channels)..where((c) => c.cid.equals(cid))) + .join([ leftOuterJoin(users, channels.createdById.equalsExp(users.id)), - ]).map((rows) { + ]) + .map((rows) { final channel = rows.readTable(channels); final createdBy = rows.readTableOrNull(users); return channel.toChannelModel(createdBy: createdBy?.toUser()); - }).getSingleOrNull(); + }) + .getSingleOrNull(); /// Delete all channels by matching cid in [cids] /// @@ -34,17 +35,18 @@ class ChannelDao extends DatabaseAccessor (delete(channels)..where((tbl) => tbl.cid.isIn(cids))).go(); /// Get the channel cids saved in the storage - Future> get cids => (select(channels) - ..orderBy([(c) => OrderingTerm.desc(c.lastMessageAt)]) - ..limit(250)) - .map((c) => c.cid) - .get(); + Future> get cids => + (select(channels) + ..orderBy([(c) => OrderingTerm.desc(c.lastMessageAt)]) + ..limit(250)) + .map((c) => c.cid) + .get(); /// Updates all the channels using the new [channelList] data Future updateChannels(List channelList) => batch( - (it) => it.insertAllOnConflictUpdate( - channels, - channelList.map((c) => c.toEntity()).toList(), - ), - ); + (it) => it.insertAllOnConflictUpdate( + channels, + channelList.map((c) => c.toEntity()).toList(), + ), + ); } diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart index b0931ee3be..a8f0d922f8 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart @@ -12,8 +12,7 @@ part 'channel_query_dao.g.dart'; /// The Data Access Object for operations in [ChannelQueries] table. @DriftAccessor(tables: [ChannelQueries, Channels, Users]) -class ChannelQueryDao extends DatabaseAccessor - with _$ChannelQueryDaoMixin { +class ChannelQueryDao extends DatabaseAccessor with _$ChannelQueryDaoMixin { /// Creates a new channel query dao instance ChannelQueryDao(super.db); @@ -32,35 +31,29 @@ class ChannelQueryDao extends DatabaseAccessor Filter? filter, List cids, { bool clearQueryCache = false, - }) async => - transaction(() async { - final hash = _computeHash(filter); - if (clearQueryCache) { - await batch((it) { - it.deleteWhere( - channelQueries, - (c) => c.queryHash.equals(hash), - ); - }); - } - - await batch((it) { - it.insertAllOnConflictUpdate( - channelQueries, - cids - .map((cid) => - ChannelQueryEntity(queryHash: hash, channelCid: cid)) - .toList(), - ); - }); + }) async => transaction(() async { + final hash = _computeHash(filter); + if (clearQueryCache) { + await batch((it) { + it.deleteWhere( + channelQueries, + (c) => c.queryHash.equals(hash), + ); }); + } + + await batch((it) { + it.insertAllOnConflictUpdate( + channelQueries, + cids.map((cid) => ChannelQueryEntity(queryHash: hash, channelCid: cid)).toList(), + ); + }); + }); /// Future> getCachedChannelCids(Filter? filter) { final hash = _computeHash(filter); - return (select(channelQueries)..where((c) => c.queryHash.equals(hash))) - .map((c) => c.channelCid) - .get(); + return (select(channelQueries)..where((c) => c.queryHash.equals(hash))).map((c) => c.channelCid).get(); } /// Get list of channels by filter, sort and paginationParams @@ -68,13 +61,16 @@ class ChannelQueryDao extends DatabaseAccessor final cachedChannelCids = await getCachedChannelCids(filter); final query = select(channels)..where((c) => c.cid.isIn(cachedChannelCids)); - final cachedChannels = await query.join([ - leftOuterJoin(users, channels.createdById.equalsExp(users.id)), - ]).map((row) { - final createdByEntity = row.readTableOrNull(users); - final channelEntity = row.readTable(channels); - return channelEntity.toChannelModel(createdBy: createdByEntity?.toUser()); - }).get(); + final cachedChannels = await query + .join([ + leftOuterJoin(users, channels.createdById.equalsExp(users.id)), + ]) + .map((row) { + final createdByEntity = row.readTableOrNull(users); + final channelEntity = row.readTable(channels); + return channelEntity.toChannelModel(createdBy: createdByEntity?.toUser()); + }) + .get(); return cachedChannels; } diff --git a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart index fa7ff9829d..976d5931f0 100644 --- a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.dart @@ -9,37 +9,32 @@ part 'connection_event_dao.g.dart'; /// The Data Access Object for operations in [ConnectionEvents] table. @DriftAccessor(tables: [ConnectionEvents]) -class ConnectionEventDao extends DatabaseAccessor - with _$ConnectionEventDaoMixin { +class ConnectionEventDao extends DatabaseAccessor with _$ConnectionEventDaoMixin { /// Creates a new connection event dao instance ConnectionEventDao(super.db); /// Get the latest stored connection event - Future get connectionEvent => select(connectionEvents) - .map((eventEntity) => eventEntity.toEvent()) - .getSingleOrNull(); + Future get connectionEvent => + select(connectionEvents).map((eventEntity) => eventEntity.toEvent()).getSingleOrNull(); /// Get the latest stored lastSyncAt - Future get lastSyncAt => - select(connectionEvents).getSingleOrNull().then((r) => r?.lastSyncAt); + Future get lastSyncAt => select(connectionEvents).getSingleOrNull().then((r) => r?.lastSyncAt); /// Update stored connection event with latest data Future updateConnectionEvent(Event event) => transaction(() async { - final connectionInfo = await select(connectionEvents).getSingleOrNull(); - return into(connectionEvents).insertOnConflictUpdate( - ConnectionEventEntity( - id: 1, - type: event.type, - lastSyncAt: connectionInfo?.lastSyncAt, - lastEventAt: event.createdAt, - totalUnreadCount: - event.totalUnreadCount ?? connectionInfo?.totalUnreadCount, - ownUser: event.me?.toJson() ?? connectionInfo?.ownUser, - unreadChannels: - event.unreadChannels ?? connectionInfo?.unreadChannels, - ), - ); - }); + final connectionInfo = await select(connectionEvents).getSingleOrNull(); + return into(connectionEvents).insertOnConflictUpdate( + ConnectionEventEntity( + id: 1, + type: event.type, + lastSyncAt: connectionInfo?.lastSyncAt, + lastEventAt: event.createdAt, + totalUnreadCount: event.totalUnreadCount ?? connectionInfo?.totalUnreadCount, + ownUser: event.me?.toJson() ?? connectionInfo?.ownUser, + unreadChannels: event.unreadChannels ?? connectionInfo?.unreadChannels, + ), + ); + }); /// Update stored lastSyncAt with latest data Future updateLastSyncAt(DateTime lastSyncAt) async => diff --git a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.g.dart index 32541de5d5..10a23aa21a 100644 --- a/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/connection_event_dao.g.dart @@ -4,6 +4,5 @@ part of 'connection_event_dao.dart'; // ignore_for_file: type=lint mixin _$ConnectionEventDaoMixin on DatabaseAccessor { - $ConnectionEventsTable get connectionEvents => - attachedDatabase.connectionEvents; + $ConnectionEventsTable get connectionEvents => attachedDatabase.connectionEvents; } 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..a0602a0679 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 @@ -10,26 +10,34 @@ part 'draft_message_dao.g.dart'; /// The Data Access Object for operations in [DraftMessages] table. @DriftAccessor(tables: [DraftMessages, Messages]) -class DraftMessageDao extends DatabaseAccessor - with _$DraftMessageDaoMixin { +class DraftMessageDao extends DatabaseAccessor with _$DraftMessageDaoMixin { /// Creates a new draft message dao instance DraftMessageDao(this._db) : super(_db); 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..f6d6bb6cfa --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/dao/location_dao.dart @@ -0,0 +1,82 @@ +// 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/member_dao.dart b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart index fb345d3bff..7d61ecb4cc 100644 --- a/packages/stream_chat_persistence/lib/src/dao/member_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/member_dao.dart @@ -9,38 +9,39 @@ part 'member_dao.g.dart'; /// The Data Access Object for operations in [Members] table. @DriftAccessor(tables: [Members, Users]) -class MemberDao extends DatabaseAccessor - with _$MemberDaoMixin { +class MemberDao extends DatabaseAccessor with _$MemberDaoMixin { /// Creates a new member dao instance MemberDao(super.db); /// Get all members where [Members.channelCid] matches [cid] Future> getMembersByCid(String cid) async => (select(members).join([ - leftOuterJoin(users, members.userId.equalsExp(users.id)), - ]) + leftOuterJoin(users, members.userId.equalsExp(users.id)), + ]) ..where(members.channelCid.equals(cid)) ..orderBy([OrderingTerm.asc(members.createdAt)])) .map((row) { - final userEntity = row.readTable(users); - final memberEntity = row.readTable(members); - return memberEntity.toMember(user: userEntity.toUser()); - }).get(); + final userEntity = row.readTable(users); + final memberEntity = row.readTable(members); + return memberEntity.toMember(user: userEntity.toUser()); + }) + .get(); /// Updates all the members using the new [memberList] data - Future updateMembers(String cid, List memberList) => - bulkUpdateMembers({cid: memberList}); + Future updateMembers(String cid, List memberList) => bulkUpdateMembers({cid: memberList}); /// Bulk updates the members data of multiple channels Future bulkUpdateMembers( Map?> channelWithMembers, ) { final entities = channelWithMembers.entries - .map((entry) => - entry.value?.map( - (member) => member.toEntity(cid: entry.key), - ) ?? - []) + .map( + (entry) => + entry.value?.map( + (member) => member.toEntity(cid: entry.key), + ) ?? + [], + ) .expand((it) => it) .toList(growable: false); return batch( @@ -50,9 +51,9 @@ class MemberDao extends DatabaseAccessor /// Deletes all the members whose [Members.channelCid] is present in [cids] Future deleteMemberByCids(List cids) async => batch((it) { - it.deleteWhere( - members, - (m) => m.channelCid.isIn(cids), - ); - }); + it.deleteWhere( + members, + (m) => m.channelCid.isIn(cids), + ); + }); } diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart index 77d84dff07..86b8f7275c 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:math'; import 'package:drift/drift.dart'; @@ -11,8 +12,7 @@ part 'message_dao.g.dart'; /// The Data Access Object for operations in [Messages] table. @DriftAccessor(tables: [Messages, Users]) -class MessageDao extends DatabaseAccessor - with _$MessageDaoMixin { +class MessageDao extends DatabaseAccessor with _$MessageDaoMixin { /// Creates a new message dao instance MessageDao(this._db) : super(_db); @@ -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); @@ -61,9 +62,14 @@ class MessageDao extends DatabaseAccessor final draft = await switch (fetchDraft) { true => _db.draftMessageDao.getDraftMessageByCid( - msgEntity.channelCid, - parentId: msgEntity.id, - ), + msgEntity.channelCid, + parentId: msgEntity.id, + ), + _ => null, + }; + + final sharedLocation = await switch (fetchSharedLocation) { + true => _db.locationDao.getLocationByMessageId(msgEntity.id), _ => null, }; @@ -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)), @@ -92,8 +100,7 @@ class MessageDao extends DatabaseAccessor _pinnedByUsers, messages.pinnedByUserId.equalsExp(_pinnedByUsers.id), ), - ]) - ..where(messages.id.equals(id)); + ])..where(messages.id.equals(id)); final result = await query.getSingleOrNull(); if (result == null) return null; @@ -101,24 +108,26 @@ class MessageDao extends DatabaseAccessor return _messageFromJoinRow( result, fetchDraft: fetchDraft, + fetchSharedLocation: fetchSharedLocation, ); } /// Returns all the messages of a particular thread by matching /// [Messages.channelCid] with [cid] - Future> getThreadMessages(String cid) async => - Future.wait(await (select(messages).join([ - leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), - leftOuterJoin( - _pinnedByUsers, - messages.pinnedByUserId.equalsExp(_pinnedByUsers.id), - ), - ]) - ..where(messages.channelCid.equals(cid)) - ..where(messages.parentId.isNotNull()) - ..orderBy([OrderingTerm.asc(messages.createdAt)])) - .map(_messageFromJoinRow) - .get()); + Future> getThreadMessages(String cid) async => Future.wait( + await (select(messages).join([ + leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), + leftOuterJoin( + _pinnedByUsers, + messages.pinnedByUserId.equalsExp(_pinnedByUsers.id), + ), + ]) + ..where(messages.channelCid.equals(cid)) + ..where(messages.parentId.isNotNull()) + ..orderBy([OrderingTerm.asc(messages.createdAt)])) + .map(_messageFromJoinRow) + .get(), + ); /// Returns all the messages of a particular thread by matching /// [Messages.parentId] with [parentId] @@ -126,18 +135,20 @@ class MessageDao extends DatabaseAccessor String parentId, { PaginationParams? options, }) async { - final msgList = await Future.wait(await (select(messages).join([ - leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), - leftOuterJoin( - _pinnedByUsers, - messages.pinnedByUserId.equalsExp(_pinnedByUsers.id), - ), - ]) - ..where(messages.parentId.isNotNull()) - ..where(messages.parentId.equals(parentId)) - ..orderBy([OrderingTerm.asc(messages.createdAt)])) - .map(_messageFromJoinRow) - .get()); + final msgList = await Future.wait( + await (select(messages).join([ + leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), + leftOuterJoin( + _pinnedByUsers, + messages.pinnedByUserId.equalsExp(_pinnedByUsers.id), + ), + ]) + ..where(messages.parentId.isNotNull()) + ..where(messages.parentId.equals(parentId)) + ..orderBy([OrderingTerm.asc(messages.createdAt)])) + .map(_messageFromJoinRow) + .get(), + ); if (msgList.isNotEmpty) { if (options?.lessThan != null) { @@ -169,18 +180,20 @@ class MessageDao extends DatabaseAccessor Future> getMessagesByCid( String cid, { bool fetchDraft = true, + bool fetchSharedLocation = true, PaginationParams? messagePagination, }) async { - final query = select(messages).join([ - leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), - leftOuterJoin( - _pinnedByUsers, - messages.pinnedByUserId.equalsExp(_pinnedByUsers.id), - ), - ]) - ..where(messages.channelCid.equals(cid)) - ..where(messages.parentId.isNull() | messages.showInChannel.equals(true)) - ..orderBy([OrderingTerm.asc(messages.createdAt)]); + final query = + select(messages).join([ + leftOuterJoin(_users, messages.userId.equalsExp(_users.id)), + leftOuterJoin( + _pinnedByUsers, + messages.pinnedByUserId.equalsExp(_pinnedByUsers.id), + ), + ]) + ..where(messages.channelCid.equals(cid)) + ..where(messages.parentId.isNull() | messages.showInChannel.equals(true)) + ..orderBy([OrderingTerm.asc(messages.createdAt)]); final result = await query.get(); if (result.isEmpty) return []; @@ -190,6 +203,7 @@ class MessageDao extends DatabaseAccessor (row) => _messageFromJoinRow( row, fetchDraft: fetchDraft, + fetchSharedLocation: fetchSharedLocation, ), ), ); @@ -212,29 +226,74 @@ class MessageDao extends DatabaseAccessor } } if (messagePagination?.limit != null) { - return msgList - .skip(max(0, msgList.length - messagePagination!.limit)) - .toList(); + return msgList.skip(max(0, msgList.length - messagePagination!.limit)).toList(); } } return msgList; } + /// Deletes all messages sent by a user with the given [userId]. + /// + /// If [hardDelete] is `true`, permanently removes messages from the database. + /// Otherwise, soft-deletes them by updating their type, deletion timestamp, + /// and state. + /// + /// If [cid] is provided, only deletes messages in that channel. Otherwise, + /// deletes messages across all channels. + /// + /// The [deletedAt] timestamp is used for soft deletes. Defaults to the + /// current time if not provided. + /// + /// Returns the number of rows affected. + Future deleteMessagesByUser({ + String? cid, + required String userId, + bool hardDelete = false, + DateTime? deletedAt, + }) async { + if (hardDelete) { + // Hard delete: remove from database + final deleteQuery = delete(messages)..where((tbl) => tbl.userId.equals(userId)); + + if (cid != null) { + deleteQuery.where((tbl) => tbl.channelCid.equals(cid)); + } + + return deleteQuery.go(); + } + + // Soft delete: update messages to mark as deleted + final updateQuery = update(messages)..where((tbl) => tbl.userId.equals(userId)); + + if (cid != null) { + updateQuery.where((tbl) => tbl.channelCid.equals(cid)); + } + + return updateQuery.write( + MessagesCompanion( + type: const Value('deleted'), + remoteDeletedAt: Value(deletedAt ?? DateTime.now()), + state: Value(jsonEncode(MessageState.softDeleted)), + ), + ); + } + /// Updates the message data of a particular channel with /// the new [messageList] data - Future updateMessages(String cid, List messageList) => - bulkUpdateMessages({cid: messageList}); + Future updateMessages(String cid, List messageList) => bulkUpdateMessages({cid: messageList}); /// Bulk updates the message data of multiple channels Future bulkUpdateMessages( Map?> channelWithMessages, ) { final entities = channelWithMessages.entries - .map((entry) => - entry.value?.map( - (message) => message.toEntity(cid: entry.key), - ) ?? - []) + .map( + (entry) => + entry.value?.map( + (message) => message.toEntity(cid: entry.key), + ) ?? + [], + ) .expand((it) => it) .toList(growable: false); return batch( 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..413c7164f4 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 @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:drift/drift.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; @@ -10,8 +12,7 @@ part 'pinned_message_dao.g.dart'; /// The Data Access Object for operations in [Messages] table. @DriftAccessor(tables: [PinnedMessages, Users]) -class PinnedMessageDao extends DatabaseAccessor - with _$PinnedMessageDaoMixin { +class PinnedMessageDao extends DatabaseAccessor with _$PinnedMessageDaoMixin { /// Creates a new message dao instance PinnedMessageDao(this._db) : super(_db); @@ -38,14 +39,13 @@ 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); final msgEntity = rows.readTable(pinnedMessages); - final latestReactions = - await _db.pinnedMessageReactionDao.getReactions(msgEntity.id); - final ownReactions = - await _db.pinnedMessageReactionDao.getReactionsByUserId( + final latestReactions = await _db.pinnedMessageReactionDao.getReactions(msgEntity.id); + final ownReactions = await _db.pinnedMessageReactionDao.getReactionsByUserId( msgEntity.id, _db.userId, ); @@ -62,9 +62,14 @@ class PinnedMessageDao extends DatabaseAccessor final draft = await switch (fetchDraft) { true => _db.draftMessageDao.getDraftMessageByCid( - msgEntity.channelCid, - parentId: msgEntity.id, - ), + msgEntity.channelCid, + parentId: msgEntity.id, + ), + _ => null, + }; + + final sharedLocation = await switch (fetchSharedLocation) { + true => _db.locationDao.getLocationByMessageId(msgEntity.id), _ => null, }; @@ -76,6 +81,7 @@ class PinnedMessageDao extends DatabaseAccessor quotedMessage: quotedMessage, poll: poll, draft: draft, + sharedLocation: sharedLocation, ); } @@ -83,6 +89,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)), @@ -90,8 +97,7 @@ class PinnedMessageDao extends DatabaseAccessor _pinnedByUsers, pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id), ), - ]) - ..where(pinnedMessages.id.equals(id)); + ])..where(pinnedMessages.id.equals(id)); final result = await query.getSingleOrNull(); if (result == null) return null; @@ -99,24 +105,26 @@ class PinnedMessageDao extends DatabaseAccessor return _messageFromJoinRow( result, fetchDraft: fetchDraft, + fetchSharedLocation: fetchSharedLocation, ); } /// Returns all the messages of a particular thread by matching /// [PinnedMessages.channelCid] with [cid] - Future> getThreadMessages(String cid) async => - Future.wait(await (select(pinnedMessages).join([ - leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), - leftOuterJoin( - _pinnedByUsers, - pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id), - ), - ]) - ..where(pinnedMessages.channelCid.equals(cid)) - ..where(pinnedMessages.parentId.isNotNull()) - ..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)])) - .map(_messageFromJoinRow) - .get()); + Future> getThreadMessages(String cid) async => Future.wait( + await (select(pinnedMessages).join([ + leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), + leftOuterJoin( + _pinnedByUsers, + pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id), + ), + ]) + ..where(pinnedMessages.channelCid.equals(cid)) + ..where(pinnedMessages.parentId.isNotNull()) + ..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)])) + .map(_messageFromJoinRow) + .get(), + ); /// Returns all the messages of a particular thread by matching /// [PinnedMessages.parentId] with [parentId] @@ -124,18 +132,20 @@ class PinnedMessageDao extends DatabaseAccessor String parentId, { PaginationParams? options, }) async { - final msgList = await Future.wait(await (select(pinnedMessages).join([ - leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), - leftOuterJoin( - _pinnedByUsers, - pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id), - ), - ]) - ..where(pinnedMessages.parentId.isNotNull()) - ..where(pinnedMessages.parentId.equals(parentId)) - ..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)])) - .map(_messageFromJoinRow) - .get()); + final msgList = await Future.wait( + await (select(pinnedMessages).join([ + leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), + leftOuterJoin( + _pinnedByUsers, + pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id), + ), + ]) + ..where(pinnedMessages.parentId.isNotNull()) + ..where(pinnedMessages.parentId.equals(parentId)) + ..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)])) + .map(_messageFromJoinRow) + .get(), + ); if (msgList.isNotEmpty) { if (options?.lessThan != null) { @@ -166,19 +176,20 @@ class PinnedMessageDao extends DatabaseAccessor Future> getMessagesByCid( String cid, { bool fetchDraft = true, + bool fetchSharedLocation = true, PaginationParams? messagePagination, }) async { - final query = select(pinnedMessages).join([ - leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), - leftOuterJoin( - _pinnedByUsers, - pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id), - ), - ]) - ..where(pinnedMessages.channelCid.equals(cid)) - ..where(pinnedMessages.parentId.isNull() | - pinnedMessages.showInChannel.equals(true)) - ..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)]); + final query = + select(pinnedMessages).join([ + leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)), + leftOuterJoin( + _pinnedByUsers, + pinnedMessages.pinnedByUserId.equalsExp(_pinnedByUsers.id), + ), + ]) + ..where(pinnedMessages.channelCid.equals(cid)) + ..where(pinnedMessages.parentId.isNull() | pinnedMessages.showInChannel.equals(true)) + ..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)]); final result = await query.get(); if (result.isEmpty) return []; @@ -188,6 +199,7 @@ class PinnedMessageDao extends DatabaseAccessor (row) => _messageFromJoinRow( row, fetchDraft: fetchDraft, + fetchSharedLocation: fetchSharedLocation, ), ), ); @@ -216,21 +228,68 @@ class PinnedMessageDao extends DatabaseAccessor return msgList; } + /// Deletes all pinned messages sent by a user with the given [userId]. + /// + /// If [hardDelete] is `true`, permanently removes pinned messages from the + /// database. Otherwise, soft-deletes them by updating their type, deletion + /// timestamp, and state. + /// + /// If [cid] is provided, only deletes pinned messages in that channel. + /// Otherwise, deletes pinned messages across all channels. + /// + /// The [deletedAt] timestamp is used for soft deletes. Defaults to the + /// current time if not provided. + /// + /// Returns the number of rows affected. + Future deleteMessagesByUser({ + String? cid, + required String userId, + bool hardDelete = false, + DateTime? deletedAt, + }) async { + if (hardDelete) { + // Hard delete: remove from database + final deleteQuery = delete(pinnedMessages)..where((tbl) => tbl.userId.equals(userId)); + + if (cid != null) { + deleteQuery.where((tbl) => tbl.channelCid.equals(cid)); + } + + return deleteQuery.go(); + } + + // Soft delete: update messages to mark as deleted + final updateQuery = update(pinnedMessages)..where((tbl) => tbl.userId.equals(userId)); + + if (cid != null) { + updateQuery.where((tbl) => tbl.channelCid.equals(cid)); + } + + return updateQuery.write( + PinnedMessagesCompanion( + type: const Value('deleted'), + remoteDeletedAt: Value(deletedAt ?? DateTime.now()), + state: Value(jsonEncode(MessageState.softDeleted)), + ), + ); + } + /// Updates the message data of a particular channel with /// the new [messageList] data - Future updateMessages(String cid, List messageList) => - bulkUpdateMessages({cid: messageList}); + Future updateMessages(String cid, List messageList) => bulkUpdateMessages({cid: messageList}); /// Bulk updates the message data of multiple channels Future bulkUpdateMessages( Map?> channelWithMessages, ) { final entities = channelWithMessages.entries - .map((entry) => - entry.value?.map( - (message) => message.toPinnedEntity(cid: entry.key), - ) ?? - []) + .map( + (entry) => + entry.value?.map( + (message) => message.toPinnedEntity(cid: entry.key), + ) ?? + [], + ) .expand((it) => it) .toList(growable: false); return batch( diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart index c5c5a6d456..9a81c6e46e 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.dart @@ -9,8 +9,7 @@ part 'pinned_message_reaction_dao.g.dart'; /// The Data Access Object for operations in [PinnedMessageReactions] table. @DriftAccessor(tables: [PinnedMessageReactions, Users]) -class PinnedMessageReactionDao extends DatabaseAccessor - with _$PinnedMessageReactionDaoMixin { +class PinnedMessageReactionDao extends DatabaseAccessor with _$PinnedMessageReactionDaoMixin { /// Creates a new reaction dao instance PinnedMessageReactionDao(super.db); @@ -18,15 +17,16 @@ class PinnedMessageReactionDao extends DatabaseAccessor /// [Reactions.messageId] with [messageId] Future> getReactions(String messageId) => (select(pinnedMessageReactions).join([ - leftOuterJoin(users, pinnedMessageReactions.userId.equalsExp(users.id)), - ]) + leftOuterJoin(users, pinnedMessageReactions.userId.equalsExp(users.id)), + ]) ..where(pinnedMessageReactions.messageId.equals(messageId)) ..orderBy([OrderingTerm.asc(pinnedMessageReactions.createdAt)])) .map((rows) { - final userEntity = rows.readTableOrNull(users); - final reactionEntity = rows.readTable(pinnedMessageReactions); - return reactionEntity.toReaction(user: userEntity?.toUser()); - }).get(); + final userEntity = rows.readTableOrNull(users); + final reactionEntity = rows.readTable(pinnedMessageReactions); + return reactionEntity.toReaction(user: userEntity?.toUser()); + }) + .get(); /// Returns all the reactions of a particular message /// added by a particular user by matching @@ -42,19 +42,18 @@ class PinnedMessageReactionDao extends DatabaseAccessor /// Updates the reactions data with the new [reactionList] data Future updateReactions(List reactionList) => batch((it) { - it.insertAllOnConflictUpdate( - pinnedMessageReactions, - reactionList.map((r) => r.toPinnedEntity()).toList(), - ); - }); + it.insertAllOnConflictUpdate( + pinnedMessageReactions, + reactionList.map((r) => r.toPinnedEntity()).toList(), + ); + }); /// Deletes all the reactions whose [Reactions.messageId] is /// present in [messageIds] - Future deleteReactionsByMessageIds(List messageIds) => - batch((it) { - it.deleteWhere( - pinnedMessageReactions, - (r) => r.messageId.isIn(messageIds), - ); - }); + Future deleteReactionsByMessageIds(List messageIds) => batch((it) { + it.deleteWhere( + pinnedMessageReactions, + (r) => r.messageId.isIn(messageIds), + ); + }); } diff --git a/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart index d25c2f4512..0b7393b68b 100644 --- a/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart +++ b/packages/stream_chat_persistence/lib/src/dao/pinned_message_reaction_dao.g.dart @@ -5,7 +5,6 @@ part of 'pinned_message_reaction_dao.dart'; // ignore_for_file: type=lint mixin _$PinnedMessageReactionDaoMixin on DatabaseAccessor { $PinnedMessagesTable get pinnedMessages => attachedDatabase.pinnedMessages; - $PinnedMessageReactionsTable get pinnedMessageReactions => - attachedDatabase.pinnedMessageReactions; + $PinnedMessageReactionsTable get pinnedMessageReactions => attachedDatabase.pinnedMessageReactions; $UsersTable get users => attachedDatabase.users; } diff --git a/packages/stream_chat_persistence/lib/src/dao/poll_dao.dart b/packages/stream_chat_persistence/lib/src/dao/poll_dao.dart index 10a10a024d..43924285e7 100644 --- a/packages/stream_chat_persistence/lib/src/dao/poll_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/poll_dao.dart @@ -45,28 +45,27 @@ class PollDao extends DatabaseAccessor with _$PollDaoMixin { } /// Returns the poll by matching [Polls.id] with [pollId] - Future getPollById(String pollId) async => - await (select(polls)..where((it) => it.id.equals(pollId))) - .join([leftOuterJoin(users, polls.createdById.equalsExp(users.id))]) - .map(_pollFromJoinRow) - .getSingleOrNull(); + Future getPollById(String pollId) async => await (select(polls)..where((it) => it.id.equals(pollId))) + .join([leftOuterJoin(users, polls.createdById.equalsExp(users.id))]) + .map(_pollFromJoinRow) + .getSingleOrNull(); /// Updates all the polls using the new [pollList] data Future updatePolls(List pollList) => batch( - (it) => it.insertAllOnConflictUpdate( - polls, - pollList.map((it) => it.toEntity()), - ), - ); + (it) => it.insertAllOnConflictUpdate( + polls, + pollList.map((it) => it.toEntity()), + ), + ); /// Returns the list of all the polls stored in db - Future> getPolls() async => Future.wait(await (select(polls) - ..orderBy([(it) => OrderingTerm.desc(it.createdAt)])) - .join([leftOuterJoin(users, polls.createdById.equalsExp(users.id))]) - .map(_pollFromJoinRow) - .get()); + Future> getPolls() async => Future.wait( + await (select(polls)..orderBy([(it) => OrderingTerm.desc(it.createdAt)])) + .join([leftOuterJoin(users, polls.createdById.equalsExp(users.id))]) + .map(_pollFromJoinRow) + .get(), + ); /// Deletes all the polls whose [Polls.id] is present in [pollIds] - Future deletePollsByIds(List pollIds) => - (delete(polls)..where((tbl) => tbl.id.isIn(pollIds))).go(); + Future deletePollsByIds(List pollIds) => (delete(polls)..where((tbl) => tbl.id.isIn(pollIds))).go(); } diff --git a/packages/stream_chat_persistence/lib/src/dao/poll_vote_dao.dart b/packages/stream_chat_persistence/lib/src/dao/poll_vote_dao.dart index 55884f4a70..6fb765d8e3 100644 --- a/packages/stream_chat_persistence/lib/src/dao/poll_vote_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/poll_vote_dao.dart @@ -11,8 +11,7 @@ part 'poll_vote_dao.g.dart'; /// The Data Access Object for operations in [Polls] table. @DriftAccessor(tables: [PollVotes, Users]) -class PollVoteDao extends DatabaseAccessor - with _$PollVoteDaoMixin { +class PollVoteDao extends DatabaseAccessor with _$PollVoteDaoMixin { /// Creates a new poll vote dao instance PollVoteDao(super.db); @@ -20,23 +19,24 @@ class PollVoteDao extends DatabaseAccessor /// [Reactions.messageId] with [messageId] Future> getPollVotes(String pollId) => (select(pollVotes).join([ - leftOuterJoin(users, pollVotes.userId.equalsExp(users.id)), - ]) + leftOuterJoin(users, pollVotes.userId.equalsExp(users.id)), + ]) ..where(pollVotes.pollId.equals(pollId)) ..orderBy([OrderingTerm.asc(pollVotes.createdAt)])) .map((rows) { - final userEntity = rows.readTableOrNull(users); - final pollVoteEntity = rows.readTable(pollVotes); - return pollVoteEntity.toPollVote(user: userEntity?.toUser()); - }).get(); + final userEntity = rows.readTableOrNull(users); + final pollVoteEntity = rows.readTable(pollVotes); + return pollVoteEntity.toPollVote(user: userEntity?.toUser()); + }) + .get(); /// Updates the poll votes data with the new [pollVoteList] data Future updatePollVotes(List pollVoteList) => batch( - (it) => it.insertAllOnConflictUpdate( - pollVotes, - pollVoteList.map((it) => it.toEntity()), - ), - ); + (it) => it.insertAllOnConflictUpdate( + pollVotes, + pollVoteList.map((it) => it.toEntity()), + ), + ); /// Deletes all the poll votes whose [PollVote.pollId] is /// present in [pollIds] diff --git a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart index d6dae9bd99..2a68626159 100644 --- a/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/reaction_dao.dart @@ -9,8 +9,7 @@ part 'reaction_dao.g.dart'; /// The Data Access Object for operations in [Reactions] table. @DriftAccessor(tables: [Reactions, Users]) -class ReactionDao extends DatabaseAccessor - with _$ReactionDaoMixin { +class ReactionDao extends DatabaseAccessor with _$ReactionDaoMixin { /// Creates a new reaction dao instance ReactionDao(super.db); @@ -18,15 +17,16 @@ class ReactionDao extends DatabaseAccessor /// [Reactions.messageId] with [messageId] Future> getReactions(String messageId) => (select(reactions).join([ - leftOuterJoin(users, reactions.userId.equalsExp(users.id)), - ]) + leftOuterJoin(users, reactions.userId.equalsExp(users.id)), + ]) ..where(reactions.messageId.equals(messageId)) ..orderBy([OrderingTerm.asc(reactions.createdAt)])) .map((rows) { - final userEntity = rows.readTableOrNull(users); - final reactionEntity = rows.readTable(reactions); - return reactionEntity.toReaction(user: userEntity?.toUser()); - }).get(); + final userEntity = rows.readTableOrNull(users); + final reactionEntity = rows.readTable(reactions); + return reactionEntity.toReaction(user: userEntity?.toUser()); + }) + .get(); /// Returns all the reactions of a particular message /// added by a particular user by matching @@ -42,19 +42,18 @@ class ReactionDao extends DatabaseAccessor /// Updates the reactions data with the new [reactionList] data Future updateReactions(List reactionList) => batch((it) { - it.insertAllOnConflictUpdate( - reactions, - reactionList.map((r) => r.toEntity()).toList(), - ); - }); + it.insertAllOnConflictUpdate( + reactions, + reactionList.map((r) => r.toEntity()).toList(), + ); + }); /// Deletes all the reactions whose [Reactions.messageId] is /// present in [messageIds] - Future deleteReactionsByMessageIds(List messageIds) => - batch((it) { - it.deleteWhere( - reactions, - (r) => r.messageId.isIn(messageIds), - ); - }); + Future deleteReactionsByMessageIds(List messageIds) => batch((it) { + it.deleteWhere( + reactions, + (r) => r.messageId.isIn(messageIds), + ); + }); } diff --git a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart index 4e2024f1ff..c6beec0f9f 100644 --- a/packages/stream_chat_persistence/lib/src/dao/read_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/read_dao.dart @@ -14,32 +14,35 @@ class ReadDao extends DatabaseAccessor with _$ReadDaoMixin { ReadDao(super.db); /// Get all reads where [Reads.channelCid] matches [cid] - Future> getReadsByCid(String cid) async => (select(reads).join([ - leftOuterJoin(users, reads.userId.equalsExp(users.id)), - ]) + Future> getReadsByCid(String cid) async => + (select(reads).join([ + leftOuterJoin(users, reads.userId.equalsExp(users.id)), + ]) ..where(reads.channelCid.equals(cid)) ..orderBy([ OrderingTerm.asc(reads.lastRead), ])) .map((row) { - final userEntity = row.readTable(users); - final readEntity = row.readTable(reads); - return readEntity.toRead(user: userEntity.toUser()); - }).get(); + final userEntity = row.readTable(users); + final readEntity = row.readTable(reads); + return readEntity.toRead(user: userEntity.toUser()); + }) + .get(); /// Updates the read data of a particular channel with /// the new [readList] data - Future updateReads(String cid, List readList) => - bulkUpdateReads({cid: readList}); + Future updateReads(String cid, List readList) => bulkUpdateReads({cid: readList}); /// Bulk updates the reads data of multiple channels Future bulkUpdateReads(Map?> channelWithReads) { final entities = channelWithReads.entries - .map((entry) => - entry.value?.map( - (read) => read.toEntity(cid: entry.key), - ) ?? - []) + .map( + (entry) => + entry.value?.map( + (read) => read.toEntity(cid: entry.key), + ) ?? + [], + ) .expand((it) => it) .toList(growable: false); return batch((batch) => batch.insertAllOnConflictUpdate(reads, entities)); diff --git a/packages/stream_chat_persistence/lib/src/dao/user_dao.dart b/packages/stream_chat_persistence/lib/src/dao/user_dao.dart index 1366dd2541..214425d882 100644 --- a/packages/stream_chat_persistence/lib/src/dao/user_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/user_dao.dart @@ -14,15 +14,13 @@ class UserDao extends DatabaseAccessor with _$UserDaoMixin { /// Updates the users data with the new [userList] data Future updateUsers(List userList) => batch( - (it) => it.insertAllOnConflictUpdate( - users, - userList.map((u) => u.toEntity()).toList(), - ), - ); + (it) => it.insertAllOnConflictUpdate( + users, + userList.map((u) => u.toEntity()).toList(), + ), + ); /// Returns the list of all the users stored in db Future> getUsers() => - (select(users)..orderBy([(u) => OrderingTerm.desc(u.createdAt)])) - .map((it) => it.toUser()) - .get(); + (select(users)..orderBy([(u) => OrderingTerm.desc(u.createdAt)])).map((it) => it.toUser()).get(); } 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 a412abb546..3fc6eb552d 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,22 +57,22 @@ class DriftChatDatabase extends _$DriftChatDatabase { // you should bump this number whenever you change or add a table definition. @override - int get schemaVersion => 27; + int get schemaVersion => 1000 + 29; @override MigrationStrategy get migration => MigrationStrategy( - beforeOpen: (details) async { - await customStatement('PRAGMA foreign_keys = ON'); - }, - onUpgrade: (migrator, from, to) async { - if (from != to) { - for (final table in allTables) { - await migrator.deleteTable(table.actualTableName); - } - await migrator.createAll(); - } - }, - ); + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON'); + }, + onUpgrade: (migrator, from, to) async { + if (from != to) { + for (final table in allTables) { + await migrator.deleteTable(table.actualTableName); + } + await migrator.createAll(); + } + }, + ); /// Deletes all the tables Future flush() async { 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 4fc780bd4b..899609385e 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 @@ -3,8 +3,7 @@ part of 'drift_chat_database.dart'; // ignore_for_file: type=lint -class $ChannelsTable extends Channels - with TableInfo<$ChannelsTable, ChannelEntity> { +class $ChannelsTable extends Channels with TableInfo<$ChannelsTable, ChannelEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -12,131 +11,164 @@ class $ChannelsTable extends Channels static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + 'id', + 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: true); + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); static const VerificationMeta _cidMeta = const VerificationMeta('cid'); @override late final GeneratedColumn cid = GeneratedColumn( - 'cid', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _ownCapabilitiesMeta = - const VerificationMeta('ownCapabilities'); - @override - late final GeneratedColumnWithTypeConverter?, String> - ownCapabilities = GeneratedColumn( - 'own_capabilities', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $ChannelsTable.$converterownCapabilitiesn); - static const VerificationMeta _configMeta = const VerificationMeta('config'); - @override - late final GeneratedColumnWithTypeConverter, String> - config = GeneratedColumn('config', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>($ChannelsTable.$converterconfig); + 'cid', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + late final GeneratedColumnWithTypeConverter?, String> ownCapabilities = GeneratedColumn( + 'own_capabilities', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($ChannelsTable.$converterownCapabilitiesn); + @override + late final GeneratedColumnWithTypeConverter, String> config = GeneratedColumn( + 'config', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ).withConverter>($ChannelsTable.$converterconfig); static const VerificationMeta _frozenMeta = const VerificationMeta('frozen'); @override late final GeneratedColumn frozen = GeneratedColumn( - 'frozen', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("frozen" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _lastMessageAtMeta = - const VerificationMeta('lastMessageAt'); - @override - late final GeneratedColumn lastMessageAt = - GeneratedColumn('last_message_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); + 'frozen', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("frozen" IN (0, 1))'), + defaultValue: const Constant(false), + ); + static const VerificationMeta _lastMessageAtMeta = const VerificationMeta('lastMessageAt'); + @override + late final GeneratedColumn lastMessageAt = GeneratedColumn( + 'last_message_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + 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'); + '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 _deletedAtMeta = - const VerificationMeta('deletedAt'); + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + static const VerificationMeta _deletedAtMeta = const VerificationMeta('deletedAt'); @override late final GeneratedColumn deletedAt = GeneratedColumn( - 'deleted_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _memberCountMeta = - const VerificationMeta('memberCount'); + 'deleted_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _memberCountMeta = const VerificationMeta('memberCount'); @override late final GeneratedColumn memberCount = GeneratedColumn( - 'member_count', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const Constant(0)); - static const VerificationMeta _messageCountMeta = - const VerificationMeta('messageCount'); + 'member_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _messageCountMeta = const VerificationMeta('messageCount'); @override late final GeneratedColumn messageCount = GeneratedColumn( - 'message_count', aliasedName, true, - type: DriftSqlType.int, requiredDuringInsert: false); - static const VerificationMeta _createdByIdMeta = - const VerificationMeta('createdById'); + 'message_count', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); + 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 _filterTagsMeta = - const VerificationMeta('filterTags'); - @override - late final GeneratedColumnWithTypeConverter?, String> - filterTags = GeneratedColumn('filter_tags', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>($ChannelsTable.$converterfilterTagsn); - static const VerificationMeta _extraDataMeta = - const VerificationMeta('extraData'); - @override - late final GeneratedColumnWithTypeConverter?, String> - extraData = GeneratedColumn('extra_data', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $ChannelsTable.$converterextraDatan); + 'created_by_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + late final GeneratedColumnWithTypeConverter?, String> filterTags = GeneratedColumn( + 'filter_tags', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($ChannelsTable.$converterfilterTagsn); + @override + late final GeneratedColumnWithTypeConverter?, String> extraData = GeneratedColumn( + 'extra_data', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($ChannelsTable.$converterextraDatan); @override List get $columns => [ - id, - type, - cid, - ownCapabilities, - config, - frozen, - lastMessageAt, - createdAt, - updatedAt, - deletedAt, - memberCount, - messageCount, - createdById, - filterTags, - extraData - ]; + id, + type, + cid, + ownCapabilities, + config, + frozen, + lastMessageAt, + createdAt, + updatedAt, + deletedAt, + memberCount, + messageCount, + createdById, + filterTags, + extraData, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'channels'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -145,61 +177,42 @@ class $ChannelsTable extends Channels context.missing(_idMeta); } if (data.containsKey('type')) { - context.handle( - _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + context.handle(_typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); } else if (isInserting) { context.missing(_typeMeta); } if (data.containsKey('cid')) { - context.handle( - _cidMeta, cid.isAcceptableOrUnknown(data['cid']!, _cidMeta)); + context.handle(_cidMeta, cid.isAcceptableOrUnknown(data['cid']!, _cidMeta)); } else if (isInserting) { context.missing(_cidMeta); } - context.handle(_ownCapabilitiesMeta, const VerificationResult.success()); - context.handle(_configMeta, const VerificationResult.success()); if (data.containsKey('frozen')) { - context.handle(_frozenMeta, - frozen.isAcceptableOrUnknown(data['frozen']!, _frozenMeta)); + context.handle(_frozenMeta, frozen.isAcceptableOrUnknown(data['frozen']!, _frozenMeta)); } if (data.containsKey('last_message_at')) { context.handle( - _lastMessageAtMeta, - lastMessageAt.isAcceptableOrUnknown( - data['last_message_at']!, _lastMessageAtMeta)); + _lastMessageAtMeta, + lastMessageAt.isAcceptableOrUnknown(data['last_message_at']!, _lastMessageAtMeta), + ); } if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + 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(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); } if (data.containsKey('deleted_at')) { - context.handle(_deletedAtMeta, - deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta)); + context.handle(_deletedAtMeta, deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta)); } if (data.containsKey('member_count')) { - context.handle( - _memberCountMeta, - memberCount.isAcceptableOrUnknown( - data['member_count']!, _memberCountMeta)); + context.handle(_memberCountMeta, memberCount.isAcceptableOrUnknown(data['member_count']!, _memberCountMeta)); } if (data.containsKey('message_count')) { - context.handle( - _messageCountMeta, - messageCount.isAcceptableOrUnknown( - data['message_count']!, _messageCountMeta)); + context.handle(_messageCountMeta, messageCount.isAcceptableOrUnknown(data['message_count']!, _messageCountMeta)); } if (data.containsKey('created_by_id')) { - context.handle( - _createdByIdMeta, - createdById.isAcceptableOrUnknown( - data['created_by_id']!, _createdByIdMeta)); + context.handle(_createdByIdMeta, createdById.isAcceptableOrUnknown(data['created_by_id']!, _createdByIdMeta)); } - context.handle(_filterTagsMeta, const VerificationResult.success()); - context.handle(_extraDataMeta, const VerificationResult.success()); return context; } @@ -209,40 +222,32 @@ class $ChannelsTable extends Channels ChannelEntity map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return ChannelEntity( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - type: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}type'])!, - cid: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}cid'])!, + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + type: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}type'])!, + cid: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}cid'])!, ownCapabilities: $ChannelsTable.$converterownCapabilitiesn.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}own_capabilities'])), - config: $ChannelsTable.$converterconfig.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}config'])!), - frozen: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}frozen'])!, + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}own_capabilities']), + ), + config: $ChannelsTable.$converterconfig.fromSql( + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}config'])!, + ), + frozen: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}frozen'])!, lastMessageAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}last_message_at']), - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - updatedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, - deletedAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), - memberCount: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}member_count'])!, - messageCount: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}message_count']), - createdById: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}created_by_id']), - filterTags: $ChannelsTable.$converterfilterTagsn.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}filter_tags'])), - extraData: $ChannelsTable.$converterextraDatan.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), + DriftSqlType.dateTime, + data['${effectivePrefix}last_message_at'], + ), + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + deletedAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + memberCount: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}member_count'])!, + messageCount: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}message_count']), + createdById: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}created_by_id']), + filterTags: $ChannelsTable.$converterfilterTagsn.fromSql( + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}filter_tags']), + ), + extraData: $ChannelsTable.$converterextraDatan.fromSql( + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}extra_data']), + ), ); } @@ -251,20 +256,19 @@ class $ChannelsTable extends Channels return $ChannelsTable(attachedDatabase, alias); } - static TypeConverter, String> $converterownCapabilities = - ListConverter(); - static TypeConverter?, String?> $converterownCapabilitiesn = - NullAwareTypeConverter.wrap($converterownCapabilities); - static TypeConverter, String> $converterconfig = - MapConverter(); - static TypeConverter, String> $converterfilterTags = - ListConverter(); - static TypeConverter?, String?> $converterfilterTagsn = - NullAwareTypeConverter.wrap($converterfilterTags); - static TypeConverter, String> $converterextraData = - MapConverter(); - static TypeConverter?, String?> $converterextraDatan = - NullAwareTypeConverter.wrap($converterextraData); + static TypeConverter, String> $converterownCapabilities = ListConverter(); + static TypeConverter?, String?> $converterownCapabilitiesn = NullAwareTypeConverter.wrap( + $converterownCapabilities, + ); + static TypeConverter, String> $converterconfig = MapConverter(); + static TypeConverter, String> $converterfilterTags = ListConverter(); + static TypeConverter?, String?> $converterfilterTagsn = NullAwareTypeConverter.wrap( + $converterfilterTags, + ); + static TypeConverter, String> $converterextraData = MapConverter(); + static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap( + $converterextraData, + ); } class ChannelEntity extends DataClass implements Insertable { @@ -312,22 +316,23 @@ class ChannelEntity extends DataClass implements Insertable { /// Map of custom channel extraData final Map? extraData; - const ChannelEntity( - {required this.id, - required this.type, - required this.cid, - this.ownCapabilities, - required this.config, - required this.frozen, - this.lastMessageAt, - required this.createdAt, - required this.updatedAt, - this.deletedAt, - required this.memberCount, - this.messageCount, - this.createdById, - this.filterTags, - this.extraData}); + const ChannelEntity({ + required this.id, + required this.type, + required this.cid, + this.ownCapabilities, + required this.config, + required this.frozen, + this.lastMessageAt, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.memberCount, + this.messageCount, + this.createdById, + this.filterTags, + this.extraData, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -335,12 +340,10 @@ class ChannelEntity extends DataClass implements Insertable { map['type'] = Variable(type); map['cid'] = Variable(cid); if (!nullToAbsent || ownCapabilities != null) { - map['own_capabilities'] = Variable( - $ChannelsTable.$converterownCapabilitiesn.toSql(ownCapabilities)); + map['own_capabilities'] = Variable($ChannelsTable.$converterownCapabilitiesn.toSql(ownCapabilities)); } { - map['config'] = - Variable($ChannelsTable.$converterconfig.toSql(config)); + map['config'] = Variable($ChannelsTable.$converterconfig.toSql(config)); } map['frozen'] = Variable(frozen); if (!nullToAbsent || lastMessageAt != null) { @@ -359,25 +362,21 @@ class ChannelEntity extends DataClass implements Insertable { map['created_by_id'] = Variable(createdById); } if (!nullToAbsent || filterTags != null) { - map['filter_tags'] = Variable( - $ChannelsTable.$converterfilterTagsn.toSql(filterTags)); + map['filter_tags'] = Variable($ChannelsTable.$converterfilterTagsn.toSql(filterTags)); } if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $ChannelsTable.$converterextraDatan.toSql(extraData)); + map['extra_data'] = Variable($ChannelsTable.$converterextraDatan.toSql(extraData)); } return map; } - factory ChannelEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + factory ChannelEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return ChannelEntity( id: serializer.fromJson(json['id']), type: serializer.fromJson(json['type']), cid: serializer.fromJson(json['cid']), - ownCapabilities: - serializer.fromJson?>(json['ownCapabilities']), + ownCapabilities: serializer.fromJson?>(json['ownCapabilities']), config: serializer.fromJson>(json['config']), frozen: serializer.fromJson(json['frozen']), lastMessageAt: serializer.fromJson(json['lastMessageAt']), @@ -413,68 +412,55 @@ class ChannelEntity extends DataClass implements Insertable { }; } - ChannelEntity copyWith( - {String? id, - String? type, - String? cid, - Value?> ownCapabilities = const Value.absent(), - Map? config, - bool? frozen, - Value lastMessageAt = const Value.absent(), - DateTime? createdAt, - DateTime? updatedAt, - Value deletedAt = const Value.absent(), - int? memberCount, - Value messageCount = const Value.absent(), - Value createdById = const Value.absent(), - Value?> filterTags = const Value.absent(), - Value?> extraData = const Value.absent()}) => - ChannelEntity( - id: id ?? this.id, - type: type ?? this.type, - cid: cid ?? this.cid, - ownCapabilities: ownCapabilities.present - ? ownCapabilities.value - : this.ownCapabilities, - config: config ?? this.config, - frozen: frozen ?? this.frozen, - lastMessageAt: - lastMessageAt.present ? lastMessageAt.value : this.lastMessageAt, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, - memberCount: memberCount ?? this.memberCount, - messageCount: - messageCount.present ? messageCount.value : this.messageCount, - createdById: createdById.present ? createdById.value : this.createdById, - filterTags: filterTags.present ? filterTags.value : this.filterTags, - extraData: extraData.present ? extraData.value : this.extraData, - ); + ChannelEntity copyWith({ + String? id, + String? type, + String? cid, + Value?> ownCapabilities = const Value.absent(), + Map? config, + bool? frozen, + Value lastMessageAt = const Value.absent(), + DateTime? createdAt, + DateTime? updatedAt, + Value deletedAt = const Value.absent(), + int? memberCount, + Value messageCount = const Value.absent(), + Value createdById = const Value.absent(), + Value?> filterTags = const Value.absent(), + Value?> extraData = const Value.absent(), + }) => ChannelEntity( + id: id ?? this.id, + type: type ?? this.type, + cid: cid ?? this.cid, + ownCapabilities: ownCapabilities.present ? ownCapabilities.value : this.ownCapabilities, + config: config ?? this.config, + frozen: frozen ?? this.frozen, + lastMessageAt: lastMessageAt.present ? lastMessageAt.value : this.lastMessageAt, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + memberCount: memberCount ?? this.memberCount, + messageCount: messageCount.present ? messageCount.value : this.messageCount, + createdById: createdById.present ? createdById.value : this.createdById, + filterTags: filterTags.present ? filterTags.value : this.filterTags, + extraData: extraData.present ? extraData.value : this.extraData, + ); ChannelEntity copyWithCompanion(ChannelsCompanion data) { return ChannelEntity( id: data.id.present ? data.id.value : this.id, type: data.type.present ? data.type.value : this.type, cid: data.cid.present ? data.cid.value : this.cid, - ownCapabilities: data.ownCapabilities.present - ? data.ownCapabilities.value - : this.ownCapabilities, + ownCapabilities: data.ownCapabilities.present ? data.ownCapabilities.value : this.ownCapabilities, config: data.config.present ? data.config.value : this.config, frozen: data.frozen.present ? data.frozen.value : this.frozen, - lastMessageAt: data.lastMessageAt.present - ? data.lastMessageAt.value - : this.lastMessageAt, + lastMessageAt: data.lastMessageAt.present ? data.lastMessageAt.value : this.lastMessageAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, - memberCount: - data.memberCount.present ? data.memberCount.value : this.memberCount, - messageCount: data.messageCount.present - ? data.messageCount.value - : this.messageCount, - createdById: - data.createdById.present ? data.createdById.value : this.createdById, - filterTags: - data.filterTags.present ? data.filterTags.value : this.filterTags, + memberCount: data.memberCount.present ? data.memberCount.value : this.memberCount, + messageCount: data.messageCount.present ? data.messageCount.value : this.messageCount, + createdById: data.createdById.present ? data.createdById.value : this.createdById, + filterTags: data.filterTags.present ? data.filterTags.value : this.filterTags, extraData: data.extraData.present ? data.extraData.value : this.extraData, ); } @@ -503,21 +489,22 @@ class ChannelEntity extends DataClass implements Insertable { @override int get hashCode => Object.hash( - id, - type, - cid, - ownCapabilities, - config, - frozen, - lastMessageAt, - createdAt, - updatedAt, - deletedAt, - memberCount, - messageCount, - createdById, - filterTags, - extraData); + id, + type, + cid, + ownCapabilities, + config, + frozen, + lastMessageAt, + createdAt, + updatedAt, + deletedAt, + memberCount, + messageCount, + createdById, + filterTags, + extraData, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -591,10 +578,10 @@ class ChannelsCompanion extends UpdateCompanion { this.filterTags = const Value.absent(), this.extraData = const Value.absent(), this.rowid = const Value.absent(), - }) : id = Value(id), - type = Value(type), - cid = Value(cid), - config = Value(config); + }) : id = Value(id), + type = Value(type), + cid = Value(cid), + config = Value(config); static Insertable custom({ Expression? id, Expression? type, @@ -633,23 +620,24 @@ class ChannelsCompanion extends UpdateCompanion { }); } - ChannelsCompanion copyWith( - {Value? id, - Value? type, - Value? cid, - Value?>? ownCapabilities, - Value>? config, - Value? frozen, - Value? lastMessageAt, - Value? createdAt, - Value? updatedAt, - Value? deletedAt, - Value? memberCount, - Value? messageCount, - Value? createdById, - Value?>? filterTags, - Value?>? extraData, - Value? rowid}) { + ChannelsCompanion copyWith({ + Value? id, + Value? type, + Value? cid, + Value?>? ownCapabilities, + Value>? config, + Value? frozen, + Value? lastMessageAt, + Value? createdAt, + Value? updatedAt, + Value? deletedAt, + Value? memberCount, + Value? messageCount, + Value? createdById, + Value?>? filterTags, + Value?>? extraData, + Value? rowid, + }) { return ChannelsCompanion( id: id ?? this.id, type: type ?? this.type, @@ -683,13 +671,12 @@ class ChannelsCompanion extends UpdateCompanion { map['cid'] = Variable(cid.value); } if (ownCapabilities.present) { - map['own_capabilities'] = Variable($ChannelsTable - .$converterownCapabilitiesn - .toSql(ownCapabilities.value)); + map['own_capabilities'] = Variable( + $ChannelsTable.$converterownCapabilitiesn.toSql(ownCapabilities.value), + ); } if (config.present) { - map['config'] = - Variable($ChannelsTable.$converterconfig.toSql(config.value)); + map['config'] = Variable($ChannelsTable.$converterconfig.toSql(config.value)); } if (frozen.present) { map['frozen'] = Variable(frozen.value); @@ -716,12 +703,10 @@ class ChannelsCompanion extends UpdateCompanion { map['created_by_id'] = Variable(createdById.value); } if (filterTags.present) { - map['filter_tags'] = Variable( - $ChannelsTable.$converterfilterTagsn.toSql(filterTags.value)); + map['filter_tags'] = Variable($ChannelsTable.$converterfilterTagsn.toSql(filterTags.value)); } if (extraData.present) { - map['extra_data'] = Variable( - $ChannelsTable.$converterextraDatan.toSql(extraData.value)); + map['extra_data'] = Variable($ChannelsTable.$converterextraDatan.toSql(extraData.value)); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -753,8 +738,7 @@ class ChannelsCompanion extends UpdateCompanion { } } -class $MessagesTable extends Messages - with TableInfo<$MessagesTable, MessageEntity> { +class $MessagesTable extends Messages with TableInfo<$MessagesTable, MessageEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -762,252 +746,336 @@ class $MessagesTable extends Messages 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'); + '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); - static const VerificationMeta _attachmentsMeta = - const VerificationMeta('attachments'); - @override - late final GeneratedColumnWithTypeConverter, String> - attachments = GeneratedColumn('attachments', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>($MessagesTable.$converterattachments); + '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>($MessagesTable.$converterattachments); static const VerificationMeta _stateMeta = const VerificationMeta('state'); @override late final GeneratedColumn state = GeneratedColumn( - 'state', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + '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')); - static const VerificationMeta _mentionedUsersMeta = - const VerificationMeta('mentionedUsers'); - @override - late final GeneratedColumnWithTypeConverter, String> - mentionedUsers = GeneratedColumn( - 'mentioned_users', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>($MessagesTable.$convertermentionedUsers); - static const VerificationMeta _reactionGroupsMeta = - const VerificationMeta('reactionGroups'); - @override - late final GeneratedColumnWithTypeConverter?, - String> reactionGroups = GeneratedColumn( - 'reaction_groups', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $MessagesTable.$converterreactionGroupsn); - static const VerificationMeta _parentIdMeta = - const VerificationMeta('parentId'); + '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>($MessagesTable.$convertermentionedUsers); + @override + late final GeneratedColumnWithTypeConverter?, String> reactionGroups = + GeneratedColumn( + 'reaction_groups', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($MessagesTable.$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'); + '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); + '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'); + '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'); + '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'); + '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'); + '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); + '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 _deletedForMeMeta = const VerificationMeta('deletedForMe'); + @override + late final GeneratedColumn deletedForMe = GeneratedColumn( + 'deleted_for_me', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("deleted_for_me" IN (0, 1))'), + ); + 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 _channelRoleMeta = - const VerificationMeta('channelRole'); + 'user_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _channelRoleMeta = const VerificationMeta('channelRole'); @override late final GeneratedColumn channelRole = GeneratedColumn( - 'channel_role', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + 'channel_role', + 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'); + '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'); + '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'); + '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'); + '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, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES channels (cid) ON DELETE CASCADE')); - static const VerificationMeta _i18nMeta = const VerificationMeta('i18n'); - @override - late final GeneratedColumnWithTypeConverter?, String> - i18n = GeneratedColumn('i18n', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>($MessagesTable.$converteri18n); - static const VerificationMeta _restrictedVisibilityMeta = - const VerificationMeta('restrictedVisibility'); - @override - late final GeneratedColumnWithTypeConverter?, String> - restrictedVisibility = GeneratedColumn( - 'restricted_visibility', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $MessagesTable.$converterrestrictedVisibilityn); - static const VerificationMeta _extraDataMeta = - const VerificationMeta('extraData'); - @override - late final GeneratedColumnWithTypeConverter?, String> - extraData = GeneratedColumn('extra_data', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $MessagesTable.$converterextraDatan); + 'channel_cid', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES channels (cid) ON DELETE CASCADE'), + ); + @override + late final GeneratedColumnWithTypeConverter?, String> i18n = GeneratedColumn( + 'i18n', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($MessagesTable.$converteri18n); + @override + late final GeneratedColumnWithTypeConverter?, String> restrictedVisibility = GeneratedColumn( + 'restricted_visibility', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($MessagesTable.$converterrestrictedVisibilityn); + @override + late final GeneratedColumnWithTypeConverter?, String> extraData = GeneratedColumn( + 'extra_data', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($MessagesTable.$converterextraDatan); @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, - channelRole, - pinned, - pinnedAt, - pinExpires, - pinnedByUserId, - channelCid, - i18n, - restrictedVisibility, - extraData - ]; + id, + messageText, + attachments, + state, + type, + mentionedUsers, + reactionGroups, + parentId, + quotedMessageId, + pollId, + replyCount, + showInChannel, + shadowed, + command, + localCreatedAt, + remoteCreatedAt, + localUpdatedAt, + remoteUpdatedAt, + localDeletedAt, + remoteDeletedAt, + deletedForMe, + messageTextUpdatedAt, + userId, + channelRole, + pinned, + pinnedAt, + pinExpires, + pinnedByUserId, + channelCid, + i18n, + restrictedVisibility, + extraData, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'messages'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -1016,142 +1084,114 @@ class $MessagesTable extends Messages context.missing(_idMeta); } if (data.containsKey('message_text')) { - context.handle( - _messageTextMeta, - messageText.isAcceptableOrUnknown( - data['message_text']!, _messageTextMeta)); + context.handle(_messageTextMeta, messageText.isAcceptableOrUnknown(data['message_text']!, _messageTextMeta)); } - context.handle(_attachmentsMeta, const VerificationResult.success()); if (data.containsKey('state')) { - context.handle( - _stateMeta, state.isAcceptableOrUnknown(data['state']!, _stateMeta)); + 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)); + context.handle(_typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); } - context.handle(_mentionedUsersMeta, const VerificationResult.success()); - context.handle(_reactionGroupsMeta, const VerificationResult.success()); if (data.containsKey('parent_id')) { - context.handle(_parentIdMeta, - parentId.isAcceptableOrUnknown(data['parent_id']!, _parentIdMeta)); + 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)); + _quotedMessageIdMeta, + quotedMessageId.isAcceptableOrUnknown(data['quoted_message_id']!, _quotedMessageIdMeta), + ); } if (data.containsKey('poll_id')) { - context.handle(_pollIdMeta, - pollId.isAcceptableOrUnknown(data['poll_id']!, _pollIdMeta)); + context.handle(_pollIdMeta, pollId.isAcceptableOrUnknown(data['poll_id']!, _pollIdMeta)); } if (data.containsKey('reply_count')) { - context.handle( - _replyCountMeta, - replyCount.isAcceptableOrUnknown( - data['reply_count']!, _replyCountMeta)); + 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)); + _showInChannelMeta, + showInChannel.isAcceptableOrUnknown(data['show_in_channel']!, _showInChannelMeta), + ); } if (data.containsKey('shadowed')) { - context.handle(_shadowedMeta, - shadowed.isAcceptableOrUnknown(data['shadowed']!, _shadowedMeta)); + context.handle(_shadowedMeta, shadowed.isAcceptableOrUnknown(data['shadowed']!, _shadowedMeta)); } if (data.containsKey('command')) { - context.handle(_commandMeta, - command.isAcceptableOrUnknown(data['command']!, _commandMeta)); + context.handle(_commandMeta, command.isAcceptableOrUnknown(data['command']!, _commandMeta)); } if (data.containsKey('local_created_at')) { context.handle( - _localCreatedAtMeta, - localCreatedAt.isAcceptableOrUnknown( - data['local_created_at']!, _localCreatedAtMeta)); + _localCreatedAtMeta, + localCreatedAt.isAcceptableOrUnknown(data['local_created_at']!, _localCreatedAtMeta), + ); } if (data.containsKey('remote_created_at')) { context.handle( - _remoteCreatedAtMeta, - remoteCreatedAt.isAcceptableOrUnknown( - data['remote_created_at']!, _remoteCreatedAtMeta)); + _remoteCreatedAtMeta, + remoteCreatedAt.isAcceptableOrUnknown(data['remote_created_at']!, _remoteCreatedAtMeta), + ); } if (data.containsKey('local_updated_at')) { context.handle( - _localUpdatedAtMeta, - localUpdatedAt.isAcceptableOrUnknown( - data['local_updated_at']!, _localUpdatedAtMeta)); + _localUpdatedAtMeta, + localUpdatedAt.isAcceptableOrUnknown(data['local_updated_at']!, _localUpdatedAtMeta), + ); } if (data.containsKey('remote_updated_at')) { context.handle( - _remoteUpdatedAtMeta, - remoteUpdatedAt.isAcceptableOrUnknown( - data['remote_updated_at']!, _remoteUpdatedAtMeta)); + _remoteUpdatedAtMeta, + remoteUpdatedAt.isAcceptableOrUnknown(data['remote_updated_at']!, _remoteUpdatedAtMeta), + ); } if (data.containsKey('local_deleted_at')) { context.handle( - _localDeletedAtMeta, - localDeletedAt.isAcceptableOrUnknown( - data['local_deleted_at']!, _localDeletedAtMeta)); + _localDeletedAtMeta, + localDeletedAt.isAcceptableOrUnknown(data['local_deleted_at']!, _localDeletedAtMeta), + ); } if (data.containsKey('remote_deleted_at')) { context.handle( - _remoteDeletedAtMeta, - remoteDeletedAt.isAcceptableOrUnknown( - data['remote_deleted_at']!, _remoteDeletedAtMeta)); + _remoteDeletedAtMeta, + remoteDeletedAt.isAcceptableOrUnknown(data['remote_deleted_at']!, _remoteDeletedAtMeta), + ); + } + if (data.containsKey('deleted_for_me')) { + context.handle(_deletedForMeMeta, deletedForMe.isAcceptableOrUnknown(data['deleted_for_me']!, _deletedForMeMeta)); } if (data.containsKey('message_text_updated_at')) { context.handle( - _messageTextUpdatedAtMeta, - messageTextUpdatedAt.isAcceptableOrUnknown( - data['message_text_updated_at']!, _messageTextUpdatedAtMeta)); + _messageTextUpdatedAtMeta, + messageTextUpdatedAt.isAcceptableOrUnknown(data['message_text_updated_at']!, _messageTextUpdatedAtMeta), + ); } if (data.containsKey('user_id')) { - context.handle(_userIdMeta, - userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + context.handle(_userIdMeta, userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); } if (data.containsKey('channel_role')) { - context.handle( - _channelRoleMeta, - channelRole.isAcceptableOrUnknown( - data['channel_role']!, _channelRoleMeta)); + context.handle(_channelRoleMeta, channelRole.isAcceptableOrUnknown(data['channel_role']!, _channelRoleMeta)); } if (data.containsKey('pinned')) { - context.handle(_pinnedMeta, - pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); + context.handle(_pinnedMeta, pinned.isAcceptableOrUnknown(data['pinned']!, _pinnedMeta)); } if (data.containsKey('pinned_at')) { - context.handle(_pinnedAtMeta, - pinnedAt.isAcceptableOrUnknown(data['pinned_at']!, _pinnedAtMeta)); + context.handle(_pinnedAtMeta, pinnedAt.isAcceptableOrUnknown(data['pinned_at']!, _pinnedAtMeta)); } if (data.containsKey('pin_expires')) { - context.handle( - _pinExpiresMeta, - pinExpires.isAcceptableOrUnknown( - data['pin_expires']!, _pinExpiresMeta)); + 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)); + _pinnedByUserIdMeta, + pinnedByUserId.isAcceptableOrUnknown(data['pinned_by_user_id']!, _pinnedByUserIdMeta), + ); } if (data.containsKey('channel_cid')) { - context.handle( - _channelCidMeta, - channelCid.isAcceptableOrUnknown( - data['channel_cid']!, _channelCidMeta)); + context.handle(_channelCidMeta, channelCid.isAcceptableOrUnknown(data['channel_cid']!, _channelCidMeta)); } else if (isInserting) { context.missing(_channelCidMeta); } - context.handle(_i18nMeta, const VerificationResult.success()); - context.handle( - _restrictedVisibilityMeta, const VerificationResult.success()); - context.handle(_extraDataMeta, const VerificationResult.success()); return context; } @@ -1161,74 +1201,77 @@ class $MessagesTable extends Messages MessageEntity map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return MessageEntity( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - messageText: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}message_text']), - attachments: $MessagesTable.$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'])!, + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + messageText: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}message_text']), + attachments: $MessagesTable.$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: $MessagesTable.$convertermentionedUsers.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}mentioned_users'])!), + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}mentioned_users'])!, + ), reactionGroups: $MessagesTable.$converterreactionGroupsn.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}reaction_groups'])), - parentId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}parent_id']), + 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']), + 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']), + DriftSqlType.dateTime, + data['${effectivePrefix}local_created_at'], + ), remoteCreatedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}remote_created_at']), + DriftSqlType.dateTime, + data['${effectivePrefix}remote_created_at'], + ), localUpdatedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}local_updated_at']), + DriftSqlType.dateTime, + data['${effectivePrefix}local_updated_at'], + ), remoteUpdatedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}remote_updated_at']), + DriftSqlType.dateTime, + data['${effectivePrefix}remote_updated_at'], + ), localDeletedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}local_deleted_at']), + DriftSqlType.dateTime, + data['${effectivePrefix}local_deleted_at'], + ), remoteDeletedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}remote_deleted_at']), + DriftSqlType.dateTime, + data['${effectivePrefix}remote_deleted_at'], + ), + deletedForMe: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}deleted_for_me']), messageTextUpdatedAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, - data['${effectivePrefix}message_text_updated_at']), - userId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}user_id']), - channelRole: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}channel_role']), - 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']), + DriftSqlType.dateTime, + data['${effectivePrefix}message_text_updated_at'], + ), + userId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}user_id']), + channelRole: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}channel_role']), + 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: $MessagesTable.$converteri18n.fromSql(attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}i18n'])), - restrictedVisibility: $MessagesTable.$converterrestrictedVisibilityn - .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, - data['${effectivePrefix}restricted_visibility'])), - extraData: $MessagesTable.$converterextraDatan.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), + DriftSqlType.string, + data['${effectivePrefix}pinned_by_user_id'], + ), + channelCid: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, + i18n: $MessagesTable.$converteri18n.fromSql( + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}i18n']), + ), + restrictedVisibility: $MessagesTable.$converterrestrictedVisibilityn.fromSql( + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}restricted_visibility']), + ), + extraData: $MessagesTable.$converterextraDatan.fromSql( + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}extra_data']), + ), ); } @@ -1237,25 +1280,21 @@ class $MessagesTable extends Messages return $MessagesTable(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); + 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 MessageEntity extends DataClass implements Insertable { @@ -1320,6 +1359,9 @@ class MessageEntity extends DataClass implements Insertable { /// The DateTime on which the message was deleted on the server. final DateTime? remoteDeletedAt; + /// Whether the message was deleted only for the current user. + final bool? deletedForMe; + /// The DateTime at which the message text was edited final DateTime? messageTextUpdatedAt; @@ -1352,38 +1394,40 @@ class MessageEntity extends DataClass implements Insertable { /// Message custom extraData final Map? extraData; - const MessageEntity( - {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, - this.channelRole, - required this.pinned, - this.pinnedAt, - this.pinExpires, - this.pinnedByUserId, - required this.channelCid, - this.i18n, - this.restrictedVisibility, - this.extraData}); + const MessageEntity({ + 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.deletedForMe, + this.messageTextUpdatedAt, + this.userId, + this.channelRole, + 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 = {}; @@ -1392,18 +1436,15 @@ class MessageEntity extends DataClass implements Insertable { map['message_text'] = Variable(messageText); } { - map['attachments'] = Variable( - $MessagesTable.$converterattachments.toSql(attachments)); + map['attachments'] = Variable($MessagesTable.$converterattachments.toSql(attachments)); } map['state'] = Variable(state); map['type'] = Variable(type); { - map['mentioned_users'] = Variable( - $MessagesTable.$convertermentionedUsers.toSql(mentionedUsers)); + map['mentioned_users'] = Variable($MessagesTable.$convertermentionedUsers.toSql(mentionedUsers)); } if (!nullToAbsent || reactionGroups != null) { - map['reaction_groups'] = Variable( - $MessagesTable.$converterreactionGroupsn.toSql(reactionGroups)); + map['reaction_groups'] = Variable($MessagesTable.$converterreactionGroupsn.toSql(reactionGroups)); } if (!nullToAbsent || parentId != null) { map['parent_id'] = Variable(parentId); @@ -1442,6 +1483,9 @@ class MessageEntity extends DataClass implements Insertable { if (!nullToAbsent || remoteDeletedAt != null) { map['remote_deleted_at'] = Variable(remoteDeletedAt); } + if (!nullToAbsent || deletedForMe != null) { + map['deleted_for_me'] = Variable(deletedForMe); + } if (!nullToAbsent || messageTextUpdatedAt != null) { map['message_text_updated_at'] = Variable(messageTextUpdatedAt); } @@ -1466,19 +1510,17 @@ class MessageEntity extends DataClass implements Insertable { map['i18n'] = Variable($MessagesTable.$converteri18n.toSql(i18n)); } if (!nullToAbsent || restrictedVisibility != null) { - map['restricted_visibility'] = Variable($MessagesTable - .$converterrestrictedVisibilityn - .toSql(restrictedVisibility)); + map['restricted_visibility'] = Variable( + $MessagesTable.$converterrestrictedVisibilityn.toSql(restrictedVisibility), + ); } if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $MessagesTable.$converterextraDatan.toSql(extraData)); + map['extra_data'] = Variable($MessagesTable.$converterextraDatan.toSql(extraData)); } return map; } - factory MessageEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + factory MessageEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return MessageEntity( id: serializer.fromJson(json['id']), @@ -1487,8 +1529,7 @@ class MessageEntity extends DataClass implements Insertable { state: serializer.fromJson(json['state']), type: serializer.fromJson(json['type']), mentionedUsers: serializer.fromJson>(json['mentionedUsers']), - reactionGroups: serializer - .fromJson?>(json['reactionGroups']), + reactionGroups: serializer.fromJson?>(json['reactionGroups']), parentId: serializer.fromJson(json['parentId']), quotedMessageId: serializer.fromJson(json['quotedMessageId']), pollId: serializer.fromJson(json['pollId']), @@ -1502,8 +1543,8 @@ class MessageEntity extends DataClass implements Insertable { remoteUpdatedAt: serializer.fromJson(json['remoteUpdatedAt']), localDeletedAt: serializer.fromJson(json['localDeletedAt']), remoteDeletedAt: serializer.fromJson(json['remoteDeletedAt']), - messageTextUpdatedAt: - serializer.fromJson(json['messageTextUpdatedAt']), + deletedForMe: serializer.fromJson(json['deletedForMe']), + messageTextUpdatedAt: serializer.fromJson(json['messageTextUpdatedAt']), userId: serializer.fromJson(json['userId']), channelRole: serializer.fromJson(json['channelRole']), pinned: serializer.fromJson(json['pinned']), @@ -1512,8 +1553,7 @@ class MessageEntity extends DataClass implements Insertable { pinnedByUserId: serializer.fromJson(json['pinnedByUserId']), channelCid: serializer.fromJson(json['channelCid']), i18n: serializer.fromJson?>(json['i18n']), - restrictedVisibility: - serializer.fromJson?>(json['restrictedVisibility']), + restrictedVisibility: serializer.fromJson?>(json['restrictedVisibility']), extraData: serializer.fromJson?>(json['extraData']), ); } @@ -1527,8 +1567,7 @@ class MessageEntity extends DataClass implements Insertable { 'state': serializer.toJson(state), 'type': serializer.toJson(type), 'mentionedUsers': serializer.toJson>(mentionedUsers), - 'reactionGroups': - serializer.toJson?>(reactionGroups), + 'reactionGroups': serializer.toJson?>(reactionGroups), 'parentId': serializer.toJson(parentId), 'quotedMessageId': serializer.toJson(quotedMessageId), 'pollId': serializer.toJson(pollId), @@ -1542,8 +1581,8 @@ class MessageEntity extends DataClass implements Insertable { 'remoteUpdatedAt': serializer.toJson(remoteUpdatedAt), 'localDeletedAt': serializer.toJson(localDeletedAt), 'remoteDeletedAt': serializer.toJson(remoteDeletedAt), - 'messageTextUpdatedAt': - serializer.toJson(messageTextUpdatedAt), + 'deletedForMe': serializer.toJson(deletedForMe), + 'messageTextUpdatedAt': serializer.toJson(messageTextUpdatedAt), 'userId': serializer.toJson(userId), 'channelRole': serializer.toJson(channelRole), 'pinned': serializer.toJson(pinned), @@ -1552,156 +1591,111 @@ class MessageEntity extends DataClass implements Insertable { 'pinnedByUserId': serializer.toJson(pinnedByUserId), 'channelCid': serializer.toJson(channelCid), 'i18n': serializer.toJson?>(i18n), - 'restrictedVisibility': - serializer.toJson?>(restrictedVisibility), + 'restrictedVisibility': serializer.toJson?>(restrictedVisibility), 'extraData': serializer.toJson?>(extraData), }; } - MessageEntity 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(), - Value channelRole = 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()}) => - MessageEntity( - 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, - channelRole: channelRole.present ? channelRole.value : this.channelRole, - 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, - ); + MessageEntity 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 deletedForMe = const Value.absent(), + Value messageTextUpdatedAt = const Value.absent(), + Value userId = const Value.absent(), + Value channelRole = 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(), + }) => MessageEntity( + 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, + deletedForMe: deletedForMe.present ? deletedForMe.value : this.deletedForMe, + messageTextUpdatedAt: messageTextUpdatedAt.present ? messageTextUpdatedAt.value : this.messageTextUpdatedAt, + userId: userId.present ? userId.value : this.userId, + channelRole: channelRole.present ? channelRole.value : this.channelRole, + 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, + ); MessageEntity copyWithCompanion(MessagesCompanion data) { return MessageEntity( 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, + 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, + 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, + 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, + 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, + 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, + deletedForMe: data.deletedForMe.present ? data.deletedForMe.value : this.deletedForMe, messageTextUpdatedAt: data.messageTextUpdatedAt.present ? data.messageTextUpdatedAt.value : this.messageTextUpdatedAt, userId: data.userId.present ? data.userId.value : this.userId, - channelRole: - data.channelRole.present ? data.channelRole.value : this.channelRole, + channelRole: data.channelRole.present ? data.channelRole.value : this.channelRole, 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, + 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 @@ -1733,6 +1727,7 @@ class MessageEntity extends DataClass implements Insertable { ..write('remoteUpdatedAt: $remoteUpdatedAt, ') ..write('localDeletedAt: $localDeletedAt, ') ..write('remoteDeletedAt: $remoteDeletedAt, ') + ..write('deletedForMe: $deletedForMe, ') ..write('messageTextUpdatedAt: $messageTextUpdatedAt, ') ..write('userId: $userId, ') ..write('channelRole: $channelRole, ') @@ -1750,38 +1745,39 @@ class MessageEntity extends DataClass implements Insertable { @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, - channelRole, - pinned, - pinnedAt, - pinExpires, - pinnedByUserId, - channelCid, - i18n, - restrictedVisibility, - extraData - ]); + id, + messageText, + attachments, + state, + type, + mentionedUsers, + reactionGroups, + parentId, + quotedMessageId, + pollId, + replyCount, + showInChannel, + shadowed, + command, + localCreatedAt, + remoteCreatedAt, + localUpdatedAt, + remoteUpdatedAt, + localDeletedAt, + remoteDeletedAt, + deletedForMe, + messageTextUpdatedAt, + userId, + channelRole, + pinned, + pinnedAt, + pinExpires, + pinnedByUserId, + channelCid, + i18n, + restrictedVisibility, + extraData, + ]); @override bool operator ==(Object other) => identical(this, other) || @@ -1806,6 +1802,7 @@ class MessageEntity extends DataClass implements Insertable { other.remoteUpdatedAt == this.remoteUpdatedAt && other.localDeletedAt == this.localDeletedAt && other.remoteDeletedAt == this.remoteDeletedAt && + other.deletedForMe == this.deletedForMe && other.messageTextUpdatedAt == this.messageTextUpdatedAt && other.userId == this.userId && other.channelRole == this.channelRole && @@ -1840,6 +1837,7 @@ class MessagesCompanion extends UpdateCompanion { final Value remoteUpdatedAt; final Value localDeletedAt; final Value remoteDeletedAt; + final Value deletedForMe; final Value messageTextUpdatedAt; final Value userId; final Value channelRole; @@ -1873,6 +1871,7 @@ class MessagesCompanion extends UpdateCompanion { this.remoteUpdatedAt = const Value.absent(), this.localDeletedAt = const Value.absent(), this.remoteDeletedAt = const Value.absent(), + this.deletedForMe = const Value.absent(), this.messageTextUpdatedAt = const Value.absent(), this.userId = const Value.absent(), this.channelRole = const Value.absent(), @@ -1907,6 +1906,7 @@ class MessagesCompanion extends UpdateCompanion { this.remoteUpdatedAt = const Value.absent(), this.localDeletedAt = const Value.absent(), this.remoteDeletedAt = const Value.absent(), + this.deletedForMe = const Value.absent(), this.messageTextUpdatedAt = const Value.absent(), this.userId = const Value.absent(), this.channelRole = const Value.absent(), @@ -1919,11 +1919,11 @@ class MessagesCompanion extends UpdateCompanion { 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); + }) : id = Value(id), + attachments = Value(attachments), + state = Value(state), + mentionedUsers = Value(mentionedUsers), + channelCid = Value(channelCid); static Insertable custom({ Expression? id, Expression? messageText, @@ -1945,6 +1945,7 @@ class MessagesCompanion extends UpdateCompanion { Expression? remoteUpdatedAt, Expression? localDeletedAt, Expression? remoteDeletedAt, + Expression? deletedForMe, Expression? messageTextUpdatedAt, Expression? userId, Expression? channelRole, @@ -1979,8 +1980,8 @@ class MessagesCompanion extends UpdateCompanion { 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 (deletedForMe != null) 'deleted_for_me': deletedForMe, + if (messageTextUpdatedAt != null) 'message_text_updated_at': messageTextUpdatedAt, if (userId != null) 'user_id': userId, if (channelRole != null) 'channel_role': channelRole, if (pinned != null) 'pinned': pinned, @@ -1989,46 +1990,47 @@ class MessagesCompanion extends UpdateCompanion { 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 (restrictedVisibility != null) 'restricted_visibility': restrictedVisibility, if (extraData != null) 'extra_data': extraData, if (rowid != null) 'rowid': rowid, }); } - MessagesCompanion 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? channelRole, - Value? pinned, - Value? pinnedAt, - Value? pinExpires, - Value? pinnedByUserId, - Value? channelCid, - Value?>? i18n, - Value?>? restrictedVisibility, - Value?>? extraData, - Value? rowid}) { + MessagesCompanion 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? deletedForMe, + Value? messageTextUpdatedAt, + Value? userId, + Value? channelRole, + Value? pinned, + Value? pinnedAt, + Value? pinExpires, + Value? pinnedByUserId, + Value? channelCid, + Value?>? i18n, + Value?>? restrictedVisibility, + Value?>? extraData, + Value? rowid, + }) { return MessagesCompanion( id: id ?? this.id, messageText: messageText ?? this.messageText, @@ -2050,6 +2052,7 @@ class MessagesCompanion extends UpdateCompanion { remoteUpdatedAt: remoteUpdatedAt ?? this.remoteUpdatedAt, localDeletedAt: localDeletedAt ?? this.localDeletedAt, remoteDeletedAt: remoteDeletedAt ?? this.remoteDeletedAt, + deletedForMe: deletedForMe ?? this.deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt ?? this.messageTextUpdatedAt, userId: userId ?? this.userId, channelRole: channelRole ?? this.channelRole, @@ -2075,8 +2078,7 @@ class MessagesCompanion extends UpdateCompanion { map['message_text'] = Variable(messageText.value); } if (attachments.present) { - map['attachments'] = Variable( - $MessagesTable.$converterattachments.toSql(attachments.value)); + map['attachments'] = Variable($MessagesTable.$converterattachments.toSql(attachments.value)); } if (state.present) { map['state'] = Variable(state.value); @@ -2085,12 +2087,10 @@ class MessagesCompanion extends UpdateCompanion { map['type'] = Variable(type.value); } if (mentionedUsers.present) { - map['mentioned_users'] = Variable( - $MessagesTable.$convertermentionedUsers.toSql(mentionedUsers.value)); + map['mentioned_users'] = Variable($MessagesTable.$convertermentionedUsers.toSql(mentionedUsers.value)); } if (reactionGroups.present) { - map['reaction_groups'] = Variable( - $MessagesTable.$converterreactionGroupsn.toSql(reactionGroups.value)); + map['reaction_groups'] = Variable($MessagesTable.$converterreactionGroupsn.toSql(reactionGroups.value)); } if (parentId.present) { map['parent_id'] = Variable(parentId.value); @@ -2131,9 +2131,11 @@ class MessagesCompanion extends UpdateCompanion { if (remoteDeletedAt.present) { map['remote_deleted_at'] = Variable(remoteDeletedAt.value); } + if (deletedForMe.present) { + map['deleted_for_me'] = Variable(deletedForMe.value); + } if (messageTextUpdatedAt.present) { - map['message_text_updated_at'] = - Variable(messageTextUpdatedAt.value); + map['message_text_updated_at'] = Variable(messageTextUpdatedAt.value); } if (userId.present) { map['user_id'] = Variable(userId.value); @@ -2157,17 +2159,15 @@ class MessagesCompanion extends UpdateCompanion { map['channel_cid'] = Variable(channelCid.value); } if (i18n.present) { - map['i18n'] = - Variable($MessagesTable.$converteri18n.toSql(i18n.value)); + map['i18n'] = Variable($MessagesTable.$converteri18n.toSql(i18n.value)); } if (restrictedVisibility.present) { - map['restricted_visibility'] = Variable($MessagesTable - .$converterrestrictedVisibilityn - .toSql(restrictedVisibility.value)); + map['restricted_visibility'] = Variable( + $MessagesTable.$converterrestrictedVisibilityn.toSql(restrictedVisibility.value), + ); } if (extraData.present) { - map['extra_data'] = Variable( - $MessagesTable.$converterextraDatan.toSql(extraData.value)); + map['extra_data'] = Variable($MessagesTable.$converterextraDatan.toSql(extraData.value)); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -2198,6 +2198,7 @@ class MessagesCompanion extends UpdateCompanion { ..write('remoteUpdatedAt: $remoteUpdatedAt, ') ..write('localDeletedAt: $localDeletedAt, ') ..write('remoteDeletedAt: $remoteDeletedAt, ') + ..write('deletedForMe: $deletedForMe, ') ..write('messageTextUpdatedAt: $messageTextUpdatedAt, ') ..write('userId: $userId, ') ..write('channelRole: $channelRole, ') @@ -2215,8 +2216,7 @@ class MessagesCompanion extends UpdateCompanion { } } -class $DraftMessagesTable extends DraftMessages - with TableInfo<$DraftMessagesTable, DraftMessageEntity> { +class $DraftMessagesTable extends DraftMessages with TableInfo<$DraftMessagesTable, DraftMessageEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -2224,132 +2224,157 @@ class $DraftMessagesTable extends DraftMessages 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'); + '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); - static const VerificationMeta _attachmentsMeta = - const VerificationMeta('attachments'); - @override - late final GeneratedColumnWithTypeConverter, String> - attachments = GeneratedColumn('attachments', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>( - $DraftMessagesTable.$converterattachments); + '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>($DraftMessagesTable.$converterattachments); 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')); - static const VerificationMeta _mentionedUsersMeta = - const VerificationMeta('mentionedUsers'); - @override - late final GeneratedColumnWithTypeConverter, String> - mentionedUsers = GeneratedColumn( - 'mentioned_users', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>( - $DraftMessagesTable.$convertermentionedUsers); - static const VerificationMeta _parentIdMeta = - const VerificationMeta('parentId'); + '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>($DraftMessagesTable.$convertermentionedUsers); + static const VerificationMeta _parentIdMeta = const VerificationMeta('parentId'); @override late final GeneratedColumn parentId = GeneratedColumn( - 'parent_id', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES messages (id) ON DELETE CASCADE')); - static const VerificationMeta _quotedMessageIdMeta = - const VerificationMeta('quotedMessageId'); + 'parent_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES messages (id) ON DELETE CASCADE'), + ); + static const VerificationMeta _quotedMessageIdMeta = const VerificationMeta('quotedMessageId'); @override late final GeneratedColumn quotedMessageId = GeneratedColumn( - 'quoted_message_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + '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 _showInChannelMeta = - const VerificationMeta('showInChannel'); + 'poll_id', + aliasedName, + true, + type: DriftSqlType.string, + 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 _commandMeta = - const VerificationMeta('command'); + 'show_in_channel', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("show_in_channel" IN (0, 1))'), + ); + static const VerificationMeta _commandMeta = const VerificationMeta('command'); @override late final GeneratedColumn command = GeneratedColumn( - 'command', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + 'command', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); static const VerificationMeta _silentMeta = const VerificationMeta('silent'); @override late final GeneratedColumn silent = GeneratedColumn( - 'silent', aliasedName, false, - type: DriftSqlType.bool, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("silent" IN (0, 1))'), - defaultValue: const Constant(false)); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); + 'silent', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("silent" IN (0, 1))'), + defaultValue: const Constant(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 _channelCidMeta = - const VerificationMeta('channelCid'); + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + 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 _extraDataMeta = - const VerificationMeta('extraData'); - @override - late final GeneratedColumnWithTypeConverter?, String> - extraData = GeneratedColumn('extra_data', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $DraftMessagesTable.$converterextraDatan); + 'channel_cid', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES channels (cid) ON DELETE CASCADE'), + ); + @override + late final GeneratedColumnWithTypeConverter?, String> extraData = GeneratedColumn( + 'extra_data', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($DraftMessagesTable.$converterextraDatan); @override List get $columns => [ - id, - messageText, - attachments, - type, - mentionedUsers, - parentId, - quotedMessageId, - pollId, - showInChannel, - command, - silent, - createdAt, - channelCid, - extraData - ]; + id, + messageText, + attachments, + type, + mentionedUsers, + parentId, + quotedMessageId, + pollId, + showInChannel, + command, + silent, + createdAt, + channelCid, + extraData, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'draft_messages'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -2358,58 +2383,43 @@ class $DraftMessagesTable extends DraftMessages context.missing(_idMeta); } if (data.containsKey('message_text')) { - context.handle( - _messageTextMeta, - messageText.isAcceptableOrUnknown( - data['message_text']!, _messageTextMeta)); + context.handle(_messageTextMeta, messageText.isAcceptableOrUnknown(data['message_text']!, _messageTextMeta)); } - context.handle(_attachmentsMeta, const VerificationResult.success()); if (data.containsKey('type')) { - context.handle( - _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + context.handle(_typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); } - context.handle(_mentionedUsersMeta, const VerificationResult.success()); if (data.containsKey('parent_id')) { - context.handle(_parentIdMeta, - parentId.isAcceptableOrUnknown(data['parent_id']!, _parentIdMeta)); + 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)); + _quotedMessageIdMeta, + quotedMessageId.isAcceptableOrUnknown(data['quoted_message_id']!, _quotedMessageIdMeta), + ); } if (data.containsKey('poll_id')) { - context.handle(_pollIdMeta, - pollId.isAcceptableOrUnknown(data['poll_id']!, _pollIdMeta)); + context.handle(_pollIdMeta, pollId.isAcceptableOrUnknown(data['poll_id']!, _pollIdMeta)); } if (data.containsKey('show_in_channel')) { context.handle( - _showInChannelMeta, - showInChannel.isAcceptableOrUnknown( - data['show_in_channel']!, _showInChannelMeta)); + _showInChannelMeta, + showInChannel.isAcceptableOrUnknown(data['show_in_channel']!, _showInChannelMeta), + ); } if (data.containsKey('command')) { - context.handle(_commandMeta, - command.isAcceptableOrUnknown(data['command']!, _commandMeta)); + context.handle(_commandMeta, command.isAcceptableOrUnknown(data['command']!, _commandMeta)); } if (data.containsKey('silent')) { - context.handle(_silentMeta, - silent.isAcceptableOrUnknown(data['silent']!, _silentMeta)); + context.handle(_silentMeta, silent.isAcceptableOrUnknown(data['silent']!, _silentMeta)); } if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + context.handle(_createdAtMeta, createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); } if (data.containsKey('channel_cid')) { - context.handle( - _channelCidMeta, - channelCid.isAcceptableOrUnknown( - data['channel_cid']!, _channelCidMeta)); + context.handle(_channelCidMeta, channelCid.isAcceptableOrUnknown(data['channel_cid']!, _channelCidMeta)); } else if (isInserting) { context.missing(_channelCidMeta); } - context.handle(_extraDataMeta, const VerificationResult.success()); return context; } @@ -2419,37 +2429,29 @@ class $DraftMessagesTable extends DraftMessages DraftMessageEntity map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return DraftMessageEntity( - id: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}id'])!, - messageText: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}message_text']), + id: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}id'])!, + messageText: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}message_text']), attachments: $DraftMessagesTable.$converterattachments.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}attachments'])!), - type: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}type'])!, + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}attachments'])!, + ), + type: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}type'])!, mentionedUsers: $DraftMessagesTable.$convertermentionedUsers.fromSql( - attachedDatabase.typeMapping.read( - DriftSqlType.string, data['${effectivePrefix}mentioned_users'])!), - parentId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}parent_id']), + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}mentioned_users'])!, + ), + 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']), - showInChannel: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}show_in_channel']), - command: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}command']), - silent: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}silent'])!, - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - channelCid: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, + DriftSqlType.string, + data['${effectivePrefix}quoted_message_id'], + ), + pollId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}poll_id']), + showInChannel: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}show_in_channel']), + command: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}command']), + silent: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}silent'])!, + createdAt: attachedDatabase.typeMapping.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + channelCid: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, extraData: $DraftMessagesTable.$converterextraDatan.fromSql( - attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}extra_data']), + ), ); } @@ -2458,18 +2460,15 @@ class $DraftMessagesTable extends DraftMessages return $DraftMessagesTable(attachedDatabase, alias); } - static TypeConverter, String> $converterattachments = - ListConverter(); - static TypeConverter, String> $convertermentionedUsers = - ListConverter(); - static TypeConverter, String> $converterextraData = - MapConverter(); - static TypeConverter?, String?> $converterextraDatan = - NullAwareTypeConverter.wrap($converterextraData); + static TypeConverter, String> $converterattachments = ListConverter(); + static TypeConverter, String> $convertermentionedUsers = ListConverter(); + static TypeConverter, String> $converterextraData = MapConverter(); + static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap( + $converterextraData, + ); } -class DraftMessageEntity extends DataClass - implements Insertable { +class DraftMessageEntity extends DataClass implements Insertable { /// The message id final String id; @@ -2512,21 +2511,22 @@ class DraftMessageEntity extends DataClass /// Message custom extraData final Map? extraData; - const DraftMessageEntity( - {required this.id, - this.messageText, - required this.attachments, - required this.type, - required this.mentionedUsers, - this.parentId, - this.quotedMessageId, - this.pollId, - this.showInChannel, - this.command, - required this.silent, - required this.createdAt, - required this.channelCid, - this.extraData}); + const DraftMessageEntity({ + required this.id, + this.messageText, + required this.attachments, + required this.type, + required this.mentionedUsers, + this.parentId, + this.quotedMessageId, + this.pollId, + this.showInChannel, + this.command, + required this.silent, + required this.createdAt, + required this.channelCid, + this.extraData, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -2535,13 +2535,11 @@ class DraftMessageEntity extends DataClass map['message_text'] = Variable(messageText); } { - map['attachments'] = Variable( - $DraftMessagesTable.$converterattachments.toSql(attachments)); + map['attachments'] = Variable($DraftMessagesTable.$converterattachments.toSql(attachments)); } map['type'] = Variable(type); { - map['mentioned_users'] = Variable( - $DraftMessagesTable.$convertermentionedUsers.toSql(mentionedUsers)); + map['mentioned_users'] = Variable($DraftMessagesTable.$convertermentionedUsers.toSql(mentionedUsers)); } if (!nullToAbsent || parentId != null) { map['parent_id'] = Variable(parentId); @@ -2562,14 +2560,12 @@ class DraftMessageEntity extends DataClass map['created_at'] = Variable(createdAt); map['channel_cid'] = Variable(channelCid); if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $DraftMessagesTable.$converterextraDatan.toSql(extraData)); + map['extra_data'] = Variable($DraftMessagesTable.$converterextraDatan.toSql(extraData)); } return map; } - factory DraftMessageEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + factory DraftMessageEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return DraftMessageEntity( id: serializer.fromJson(json['id']), @@ -2609,64 +2605,52 @@ class DraftMessageEntity extends DataClass }; } - DraftMessageEntity copyWith( - {String? id, - Value messageText = const Value.absent(), - List? attachments, - String? type, - 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(), - bool? silent, - DateTime? createdAt, - String? channelCid, - Value?> extraData = const Value.absent()}) => - DraftMessageEntity( - id: id ?? this.id, - messageText: messageText.present ? messageText.value : this.messageText, - attachments: attachments ?? this.attachments, - type: type ?? this.type, - mentionedUsers: mentionedUsers ?? this.mentionedUsers, - parentId: parentId.present ? parentId.value : this.parentId, - quotedMessageId: quotedMessageId.present - ? quotedMessageId.value - : this.quotedMessageId, - pollId: pollId.present ? pollId.value : this.pollId, - showInChannel: - showInChannel.present ? showInChannel.value : this.showInChannel, - command: command.present ? command.value : this.command, - silent: silent ?? this.silent, - createdAt: createdAt ?? this.createdAt, - channelCid: channelCid ?? this.channelCid, - extraData: extraData.present ? extraData.value : this.extraData, - ); + DraftMessageEntity copyWith({ + String? id, + Value messageText = const Value.absent(), + List? attachments, + String? type, + 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(), + bool? silent, + DateTime? createdAt, + String? channelCid, + Value?> extraData = const Value.absent(), + }) => DraftMessageEntity( + id: id ?? this.id, + messageText: messageText.present ? messageText.value : this.messageText, + attachments: attachments ?? this.attachments, + type: type ?? this.type, + mentionedUsers: mentionedUsers ?? this.mentionedUsers, + parentId: parentId.present ? parentId.value : this.parentId, + quotedMessageId: quotedMessageId.present ? quotedMessageId.value : this.quotedMessageId, + pollId: pollId.present ? pollId.value : this.pollId, + showInChannel: showInChannel.present ? showInChannel.value : this.showInChannel, + command: command.present ? command.value : this.command, + silent: silent ?? this.silent, + createdAt: createdAt ?? this.createdAt, + channelCid: channelCid ?? this.channelCid, + extraData: extraData.present ? extraData.value : this.extraData, + ); DraftMessageEntity copyWithCompanion(DraftMessagesCompanion data) { return DraftMessageEntity( 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, + messageText: data.messageText.present ? data.messageText.value : this.messageText, + attachments: data.attachments.present ? data.attachments.value : this.attachments, type: data.type.present ? data.type.value : this.type, - mentionedUsers: data.mentionedUsers.present - ? data.mentionedUsers.value - : this.mentionedUsers, + mentionedUsers: data.mentionedUsers.present ? data.mentionedUsers.value : this.mentionedUsers, parentId: data.parentId.present ? data.parentId.value : this.parentId, - quotedMessageId: data.quotedMessageId.present - ? data.quotedMessageId.value - : this.quotedMessageId, + quotedMessageId: data.quotedMessageId.present ? data.quotedMessageId.value : this.quotedMessageId, pollId: data.pollId.present ? data.pollId.value : this.pollId, - showInChannel: data.showInChannel.present - ? data.showInChannel.value - : this.showInChannel, + showInChannel: data.showInChannel.present ? data.showInChannel.value : this.showInChannel, command: data.command.present ? data.command.value : this.command, silent: data.silent.present ? data.silent.value : this.silent, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, - channelCid: - data.channelCid.present ? data.channelCid.value : this.channelCid, + channelCid: data.channelCid.present ? data.channelCid.value : this.channelCid, extraData: data.extraData.present ? data.extraData.value : this.extraData, ); } @@ -2694,20 +2678,21 @@ class DraftMessageEntity extends DataClass @override int get hashCode => Object.hash( - id, - messageText, - attachments, - type, - mentionedUsers, - parentId, - quotedMessageId, - pollId, - showInChannel, - command, - silent, - createdAt, - channelCid, - extraData); + id, + messageText, + attachments, + type, + mentionedUsers, + parentId, + quotedMessageId, + pollId, + showInChannel, + command, + silent, + createdAt, + channelCid, + extraData, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -2777,10 +2762,10 @@ class DraftMessagesCompanion extends UpdateCompanion { required String channelCid, this.extraData = const Value.absent(), this.rowid = const Value.absent(), - }) : id = Value(id), - attachments = Value(attachments), - mentionedUsers = Value(mentionedUsers), - channelCid = Value(channelCid); + }) : id = Value(id), + attachments = Value(attachments), + mentionedUsers = Value(mentionedUsers), + channelCid = Value(channelCid); static Insertable custom({ Expression? id, Expression? messageText, @@ -2817,22 +2802,23 @@ class DraftMessagesCompanion extends UpdateCompanion { }); } - DraftMessagesCompanion copyWith( - {Value? id, - Value? messageText, - Value>? attachments, - Value? type, - Value>? mentionedUsers, - Value? parentId, - Value? quotedMessageId, - Value? pollId, - Value? showInChannel, - Value? command, - Value? silent, - Value? createdAt, - Value? channelCid, - Value?>? extraData, - Value? rowid}) { + DraftMessagesCompanion copyWith({ + Value? id, + Value? messageText, + Value>? attachments, + Value? type, + Value>? mentionedUsers, + Value? parentId, + Value? quotedMessageId, + Value? pollId, + Value? showInChannel, + Value? command, + Value? silent, + Value? createdAt, + Value? channelCid, + Value?>? extraData, + Value? rowid, + }) { return DraftMessagesCompanion( id: id ?? this.id, messageText: messageText ?? this.messageText, @@ -2862,16 +2848,15 @@ class DraftMessagesCompanion extends UpdateCompanion { map['message_text'] = Variable(messageText.value); } if (attachments.present) { - map['attachments'] = Variable( - $DraftMessagesTable.$converterattachments.toSql(attachments.value)); + map['attachments'] = Variable($DraftMessagesTable.$converterattachments.toSql(attachments.value)); } if (type.present) { map['type'] = Variable(type.value); } if (mentionedUsers.present) { - map['mentioned_users'] = Variable($DraftMessagesTable - .$convertermentionedUsers - .toSql(mentionedUsers.value)); + map['mentioned_users'] = Variable( + $DraftMessagesTable.$convertermentionedUsers.toSql(mentionedUsers.value), + ); } if (parentId.present) { map['parent_id'] = Variable(parentId.value); @@ -2898,8 +2883,7 @@ class DraftMessagesCompanion extends UpdateCompanion { map['channel_cid'] = Variable(channelCid.value); } if (extraData.present) { - map['extra_data'] = Variable( - $DraftMessagesTable.$converterextraDatan.toSql(extraData.value)); + map['extra_data'] = Variable($DraftMessagesTable.$converterextraDatan.toSql(extraData.value)); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -2930,557 +2914,1087 @@ 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); - static const VerificationMeta _attachmentsMeta = - const VerificationMeta('attachments'); - @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')); - static const VerificationMeta _mentionedUsersMeta = - const VerificationMeta('mentionedUsers'); - @override - late final GeneratedColumnWithTypeConverter, String> - mentionedUsers = GeneratedColumn( - 'mentioned_users', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>( - $PinnedMessagesTable.$convertermentionedUsers); - static const VerificationMeta _reactionGroupsMeta = - const VerificationMeta('reactionGroups'); - @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'); + $LocationsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _channelCidMeta = const VerificationMeta('channelCid'); @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'); + late final GeneratedColumn channelCid = GeneratedColumn( + 'channel_cid', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES channels (cid) ON DELETE CASCADE'), + ); + static const VerificationMeta _messageIdMeta = const VerificationMeta('messageId'); @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); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', + aliasedName, + true, + type: DriftSqlType.string, + 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 _channelRoleMeta = - const VerificationMeta('channelRole'); + 'user_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + 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 endAt = GeneratedColumn( + 'end_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta('createdAt'); @override - late final GeneratedColumn channelRole = GeneratedColumn( - 'channel_role', 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'); + 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 channelCid = GeneratedColumn( - 'channel_cid', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _i18nMeta = const VerificationMeta('i18n'); - @override - late final GeneratedColumnWithTypeConverter?, String> - i18n = GeneratedColumn('i18n', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $PinnedMessagesTable.$converteri18n); - static const VerificationMeta _restrictedVisibilityMeta = - const VerificationMeta('restrictedVisibility'); - @override - late final GeneratedColumnWithTypeConverter?, String> - restrictedVisibility = GeneratedColumn( - 'restricted_visibility', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $PinnedMessagesTable.$converterrestrictedVisibilityn); - static const VerificationMeta _extraDataMeta = - const VerificationMeta('extraData'); - @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, - channelRole, - pinned, - pinnedAt, - pinExpires, - pinnedByUserId, - channelCid, - i18n, - restrictedVisibility, - extraData - ]; + channelCid, + 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, - {bool isInserting = false}) { + 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)); - } - context.handle(_attachmentsMeta, const VerificationResult.success()); - 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)); - } - context.handle(_mentionedUsersMeta, const VerificationResult.success()); - context.handle(_reactionGroupsMeta, const VerificationResult.success()); - 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('channel_cid')) { + context.handle(_channelCidMeta, channelCid.isAcceptableOrUnknown(data['channel_cid']!, _channelCidMeta)); } - 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)); + context.handle(_userIdMeta, userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); } - if (data.containsKey('channel_role')) { - context.handle( - _channelRoleMeta, - channelRole.isAcceptableOrUnknown( - data['channel_role']!, _channelRoleMeta)); - } - 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')) { - context.handle( - _pinnedByUserIdMeta, - pinnedByUserId.isAcceptableOrUnknown( - data['pinned_by_user_id']!, _pinnedByUserIdMeta)); + if (data.containsKey('end_at')) { + context.handle(_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)); } - context.handle(_i18nMeta, const VerificationResult.success()); - context.handle( - _restrictedVisibilityMeta, const VerificationResult.success()); - context.handle(_extraDataMeta, const VerificationResult.success()); 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']), - userId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}user_id']), - channelRole: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}channel_role']), - 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'])), + 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']), + 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; - - /// The current state of the message. - final String state; +class LocationEntity extends DataClass implements Insertable { + /// The channel CID where the location is shared + final String? channelCid; - /// The message type - final String type; + /// The ID of the message that contains this shared location + final String? messageId; - /// The list of user mentioned in the message - final List mentionedUsers; + /// The ID of the user who shared the location + final String? userId; - /// A map describing the reaction group for every reaction - final Map? reactionGroups; + /// The latitude of the shared location + final double latitude; - /// The ID of the parent message, if the message is a thread reply. - final String? parentId; + /// The longitude of the shared location + final double longitude; - /// The ID of the quoted message, if the message is a quoted reply. - final String? quotedMessageId; + /// The ID of the device that created the location + final String? createdByDeviceId; - /// The ID of the poll, if the message is a poll. - final String? pollId; + /// The date at which the shared location will end (for live locations) + /// If null, this is a static location + final DateTime? endAt; - /// Number of replies for this message. - final int? replyCount; + /// The date at which the location was created + final DateTime createdAt; - /// Check if this message needs to show in the channel. - final bool? showInChannel; + /// 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; + } - /// If true the message is shadowed - final bool shadowed; + 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), + }; + } - /// A used command name. - final String? command; + 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, + ); + } + + @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(); + } + + @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); +} + +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, + }); + } + + 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, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (channelCid.present) { + map['channel_cid'] = Variable(channelCid.value); + } + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (latitude.present) { + map['latitude'] = Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = Variable(longitude.value); + } + if (createdByDeviceId.present) { + map['created_by_device_id'] = Variable(createdByDeviceId.value); + } + if (endAt.present) { + map['end_at'] = Variable(endAt.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); + } + return map; + } + + @override + 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 + 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'); + @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 _deletedForMeMeta = const VerificationMeta('deletedForMe'); + @override + late final GeneratedColumn deletedForMe = GeneratedColumn( + 'deleted_for_me', + aliasedName, + true, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("deleted_for_me" IN (0, 1))'), + ); + 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 _channelRoleMeta = const VerificationMeta('channelRole'); + @override + late final GeneratedColumn channelRole = GeneratedColumn( + 'channel_role', + 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, + state, + type, + mentionedUsers, + reactionGroups, + parentId, + quotedMessageId, + pollId, + replyCount, + showInChannel, + shadowed, + command, + localCreatedAt, + remoteCreatedAt, + localUpdatedAt, + remoteUpdatedAt, + localDeletedAt, + remoteDeletedAt, + deletedForMe, + messageTextUpdatedAt, + userId, + channelRole, + pinned, + pinnedAt, + pinExpires, + pinnedByUserId, + channelCid, + i18n, + restrictedVisibility, + extraData, + ]; + @override + 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('deleted_for_me')) { + context.handle(_deletedForMeMeta, deletedForMe.isAcceptableOrUnknown(data['deleted_for_me']!, _deletedForMeMeta)); + } + 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('channel_role')) { + context.handle(_channelRoleMeta, channelRole.isAcceptableOrUnknown(data['channel_role']!, _channelRoleMeta)); + } + 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'], + ), + deletedForMe: attachedDatabase.typeMapping.read(DriftSqlType.bool, data['${effectivePrefix}deleted_for_me']), + messageTextUpdatedAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}message_text_updated_at'], + ), + userId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}user_id']), + channelRole: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}channel_role']), + 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; @@ -3500,6 +4014,9 @@ class PinnedMessageEntity extends DataClass /// The DateTime on which the message was deleted on the server. final DateTime? remoteDeletedAt; + /// Whether the message was deleted only for the current user. + final bool? deletedForMe; + /// The DateTime at which the message text was edited final DateTime? messageTextUpdatedAt; @@ -3532,38 +4049,40 @@ class PinnedMessageEntity extends DataClass /// 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, - this.channelRole, - required this.pinned, - this.pinnedAt, - this.pinExpires, - this.pinnedByUserId, - required this.channelCid, - this.i18n, - this.restrictedVisibility, - this.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.deletedForMe, + this.messageTextUpdatedAt, + this.userId, + this.channelRole, + 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 = {}; @@ -3572,18 +4091,15 @@ class PinnedMessageEntity extends DataClass map['message_text'] = Variable(messageText); } { - map['attachments'] = Variable( - $PinnedMessagesTable.$converterattachments.toSql(attachments)); + map['attachments'] = Variable($PinnedMessagesTable.$converterattachments.toSql(attachments)); } map['state'] = Variable(state); map['type'] = Variable(type); { - map['mentioned_users'] = Variable( - $PinnedMessagesTable.$convertermentionedUsers.toSql(mentionedUsers)); + map['mentioned_users'] = Variable($PinnedMessagesTable.$convertermentionedUsers.toSql(mentionedUsers)); } if (!nullToAbsent || reactionGroups != null) { - map['reaction_groups'] = Variable( - $PinnedMessagesTable.$converterreactionGroupsn.toSql(reactionGroups)); + map['reaction_groups'] = Variable($PinnedMessagesTable.$converterreactionGroupsn.toSql(reactionGroups)); } if (!nullToAbsent || parentId != null) { map['parent_id'] = Variable(parentId); @@ -3622,6 +4138,9 @@ class PinnedMessageEntity extends DataClass if (!nullToAbsent || remoteDeletedAt != null) { map['remote_deleted_at'] = Variable(remoteDeletedAt); } + if (!nullToAbsent || deletedForMe != null) { + map['deleted_for_me'] = Variable(deletedForMe); + } if (!nullToAbsent || messageTextUpdatedAt != null) { map['message_text_updated_at'] = Variable(messageTextUpdatedAt); } @@ -3643,23 +4162,20 @@ class PinnedMessageEntity extends DataClass } map['channel_cid'] = Variable(channelCid); if (!nullToAbsent || i18n != null) { - map['i18n'] = - Variable($PinnedMessagesTable.$converteri18n.toSql(i18n)); + map['i18n'] = Variable($PinnedMessagesTable.$converteri18n.toSql(i18n)); } if (!nullToAbsent || restrictedVisibility != null) { - map['restricted_visibility'] = Variable($PinnedMessagesTable - .$converterrestrictedVisibilityn - .toSql(restrictedVisibility)); + map['restricted_visibility'] = Variable( + $PinnedMessagesTable.$converterrestrictedVisibilityn.toSql(restrictedVisibility), + ); } if (!nullToAbsent || extraData != null) { - map['extra_data'] = Variable( - $PinnedMessagesTable.$converterextraDatan.toSql(extraData)); + map['extra_data'] = Variable($PinnedMessagesTable.$converterextraDatan.toSql(extraData)); } return map; } - factory PinnedMessageEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + factory PinnedMessageEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return PinnedMessageEntity( id: serializer.fromJson(json['id']), @@ -3668,8 +4184,7 @@ class PinnedMessageEntity extends DataClass state: serializer.fromJson(json['state']), type: serializer.fromJson(json['type']), mentionedUsers: serializer.fromJson>(json['mentionedUsers']), - reactionGroups: serializer - .fromJson?>(json['reactionGroups']), + reactionGroups: serializer.fromJson?>(json['reactionGroups']), parentId: serializer.fromJson(json['parentId']), quotedMessageId: serializer.fromJson(json['quotedMessageId']), pollId: serializer.fromJson(json['pollId']), @@ -3683,8 +4198,8 @@ class PinnedMessageEntity extends DataClass remoteUpdatedAt: serializer.fromJson(json['remoteUpdatedAt']), localDeletedAt: serializer.fromJson(json['localDeletedAt']), remoteDeletedAt: serializer.fromJson(json['remoteDeletedAt']), - messageTextUpdatedAt: - serializer.fromJson(json['messageTextUpdatedAt']), + deletedForMe: serializer.fromJson(json['deletedForMe']), + messageTextUpdatedAt: serializer.fromJson(json['messageTextUpdatedAt']), userId: serializer.fromJson(json['userId']), channelRole: serializer.fromJson(json['channelRole']), pinned: serializer.fromJson(json['pinned']), @@ -3693,8 +4208,7 @@ class PinnedMessageEntity extends DataClass pinnedByUserId: serializer.fromJson(json['pinnedByUserId']), channelCid: serializer.fromJson(json['channelCid']), i18n: serializer.fromJson?>(json['i18n']), - restrictedVisibility: - serializer.fromJson?>(json['restrictedVisibility']), + restrictedVisibility: serializer.fromJson?>(json['restrictedVisibility']), extraData: serializer.fromJson?>(json['extraData']), ); } @@ -3708,8 +4222,7 @@ class PinnedMessageEntity extends DataClass 'state': serializer.toJson(state), 'type': serializer.toJson(type), 'mentionedUsers': serializer.toJson>(mentionedUsers), - 'reactionGroups': - serializer.toJson?>(reactionGroups), + 'reactionGroups': serializer.toJson?>(reactionGroups), 'parentId': serializer.toJson(parentId), 'quotedMessageId': serializer.toJson(quotedMessageId), 'pollId': serializer.toJson(pollId), @@ -3723,8 +4236,8 @@ class PinnedMessageEntity extends DataClass 'remoteUpdatedAt': serializer.toJson(remoteUpdatedAt), 'localDeletedAt': serializer.toJson(localDeletedAt), 'remoteDeletedAt': serializer.toJson(remoteDeletedAt), - 'messageTextUpdatedAt': - serializer.toJson(messageTextUpdatedAt), + 'deletedForMe': serializer.toJson(deletedForMe), + 'messageTextUpdatedAt': serializer.toJson(messageTextUpdatedAt), 'userId': serializer.toJson(userId), 'channelRole': serializer.toJson(channelRole), 'pinned': serializer.toJson(pinned), @@ -3733,156 +4246,111 @@ class PinnedMessageEntity extends DataClass 'pinnedByUserId': serializer.toJson(pinnedByUserId), 'channelCid': serializer.toJson(channelCid), 'i18n': serializer.toJson?>(i18n), - 'restrictedVisibility': - serializer.toJson?>(restrictedVisibility), + '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(), - Value channelRole = 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, - channelRole: channelRole.present ? channelRole.value : this.channelRole, - 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 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 deletedForMe = const Value.absent(), + Value messageTextUpdatedAt = const Value.absent(), + Value userId = const Value.absent(), + Value channelRole = 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, + deletedForMe: deletedForMe.present ? deletedForMe.value : this.deletedForMe, + messageTextUpdatedAt: messageTextUpdatedAt.present ? messageTextUpdatedAt.value : this.messageTextUpdatedAt, + userId: userId.present ? userId.value : this.userId, + channelRole: channelRole.present ? channelRole.value : this.channelRole, + 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, + 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, + 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, + 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, + 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, + 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, + deletedForMe: data.deletedForMe.present ? data.deletedForMe.value : this.deletedForMe, messageTextUpdatedAt: data.messageTextUpdatedAt.present ? data.messageTextUpdatedAt.value : this.messageTextUpdatedAt, userId: data.userId.present ? data.userId.value : this.userId, - channelRole: - data.channelRole.present ? data.channelRole.value : this.channelRole, + channelRole: data.channelRole.present ? data.channelRole.value : this.channelRole, 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, + 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 @@ -3914,6 +4382,7 @@ class PinnedMessageEntity extends DataClass ..write('remoteUpdatedAt: $remoteUpdatedAt, ') ..write('localDeletedAt: $localDeletedAt, ') ..write('remoteDeletedAt: $remoteDeletedAt, ') + ..write('deletedForMe: $deletedForMe, ') ..write('messageTextUpdatedAt: $messageTextUpdatedAt, ') ..write('userId: $userId, ') ..write('channelRole: $channelRole, ') @@ -3931,38 +4400,39 @@ class PinnedMessageEntity extends DataClass @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, - channelRole, - pinned, - pinnedAt, - pinExpires, - pinnedByUserId, - channelCid, - i18n, - restrictedVisibility, - extraData - ]); + id, + messageText, + attachments, + state, + type, + mentionedUsers, + reactionGroups, + parentId, + quotedMessageId, + pollId, + replyCount, + showInChannel, + shadowed, + command, + localCreatedAt, + remoteCreatedAt, + localUpdatedAt, + remoteUpdatedAt, + localDeletedAt, + remoteDeletedAt, + deletedForMe, + messageTextUpdatedAt, + userId, + channelRole, + pinned, + pinnedAt, + pinExpires, + pinnedByUserId, + channelCid, + i18n, + restrictedVisibility, + extraData, + ]); @override bool operator ==(Object other) => identical(this, other) || @@ -3987,6 +4457,7 @@ class PinnedMessageEntity extends DataClass other.remoteUpdatedAt == this.remoteUpdatedAt && other.localDeletedAt == this.localDeletedAt && other.remoteDeletedAt == this.remoteDeletedAt && + other.deletedForMe == this.deletedForMe && other.messageTextUpdatedAt == this.messageTextUpdatedAt && other.userId == this.userId && other.channelRole == this.channelRole && @@ -4021,6 +4492,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { final Value remoteUpdatedAt; final Value localDeletedAt; final Value remoteDeletedAt; + final Value deletedForMe; final Value messageTextUpdatedAt; final Value userId; final Value channelRole; @@ -4054,6 +4526,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.remoteUpdatedAt = const Value.absent(), this.localDeletedAt = const Value.absent(), this.remoteDeletedAt = const Value.absent(), + this.deletedForMe = const Value.absent(), this.messageTextUpdatedAt = const Value.absent(), this.userId = const Value.absent(), this.channelRole = const Value.absent(), @@ -4088,6 +4561,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.remoteUpdatedAt = const Value.absent(), this.localDeletedAt = const Value.absent(), this.remoteDeletedAt = const Value.absent(), + this.deletedForMe = const Value.absent(), this.messageTextUpdatedAt = const Value.absent(), this.userId = const Value.absent(), this.channelRole = const Value.absent(), @@ -4100,11 +4574,11 @@ class PinnedMessagesCompanion extends UpdateCompanion { 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); + }) : id = Value(id), + attachments = Value(attachments), + state = Value(state), + mentionedUsers = Value(mentionedUsers), + channelCid = Value(channelCid); static Insertable custom({ Expression? id, Expression? messageText, @@ -4126,6 +4600,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { Expression? remoteUpdatedAt, Expression? localDeletedAt, Expression? remoteDeletedAt, + Expression? deletedForMe, Expression? messageTextUpdatedAt, Expression? userId, Expression? channelRole, @@ -4160,8 +4635,8 @@ class PinnedMessagesCompanion extends UpdateCompanion { 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 (deletedForMe != null) 'deleted_for_me': deletedForMe, + if (messageTextUpdatedAt != null) 'message_text_updated_at': messageTextUpdatedAt, if (userId != null) 'user_id': userId, if (channelRole != null) 'channel_role': channelRole, if (pinned != null) 'pinned': pinned, @@ -4170,46 +4645,47 @@ class PinnedMessagesCompanion extends UpdateCompanion { 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 (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? channelRole, - Value? pinned, - Value? pinnedAt, - Value? pinExpires, - Value? pinnedByUserId, - Value? channelCid, - Value?>? i18n, - Value?>? restrictedVisibility, - Value?>? extraData, - Value? 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? deletedForMe, + Value? messageTextUpdatedAt, + Value? userId, + Value? channelRole, + 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, @@ -4231,6 +4707,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { remoteUpdatedAt: remoteUpdatedAt ?? this.remoteUpdatedAt, localDeletedAt: localDeletedAt ?? this.localDeletedAt, remoteDeletedAt: remoteDeletedAt ?? this.remoteDeletedAt, + deletedForMe: deletedForMe ?? this.deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt ?? this.messageTextUpdatedAt, userId: userId ?? this.userId, channelRole: channelRole ?? this.channelRole, @@ -4256,8 +4733,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { map['message_text'] = Variable(messageText.value); } if (attachments.present) { - map['attachments'] = Variable( - $PinnedMessagesTable.$converterattachments.toSql(attachments.value)); + map['attachments'] = Variable($PinnedMessagesTable.$converterattachments.toSql(attachments.value)); } if (state.present) { map['state'] = Variable(state.value); @@ -4266,14 +4742,14 @@ class PinnedMessagesCompanion extends UpdateCompanion { map['type'] = Variable(type.value); } if (mentionedUsers.present) { - map['mentioned_users'] = Variable($PinnedMessagesTable - .$convertermentionedUsers - .toSql(mentionedUsers.value)); + map['mentioned_users'] = Variable( + $PinnedMessagesTable.$convertermentionedUsers.toSql(mentionedUsers.value), + ); } if (reactionGroups.present) { - map['reaction_groups'] = Variable($PinnedMessagesTable - .$converterreactionGroupsn - .toSql(reactionGroups.value)); + map['reaction_groups'] = Variable( + $PinnedMessagesTable.$converterreactionGroupsn.toSql(reactionGroups.value), + ); } if (parentId.present) { map['parent_id'] = Variable(parentId.value); @@ -4314,9 +4790,11 @@ class PinnedMessagesCompanion extends UpdateCompanion { if (remoteDeletedAt.present) { map['remote_deleted_at'] = Variable(remoteDeletedAt.value); } + if (deletedForMe.present) { + map['deleted_for_me'] = Variable(deletedForMe.value); + } if (messageTextUpdatedAt.present) { - map['message_text_updated_at'] = - Variable(messageTextUpdatedAt.value); + map['message_text_updated_at'] = Variable(messageTextUpdatedAt.value); } if (userId.present) { map['user_id'] = Variable(userId.value); @@ -4340,17 +4818,15 @@ class PinnedMessagesCompanion extends UpdateCompanion { map['channel_cid'] = Variable(channelCid.value); } if (i18n.present) { - map['i18n'] = Variable( - $PinnedMessagesTable.$converteri18n.toSql(i18n.value)); + map['i18n'] = Variable($PinnedMessagesTable.$converteri18n.toSql(i18n.value)); } if (restrictedVisibility.present) { - map['restricted_visibility'] = Variable($PinnedMessagesTable - .$converterrestrictedVisibilityn - .toSql(restrictedVisibility.value)); + map['restricted_visibility'] = Variable( + $PinnedMessagesTable.$converterrestrictedVisibilityn.toSql(restrictedVisibility.value), + ); } if (extraData.present) { - map['extra_data'] = Variable( - $PinnedMessagesTable.$converterextraDatan.toSql(extraData.value)); + map['extra_data'] = Variable($PinnedMessagesTable.$converterextraDatan.toSql(extraData.value)); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -4381,6 +4857,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { ..write('remoteUpdatedAt: $remoteUpdatedAt, ') ..write('localDeletedAt: $localDeletedAt, ') ..write('remoteDeletedAt: $remoteDeletedAt, ') + ..write('deletedForMe: $deletedForMe, ') ..write('messageTextUpdatedAt: $messageTextUpdatedAt, ') ..write('userId: $userId, ') ..write('channelRole: $channelRole, ') @@ -4406,166 +4883,192 @@ class $PollsTable extends Polls with TableInfo<$PollsTable, PollEntity> { static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + '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'); + '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); - static const VerificationMeta _optionsMeta = - const VerificationMeta('options'); - @override - late final GeneratedColumnWithTypeConverter, String> options = - GeneratedColumn('options', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>($PollsTable.$converteroptions); - static const VerificationMeta _votingVisibilityMeta = - const VerificationMeta('votingVisibility'); - @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'); + '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'); + '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'); + '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'); + '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'); + '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)); - static const VerificationMeta _voteCountsByOptionMeta = - const VerificationMeta('voteCountsByOption'); - @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'); + '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'); + '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'); + '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'); + '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 _extraDataMeta = - const VerificationMeta('extraData'); - @override - late final GeneratedColumnWithTypeConverter?, String> - extraData = GeneratedColumn('extra_data', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $PollsTable.$converterextraDatan); + '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 - ]; + 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}) { + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -4574,74 +5077,55 @@ class $PollsTable extends Polls with TableInfo<$PollsTable, PollEntity> { context.missing(_idMeta); } if (data.containsKey('name')) { - context.handle( - _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + 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)); + context.handle(_descriptionMeta, description.isAcceptableOrUnknown(data['description']!, _descriptionMeta)); } - context.handle(_optionsMeta, const VerificationResult.success()); - context.handle(_votingVisibilityMeta, const VerificationResult.success()); if (data.containsKey('enforce_unique_vote')) { context.handle( - _enforceUniqueVoteMeta, - enforceUniqueVote.isAcceptableOrUnknown( - data['enforce_unique_vote']!, _enforceUniqueVoteMeta)); + _enforceUniqueVoteMeta, + enforceUniqueVote.isAcceptableOrUnknown(data['enforce_unique_vote']!, _enforceUniqueVoteMeta), + ); } if (data.containsKey('max_votes_allowed')) { context.handle( - _maxVotesAllowedMeta, - maxVotesAllowed.isAcceptableOrUnknown( - data['max_votes_allowed']!, _maxVotesAllowedMeta)); + _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, - allowUserSuggestedOptions.isAcceptableOrUnknown( - data['allow_user_suggested_options']!, - _allowUserSuggestedOptionsMeta)); + ), + ); } if (data.containsKey('allow_answers')) { - context.handle( - _allowAnswersMeta, - allowAnswers.isAcceptableOrUnknown( - data['allow_answers']!, _allowAnswersMeta)); + context.handle(_allowAnswersMeta, allowAnswers.isAcceptableOrUnknown(data['allow_answers']!, _allowAnswersMeta)); } if (data.containsKey('is_closed')) { - context.handle(_isClosedMeta, - isClosed.isAcceptableOrUnknown(data['is_closed']!, _isClosedMeta)); + context.handle(_isClosedMeta, isClosed.isAcceptableOrUnknown(data['is_closed']!, _isClosedMeta)); } if (data.containsKey('answers_count')) { - context.handle( - _answersCountMeta, - answersCount.isAcceptableOrUnknown( - data['answers_count']!, _answersCountMeta)); + context.handle(_answersCountMeta, answersCount.isAcceptableOrUnknown(data['answers_count']!, _answersCountMeta)); } - context.handle(_voteCountsByOptionMeta, const VerificationResult.success()); if (data.containsKey('vote_count')) { - context.handle(_voteCountMeta, - voteCount.isAcceptableOrUnknown(data['vote_count']!, _voteCountMeta)); + 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)); + context.handle(_createdByIdMeta, createdById.isAcceptableOrUnknown(data['created_by_id']!, _createdByIdMeta)); } if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + 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(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); } - context.handle(_extraDataMeta, const VerificationResult.success()); return context; } @@ -4651,45 +5135,37 @@ class $PollsTable extends Polls with TableInfo<$PollsTable, PollEntity> { 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'])!), + 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'])!), + 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']), + 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'])!, + 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'])), + 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']), + ), ); } @@ -4698,16 +5174,13 @@ class $PollsTable extends Polls with TableInfo<$PollsTable, PollEntity> { 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); + 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 { @@ -4769,24 +5242,25 @@ class PollEntity extends DataClass implements Insertable { /// 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}); + 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 = {}; @@ -4796,25 +5270,23 @@ class PollEntity extends DataClass implements Insertable { map['description'] = Variable(description); } { - map['options'] = - Variable($PollsTable.$converteroptions.toSql(options)); + map['options'] = Variable($PollsTable.$converteroptions.toSql(options)); } { - map['voting_visibility'] = Variable( - $PollsTable.$convertervotingVisibility.toSql(votingVisibility)); + 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_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)); + $PollsTable.$convertervoteCountsByOption.toSql(voteCountsByOption), + ); } map['vote_count'] = Variable(voteCount); if (!nullToAbsent || createdById != null) { @@ -4823,31 +5295,26 @@ class PollEntity extends DataClass implements Insertable { map['created_at'] = Variable(createdAt); map['updated_at'] = Variable(updatedAt); if (!nullToAbsent || extraData != null) { - map['extra_data'] = - Variable($PollsTable.$converterextraDatan.toSql(extraData)); + map['extra_data'] = Variable($PollsTable.$converterextraDatan.toSql(extraData)); } return map; } - factory PollEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + 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']), + votingVisibility: serializer.fromJson(json['votingVisibility']), enforceUniqueVote: serializer.fromJson(json['enforceUniqueVote']), maxVotesAllowed: serializer.fromJson(json['maxVotesAllowed']), - allowUserSuggestedOptions: - serializer.fromJson(json['allowUserSuggestedOptions']), + 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']), + voteCountsByOption: serializer.fromJson>(json['voteCountsByOption']), voteCount: serializer.fromJson(json['voteCount']), createdById: serializer.fromJson(json['createdById']), createdAt: serializer.fromJson(json['createdAt']), @@ -4866,13 +5333,11 @@ class PollEntity extends DataClass implements Insertable { 'votingVisibility': serializer.toJson(votingVisibility), 'enforceUniqueVote': serializer.toJson(enforceUniqueVote), 'maxVotesAllowed': serializer.toJson(maxVotesAllowed), - 'allowUserSuggestedOptions': - serializer.toJson(allowUserSuggestedOptions), + 'allowUserSuggestedOptions': serializer.toJson(allowUserSuggestedOptions), 'allowAnswers': serializer.toJson(allowAnswers), 'isClosed': serializer.toJson(isClosed), 'answersCount': serializer.toJson(answersCount), - 'voteCountsByOption': - serializer.toJson>(voteCountsByOption), + 'voteCountsByOption': serializer.toJson>(voteCountsByOption), 'voteCount': serializer.toJson(voteCount), 'createdById': serializer.toJson(createdById), 'createdAt': serializer.toJson(createdAt), @@ -4881,78 +5346,61 @@ class PollEntity extends DataClass implements Insertable { }; } - 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 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, + 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, + 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, + 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, + 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, + 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, @@ -4985,23 +5433,24 @@ class PollEntity extends DataClass implements Insertable { @override int get hashCode => Object.hash( - id, - name, - description, - options, - votingVisibility, - enforceUniqueVote, - maxVotesAllowed, - allowUserSuggestedOptions, - allowAnswers, - isClosed, - answersCount, - voteCountsByOption, - voteCount, - createdById, - createdAt, - updatedAt, - extraData); + 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) || @@ -5083,10 +5532,10 @@ class PollsCompanion extends UpdateCompanion { 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); + }) : id = Value(id), + name = Value(name), + options = Value(options), + voteCountsByOption = Value(voteCountsByOption); static Insertable custom({ Expression? id, Expression? name, @@ -5115,13 +5564,11 @@ class PollsCompanion extends UpdateCompanion { 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 (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 (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, @@ -5131,25 +5578,26 @@ class PollsCompanion extends UpdateCompanion { }); } - 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}) { + 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, @@ -5158,8 +5606,7 @@ class PollsCompanion extends UpdateCompanion { votingVisibility: votingVisibility ?? this.votingVisibility, enforceUniqueVote: enforceUniqueVote ?? this.enforceUniqueVote, maxVotesAllowed: maxVotesAllowed ?? this.maxVotesAllowed, - allowUserSuggestedOptions: - allowUserSuggestedOptions ?? this.allowUserSuggestedOptions, + allowUserSuggestedOptions: allowUserSuggestedOptions ?? this.allowUserSuggestedOptions, allowAnswers: allowAnswers ?? this.allowAnswers, isClosed: isClosed ?? this.isClosed, answersCount: answersCount ?? this.answersCount, @@ -5186,12 +5633,10 @@ class PollsCompanion extends UpdateCompanion { map['description'] = Variable(description.value); } if (options.present) { - map['options'] = - Variable($PollsTable.$converteroptions.toSql(options.value)); + map['options'] = Variable($PollsTable.$converteroptions.toSql(options.value)); } if (votingVisibility.present) { - map['voting_visibility'] = Variable( - $PollsTable.$convertervotingVisibility.toSql(votingVisibility.value)); + map['voting_visibility'] = Variable($PollsTable.$convertervotingVisibility.toSql(votingVisibility.value)); } if (enforceUniqueVote.present) { map['enforce_unique_vote'] = Variable(enforceUniqueVote.value); @@ -5200,8 +5645,7 @@ class PollsCompanion extends UpdateCompanion { map['max_votes_allowed'] = Variable(maxVotesAllowed.value); } if (allowUserSuggestedOptions.present) { - map['allow_user_suggested_options'] = - Variable(allowUserSuggestedOptions.value); + map['allow_user_suggested_options'] = Variable(allowUserSuggestedOptions.value); } if (allowAnswers.present) { map['allow_answers'] = Variable(allowAnswers.value); @@ -5213,9 +5657,9 @@ class PollsCompanion extends UpdateCompanion { map['answers_count'] = Variable(answersCount.value); } if (voteCountsByOption.present) { - map['vote_counts_by_option'] = Variable($PollsTable - .$convertervoteCountsByOption - .toSql(voteCountsByOption.value)); + map['vote_counts_by_option'] = Variable( + $PollsTable.$convertervoteCountsByOption.toSql(voteCountsByOption.value), + ); } if (voteCount.present) { map['vote_count'] = Variable(voteCount.value); @@ -5230,8 +5674,7 @@ class PollsCompanion extends UpdateCompanion { map['updated_at'] = Variable(updatedAt.value); } if (extraData.present) { - map['extra_data'] = Variable( - $PollsTable.$converterextraDatan.toSql(extraData.value)); + map['extra_data'] = Variable($PollsTable.$converterextraDatan.toSql(extraData.value)); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -5265,8 +5708,7 @@ class PollsCompanion extends UpdateCompanion { } } -class $PollVotesTable extends PollVotes - with TableInfo<$PollVotesTable, PollVoteEntity> { +class $PollVotesTable extends PollVotes with TableInfo<$PollVotesTable, PollVoteEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -5274,90 +5716,100 @@ class $PollVotesTable extends PollVotes static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + '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'); + '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'); + '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'); + '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'); + '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); + '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); + 'user_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); @override - List get $columns => - [id, pollId, optionId, answerText, createdAt, updatedAt, userId]; + 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}) { + 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)); + context.handle(_pollIdMeta, pollId.isAcceptableOrUnknown(data['poll_id']!, _pollIdMeta)); } if (data.containsKey('option_id')) { - context.handle(_optionIdMeta, - optionId.isAcceptableOrUnknown(data['option_id']!, _optionIdMeta)); + context.handle(_optionIdMeta, optionId.isAcceptableOrUnknown(data['option_id']!, _optionIdMeta)); } if (data.containsKey('answer_text')) { - context.handle( - _answerTextMeta, - answerText.isAcceptableOrUnknown( - data['answer_text']!, _answerTextMeta)); + context.handle(_answerTextMeta, answerText.isAcceptableOrUnknown(data['answer_text']!, _answerTextMeta)); } if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + 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(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); } if (data.containsKey('user_id')) { - context.handle(_userIdMeta, - userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + context.handle(_userIdMeta, userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); } return context; } @@ -5368,20 +5820,13 @@ class $PollVotesTable extends PollVotes 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']), + 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']), ); } @@ -5418,14 +5863,15 @@ class PollVoteEntity extends DataClass implements Insertable { /// /// 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}); + 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 = {}; @@ -5449,8 +5895,7 @@ class PollVoteEntity extends DataClass implements Insertable { return map; } - factory PollVoteEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + factory PollVoteEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return PollVoteEntity( id: serializer.fromJson(json['id']), @@ -5476,30 +5921,29 @@ class PollVoteEntity extends DataClass implements Insertable { }; } - 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 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, + 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, @@ -5521,8 +5965,7 @@ class PollVoteEntity extends DataClass implements Insertable { } @override - int get hashCode => Object.hash( - id, pollId, optionId, answerText, createdAt, updatedAt, userId); + int get hashCode => Object.hash(id, pollId, optionId, answerText, createdAt, updatedAt, userId); @override bool operator ==(Object other) => identical(this, other) || @@ -5587,15 +6030,16 @@ class PollVotesCompanion extends UpdateCompanion { }); } - PollVotesCompanion copyWith( - {Value? id, - Value? pollId, - Value? optionId, - Value? answerText, - Value? createdAt, - Value? updatedAt, - Value? userId, - Value? rowid}) { + PollVotesCompanion copyWith({ + Value? id, + Value? pollId, + Value? optionId, + Value? answerText, + Value? createdAt, + Value? updatedAt, + Value? userId, + Value? rowid, + }) { return PollVotesCompanion( id: id ?? this.id, pollId: pollId ?? this.pollId, @@ -5663,109 +6107,131 @@ class $PinnedMessageReactionsTable extends PinnedMessageReactions 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 _messageIdMeta = - const VerificationMeta('messageId'); + 'user_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _messageIdMeta = const VerificationMeta('messageId'); @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')); + 'message_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES pinned_messages (id) ON DELETE CASCADE'), + ); static const VerificationMeta _typeMeta = const VerificationMeta('type'); @override late final GeneratedColumn type = GeneratedColumn( - 'type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); + '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'); @override late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); + '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 _scoreMeta = const VerificationMeta('score'); @override late final GeneratedColumn score = GeneratedColumn( - 'score', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const Constant(0)); - static const VerificationMeta _extraDataMeta = - const VerificationMeta('extraData'); - @override - late final GeneratedColumnWithTypeConverter?, String> - extraData = GeneratedColumn('extra_data', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false) - .withConverter?>( - $PinnedMessageReactionsTable.$converterextraDatan); - @override - List get $columns => - [userId, messageId, type, createdAt, score, extraData]; + '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?>($PinnedMessageReactionsTable.$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 = 'pinned_message_reactions'; @override - VerificationContext validateIntegrity( - Insertable instance, - {bool isInserting = false}) { + 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); + context.handle(_userIdMeta, userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); } if (data.containsKey('message_id')) { - context.handle(_messageIdMeta, - messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); - } else if (isInserting) { - context.missing(_messageIdMeta); + context.handle(_messageIdMeta, messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); } if (data.containsKey('type')) { - context.handle( - _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + context.handle(_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, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + 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('score')) { - context.handle( - _scoreMeta, score.isAcceptableOrUnknown(data['score']!, _scoreMeta)); + context.handle(_scoreMeta, score.isAcceptableOrUnknown(data['score']!, _scoreMeta)); } - context.handle(_extraDataMeta, const VerificationResult.success()); return context; } @override Set get $primaryKey => {messageId, type, userId}; @override - PinnedMessageReactionEntity map(Map data, - {String? tablePrefix}) { + PinnedMessageReactionEntity 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'])!, - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - score: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}score'])!, + 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'])!, + score: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}score'])!, extraData: $PinnedMessageReactionsTable.$converterextraDatan.fromSql( - attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}extra_data']), + ), ); } @@ -5774,61 +6240,77 @@ class $PinnedMessageReactionsTable extends PinnedMessageReactions return $PinnedMessageReactionsTable(attachedDatabase, alias); } - static TypeConverter, String> $converterextraData = - MapConverter(); - static TypeConverter?, String?> $converterextraDatan = - NullAwareTypeConverter.wrap($converterextraData); + static TypeConverter, String> $converterextraData = MapConverter(); + static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap( + $converterextraData, + ); } -class PinnedMessageReactionEntity extends DataClass - implements Insertable { +class PinnedMessageReactionEntity extends DataClass implements Insertable { /// The id of the user that sent the reaction - final String userId; + final String? userId; /// The messageId to which the reaction belongs - final String messageId; + final String? messageId; /// The type of the reaction final String type; + /// The emoji code for the reaction + final String? emojiCode; + /// The DateTime on which the reaction is created final DateTime createdAt; + /// The DateTime on which the reaction was last updated + final DateTime updatedAt; + /// The score of the reaction (ie. number of reactions sent) final int score; /// 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}); + 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['user_id'] = Variable(userId); - map['message_id'] = Variable(messageId); + if (!nullToAbsent || userId != null) { + map['user_id'] = Variable(userId); + } + if (!nullToAbsent || messageId != null) { + map['message_id'] = Variable(messageId); + } 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( - $PinnedMessageReactionsTable.$converterextraDatan.toSql(extraData)); + map['extra_data'] = Variable($PinnedMessageReactionsTable.$converterextraDatan.toSql(extraData)); } return map; } - factory PinnedMessageReactionEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + factory PinnedMessageReactionEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return PinnedMessageReactionEntity( - userId: serializer.fromJson(json['userId']), - messageId: serializer.fromJson(json['messageId']), + 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']), ); @@ -5837,37 +6319,44 @@ class PinnedMessageReactionEntity extends DataClass Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'userId': serializer.toJson(userId), - 'messageId': serializer.toJson(messageId), + '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), }; } - 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, - ); - PinnedMessageReactionEntity copyWithCompanion( - PinnedMessageReactionsCompanion data) { + 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(), + }) => 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, + ); + 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, ); @@ -5879,7 +6368,9 @@ class PinnedMessageReactionEntity extends DataClass ..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(')')) @@ -5887,8 +6378,7 @@ class PinnedMessageReactionEntity extends DataClass } @override - int get hashCode => - Object.hash(userId, messageId, type, createdAt, score, extraData); + int get hashCode => Object.hash(userId, messageId, type, emojiCode, createdAt, updatedAt, score, extraData); @override bool operator ==(Object other) => identical(this, other) || @@ -5896,17 +6386,20 @@ class PinnedMessageReactionEntity extends DataClass 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 PinnedMessageReactionsCompanion - extends UpdateCompanion { - final Value userId; - final Value messageId; +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; @@ -5914,27 +6407,31 @@ class 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(), }); PinnedMessageReactionsCompanion.insert({ - required String userId, - required String messageId, + 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(), - }) : userId = Value(userId), - messageId = Value(messageId), - type = Value(type); + }) : type = Value(type); static Insertable custom({ Expression? userId, Expression? messageId, Expression? type, + Expression? emojiCode, Expression? createdAt, + Expression? updatedAt, Expression? score, Expression? extraData, Expression? rowid, @@ -5943,26 +6440,33 @@ class PinnedMessageReactionsCompanion 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, }); } - PinnedMessageReactionsCompanion copyWith( - {Value? userId, - Value? messageId, - Value? type, - Value? createdAt, - Value? score, - Value?>? extraData, - Value? rowid}) { + PinnedMessageReactionsCompanion copyWith({ + Value? userId, + Value? messageId, + Value? type, + Value? emojiCode, + Value? createdAt, + Value? updatedAt, + Value? score, + Value?>? extraData, + Value? rowid, + }) { 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, @@ -5981,16 +6485,20 @@ class PinnedMessageReactionsCompanion if (type.present) { map['type'] = Variable(type.value); } + if (emojiCode.present) { + map['emoji_code'] = Variable(emojiCode.value); + } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } if (score.present) { map['score'] = Variable(score.value); } if (extraData.present) { - map['extra_data'] = Variable($PinnedMessageReactionsTable - .$converterextraDatan - .toSql(extraData.value)); + map['extra_data'] = Variable($PinnedMessageReactionsTable.$converterextraDatan.toSql(extraData.value)); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -6004,7 +6512,9 @@ class 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') @@ -6013,8 +6523,7 @@ class PinnedMessageReactionsCompanion } } -class $ReactionsTable extends Reactions - with TableInfo<$ReactionsTable, ReactionEntity> { +class $ReactionsTable extends Reactions with TableInfo<$ReactionsTable, ReactionEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -6022,85 +6531,112 @@ class $ReactionsTable extends Reactions 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 _messageIdMeta = - const VerificationMeta('messageId'); + 'user_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _messageIdMeta = const VerificationMeta('messageId'); @override late final GeneratedColumn messageId = GeneratedColumn( - 'message_id', aliasedName, false, - type: DriftSqlType.string, - requiredDuringInsert: true, - defaultConstraints: GeneratedColumn.constraintIsAlways( - 'REFERENCES messages (id) ON DELETE CASCADE')); + 'message_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways('REFERENCES messages (id) ON DELETE CASCADE'), + ); static const VerificationMeta _typeMeta = const VerificationMeta('type'); @override late final GeneratedColumn type = GeneratedColumn( - 'type', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); - static const VerificationMeta _createdAtMeta = - const VerificationMeta('createdAt'); + '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'); @override late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', aliasedName, false, - type: DriftSqlType.dateTime, - requiredDuringInsert: false, - defaultValue: currentDateAndTime); + '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 _scoreMeta = const VerificationMeta('score'); @override late final GeneratedColumn score = GeneratedColumn( - 'score', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const Constant(0)); - static const VerificationMeta _extraDataMeta = - const VerificationMeta('extraData'); - @override - 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, createdAt, score, extraData]; + '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?>($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 = 'reactions'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + 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); + context.handle(_userIdMeta, userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); } if (data.containsKey('message_id')) { - context.handle(_messageIdMeta, - messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); - } else if (isInserting) { - context.missing(_messageIdMeta); + context.handle(_messageIdMeta, messageId.isAcceptableOrUnknown(data['message_id']!, _messageIdMeta)); } if (data.containsKey('type')) { - context.handle( - _typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); + context.handle(_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, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + 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('score')) { - context.handle( - _scoreMeta, score.isAcceptableOrUnknown(data['score']!, _scoreMeta)); + context.handle(_scoreMeta, score.isAcceptableOrUnknown(data['score']!, _scoreMeta)); } - context.handle(_extraDataMeta, const VerificationResult.success()); return context; } @@ -6110,19 +6646,16 @@ class $ReactionsTable extends Reactions ReactionEntity map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; 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'])!, - createdAt: attachedDatabase.typeMapping - .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, - score: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}score'])!, - extraData: $ReactionsTable.$converterextraDatan.fromSql(attachedDatabase - .typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}extra_data'])), + 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'])!, + score: attachedDatabase.typeMapping.read(DriftSqlType.int, data['${effectivePrefix}score'])!, + extraData: $ReactionsTable.$converterextraDatan.fromSql( + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}extra_data']), + ), ); } @@ -6131,60 +6664,77 @@ class $ReactionsTable extends Reactions return $ReactionsTable(attachedDatabase, alias); } - static TypeConverter, String> $converterextraData = - MapConverter(); - static TypeConverter?, String?> $converterextraDatan = - NullAwareTypeConverter.wrap($converterextraData); + static TypeConverter, String> $converterextraData = MapConverter(); + static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap( + $converterextraData, + ); } class ReactionEntity extends DataClass implements Insertable { /// The id of the user that sent the reaction - final String userId; + final String? userId; /// The messageId to which the reaction belongs - final String messageId; + final String? messageId; /// The type of the reaction final String type; + /// The emoji code for the reaction + final String? emojiCode; + /// The DateTime on which the reaction is created final DateTime createdAt; + /// The DateTime on which the reaction was last updated + final DateTime updatedAt; + /// The score of the reaction (ie. number of reactions sent) final int score; /// Reaction custom extraData final Map? extraData; - const ReactionEntity( - {required this.userId, - required this.messageId, - required this.type, - required this.createdAt, - required this.score, - this.extraData}); + const ReactionEntity({ + 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['user_id'] = Variable(userId); - map['message_id'] = Variable(messageId); + if (!nullToAbsent || userId != null) { + map['user_id'] = Variable(userId); + } + if (!nullToAbsent || messageId != null) { + map['message_id'] = Variable(messageId); + } 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( - $ReactionsTable.$converterextraDatan.toSql(extraData)); + map['extra_data'] = Variable($ReactionsTable.$converterextraDatan.toSql(extraData)); } return map; } - factory ReactionEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + factory ReactionEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return ReactionEntity( - userId: serializer.fromJson(json['userId']), - messageId: serializer.fromJson(json['messageId']), + 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']), ); @@ -6193,36 +6743,44 @@ class ReactionEntity extends DataClass implements Insertable { Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'userId': serializer.toJson(userId), - 'messageId': serializer.toJson(messageId), + '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), }; } - ReactionEntity copyWith( - {String? userId, - String? messageId, - String? type, - DateTime? createdAt, - int? score, - Value?> extraData = const Value.absent()}) => - ReactionEntity( - 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, - ); + ReactionEntity 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(), + }) => 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, + score: score ?? this.score, + extraData: extraData.present ? extraData.value : this.extraData, + ); 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, score: data.score.present ? data.score.value : this.score, extraData: data.extraData.present ? data.extraData.value : this.extraData, ); @@ -6234,7 +6792,9 @@ class ReactionEntity extends DataClass implements Insertable { ..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(')')) @@ -6242,8 +6802,7 @@ class ReactionEntity extends DataClass implements Insertable { } @override - int get hashCode => - Object.hash(userId, messageId, type, createdAt, score, extraData); + int get hashCode => Object.hash(userId, messageId, type, emojiCode, createdAt, updatedAt, score, extraData); @override bool operator ==(Object other) => identical(this, other) || @@ -6251,16 +6810,20 @@ class ReactionEntity extends DataClass implements Insertable { 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 ReactionsCompanion extends UpdateCompanion { - final Value userId; - final Value messageId; + 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; @@ -6268,27 +6831,31 @@ class ReactionsCompanion extends UpdateCompanion { 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(), }); ReactionsCompanion.insert({ - required String userId, - required String messageId, + 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(), - }) : userId = Value(userId), - messageId = Value(messageId), - type = Value(type); + }) : type = Value(type); static Insertable custom({ Expression? userId, Expression? messageId, Expression? type, + Expression? emojiCode, Expression? createdAt, + Expression? updatedAt, Expression? score, Expression? extraData, Expression? rowid, @@ -6297,26 +6864,33 @@ class ReactionsCompanion extends UpdateCompanion { 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, }); } - ReactionsCompanion copyWith( - {Value? userId, - Value? messageId, - Value? type, - Value? createdAt, - Value? score, - Value?>? extraData, - Value? rowid}) { + ReactionsCompanion copyWith({ + Value? userId, + Value? messageId, + Value? type, + Value? emojiCode, + Value? createdAt, + Value? updatedAt, + Value? score, + Value?>? extraData, + Value? rowid, + }) { 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, score: score ?? this.score, extraData: extraData ?? this.extraData, rowid: rowid ?? this.rowid, @@ -6335,15 +6909,20 @@ class ReactionsCompanion extends UpdateCompanion { if (type.present) { map['type'] = Variable(type.value); } + if (emojiCode.present) { + map['emoji_code'] = Variable(emojiCode.value); + } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } + if (updatedAt.present) { + map['updated_at'] = Variable(updatedAt.value); + } if (score.present) { map['score'] = Variable(score.value); } if (extraData.present) { - map['extra_data'] = Variable( - $ReactionsTable.$converterextraDatan.toSql(extraData.value)); + map['extra_data'] = Variable($ReactionsTable.$converterextraDatan.toSql(extraData.value)); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -6357,7 +6936,9 @@ class ReactionsCompanion extends UpdateCompanion { ..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') @@ -6374,98 +6955,125 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true); + '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'); + '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'); + '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'); + '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'); + '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); + '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, - defaultConstraints: - GeneratedColumn.constraintIsAlways('CHECK ("online" IN (0, 1))'), - defaultValue: const Constant(false)); + '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'); @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 _teamsRoleMeta = - const VerificationMeta('teamsRole'); - @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'); + 'banned', + aliasedName, + false, + type: DriftSqlType.bool, + 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); - static const VerificationMeta _extraDataMeta = - const VerificationMeta('extraData'); - @override - late final GeneratedColumnWithTypeConverter, String> - extraData = GeneratedColumn('extra_data', aliasedName, false, - type: DriftSqlType.string, requiredDuringInsert: true) - .withConverter>($UsersTable.$converterextraData); + '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 - ]; + 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 = 'users'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); if (data.containsKey('id')) { @@ -6474,43 +7082,32 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { context.missing(_idMeta); } if (data.containsKey('role')) { - context.handle( - _roleMeta, role.isAcceptableOrUnknown(data['role']!, _roleMeta)); + context.handle(_roleMeta, role.isAcceptableOrUnknown(data['role']!, _roleMeta)); } if (data.containsKey('language')) { - context.handle(_languageMeta, - language.isAcceptableOrUnknown(data['language']!, _languageMeta)); + context.handle(_languageMeta, language.isAcceptableOrUnknown(data['language']!, _languageMeta)); } if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + 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(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); } if (data.containsKey('last_active')) { - context.handle( - _lastActiveMeta, - lastActive.isAcceptableOrUnknown( - data['last_active']!, _lastActiveMeta)); + context.handle(_lastActiveMeta, lastActive.isAcceptableOrUnknown(data['last_active']!, _lastActiveMeta)); } if (data.containsKey('online')) { - context.handle(_onlineMeta, - online.isAcceptableOrUnknown(data['online']!, _onlineMeta)); + context.handle(_onlineMeta, online.isAcceptableOrUnknown(data['online']!, _onlineMeta)); } if (data.containsKey('banned')) { - context.handle(_bannedMeta, - banned.isAcceptableOrUnknown(data['banned']!, _bannedMeta)); + context.handle(_bannedMeta, banned.isAcceptableOrUnknown(data['banned']!, _bannedMeta)); } - context.handle(_teamsRoleMeta, const VerificationResult.success()); if (data.containsKey('avg_response_time')) { context.handle( - _avgResponseTimeMeta, - avgResponseTime.isAcceptableOrUnknown( - data['avg_response_time']!, _avgResponseTimeMeta)); + _avgResponseTimeMeta, + avgResponseTime.isAcceptableOrUnknown(data['avg_response_time']!, _avgResponseTimeMeta), + ); } - context.handle(_extraDataMeta, const VerificationResult.success()); return context; } @@ -6520,30 +7117,21 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { UserEntity 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'])!), + 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'])!, + ), ); } @@ -6552,12 +7140,11 @@ class $UsersTable extends Users with TableInfo<$UsersTable, UserEntity> { return $UsersTable(attachedDatabase, alias); } - static TypeConverter, String> $converterteamsRole = - MapConverter(); - static TypeConverter?, String?> $converterteamsRolen = - NullAwareTypeConverter.wrap($converterteamsRole); - static TypeConverter, String> $converterextraData = - MapConverter(); + static TypeConverter, String> $converterteamsRole = MapConverter(); + static TypeConverter?, String?> $converterteamsRolen = NullAwareTypeConverter.wrap( + $converterteamsRole, + ); + static TypeConverter, String> $converterextraData = MapConverter(); } class UserEntity extends DataClass implements Insertable { @@ -6595,18 +7182,19 @@ class UserEntity extends DataClass implements Insertable { /// 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}); + 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 = {}; @@ -6629,21 +7217,18 @@ class UserEntity extends DataClass implements Insertable { map['online'] = Variable(online); map['banned'] = Variable(banned); if (!nullToAbsent || teamsRole != null) { - map['teams_role'] = - Variable($UsersTable.$converterteamsRolen.toSql(teamsRole)); + 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['extra_data'] = Variable($UsersTable.$converterextraData.toSql(extraData)); } return map; } - factory UserEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + factory UserEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return UserEntity( id: serializer.fromJson(json['id']), @@ -6677,33 +7262,31 @@ class UserEntity extends DataClass implements Insertable { }; } - 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, - ); + 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, + ); UserEntity copyWithCompanion(UsersCompanion data) { return UserEntity( id: data.id.present ? data.id.value : this.id, @@ -6711,14 +7294,11 @@ class UserEntity extends DataClass implements Insertable { 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, + 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, + avgResponseTime: data.avgResponseTime.present ? data.avgResponseTime.value : this.avgResponseTime, extraData: data.extraData.present ? data.extraData.value : this.extraData, ); } @@ -6742,8 +7322,19 @@ class UserEntity extends DataClass implements Insertable { } @override - int get hashCode => Object.hash(id, role, language, createdAt, updatedAt, - lastActive, online, banned, teamsRole, avgResponseTime, 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) || @@ -6801,8 +7392,8 @@ class UsersCompanion extends UpdateCompanion { this.avgResponseTime = const Value.absent(), required Map extraData, this.rowid = const Value.absent(), - }) : id = Value(id), - extraData = Value(extraData); + }) : id = Value(id), + extraData = Value(extraData); static Insertable custom({ Expression? id, Expression? role, @@ -6833,19 +7424,20 @@ class UsersCompanion extends UpdateCompanion { }); } - 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}) { + 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, @@ -6890,15 +7482,13 @@ class UsersCompanion extends UpdateCompanion { map['banned'] = Variable(banned.value); } if (teamsRole.present) { - map['teams_role'] = Variable( - $UsersTable.$converterteamsRolen.toSql(teamsRole.value)); + 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( - $UsersTable.$converterextraData.toSql(extraData.value)); + map['extra_data'] = Variable($UsersTable.$converterextraData.toSql(extraData.value)); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -6926,8 +7516,7 @@ class UsersCompanion extends UpdateCompanion { } } -class $MembersTable extends Members - with TableInfo<$MembersTable, MemberEntity> { +class $MembersTable extends Members with TableInfo<$MembersTable, MemberEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -6935,211 +7524,224 @@ class $MembersTable extends Members 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'); + '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'); + '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'); + '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)); + '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'); + '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'); + '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'); + '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'); + '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)); - static const VerificationMeta _extraDataMeta = - const VerificationMeta('extraData'); - @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'); + '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'); + '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); + 'updated_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime, + ); + @override + late final GeneratedColumnWithTypeConverter, String> deletedMessages = GeneratedColumn( + 'deleted_messages', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ).withConverter>($MembersTable.$converterdeletedMessages); @override List get $columns => [ - userId, - channelCid, - channelRole, - inviteAcceptedAt, - inviteRejectedAt, - invited, - banned, - shadowBanned, - pinnedAt, - archivedAt, - isModerator, - extraData, - createdAt, - updatedAt - ]; + userId, + channelCid, + channelRole, + inviteAcceptedAt, + inviteRejectedAt, + invited, + banned, + shadowBanned, + pinnedAt, + archivedAt, + isModerator, + extraData, + createdAt, + updatedAt, + deletedMessages, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'members'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + 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)); + context.handle(_userIdMeta, userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); } else if (isInserting) { context.missing(_userIdMeta); } if (data.containsKey('channel_cid')) { - context.handle( - _channelCidMeta, - channelCid.isAcceptableOrUnknown( - data['channel_cid']!, _channelCidMeta)); + 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)); + 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)); + _inviteAcceptedAtMeta, + inviteAcceptedAt.isAcceptableOrUnknown(data['invite_accepted_at']!, _inviteAcceptedAtMeta), + ); } if (data.containsKey('invite_rejected_at')) { context.handle( - _inviteRejectedAtMeta, - inviteRejectedAt.isAcceptableOrUnknown( - data['invite_rejected_at']!, _inviteRejectedAtMeta)); + _inviteRejectedAtMeta, + inviteRejectedAt.isAcceptableOrUnknown(data['invite_rejected_at']!, _inviteRejectedAtMeta), + ); } if (data.containsKey('invited')) { - context.handle(_invitedMeta, - invited.isAcceptableOrUnknown(data['invited']!, _invitedMeta)); + context.handle(_invitedMeta, invited.isAcceptableOrUnknown(data['invited']!, _invitedMeta)); } if (data.containsKey('banned')) { - context.handle(_bannedMeta, - banned.isAcceptableOrUnknown(data['banned']!, _bannedMeta)); + context.handle(_bannedMeta, banned.isAcceptableOrUnknown(data['banned']!, _bannedMeta)); } if (data.containsKey('shadow_banned')) { - context.handle( - _shadowBannedMeta, - shadowBanned.isAcceptableOrUnknown( - data['shadow_banned']!, _shadowBannedMeta)); + context.handle(_shadowBannedMeta, shadowBanned.isAcceptableOrUnknown(data['shadow_banned']!, _shadowBannedMeta)); } if (data.containsKey('pinned_at')) { - context.handle(_pinnedAtMeta, - pinnedAt.isAcceptableOrUnknown(data['pinned_at']!, _pinnedAtMeta)); + context.handle(_pinnedAtMeta, pinnedAt.isAcceptableOrUnknown(data['pinned_at']!, _pinnedAtMeta)); } if (data.containsKey('archived_at')) { - context.handle( - _archivedAtMeta, - archivedAt.isAcceptableOrUnknown( - data['archived_at']!, _archivedAtMeta)); + context.handle(_archivedAtMeta, archivedAt.isAcceptableOrUnknown(data['archived_at']!, _archivedAtMeta)); } if (data.containsKey('is_moderator')) { - context.handle( - _isModeratorMeta, - isModerator.isAcceptableOrUnknown( - data['is_moderator']!, _isModeratorMeta)); + context.handle(_isModeratorMeta, isModerator.isAcceptableOrUnknown(data['is_moderator']!, _isModeratorMeta)); } - context.handle(_extraDataMeta, const VerificationResult.success()); if (data.containsKey('created_at')) { - context.handle(_createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + 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(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); } return context; } @@ -7150,35 +7752,31 @@ class $MembersTable extends Members MemberEntity map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return MemberEntity( - userId: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, - channelCid: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, - channelRole: attachedDatabase.typeMapping - .read(DriftSqlType.string, data['${effectivePrefix}channel_role']), + userId: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + 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']), + 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'])!, + 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'])!, + deletedMessages: $MembersTable.$converterdeletedMessages.fromSql( + attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}deleted_messages'])!, + ), ); } @@ -7187,10 +7785,11 @@ class $MembersTable extends Members return $MembersTable(attachedDatabase, alias); } - static TypeConverter, String> $converterextraData = - MapConverter(); - static TypeConverter?, String?> $converterextraDatan = - NullAwareTypeConverter.wrap($converterextraData); + static TypeConverter, String> $converterextraData = MapConverter(); + static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap( + $converterextraData, + ); + static TypeConverter, String> $converterdeletedMessages = ListConverter(); } class MemberEntity extends DataClass implements Insertable { @@ -7235,21 +7834,29 @@ class MemberEntity extends DataClass implements Insertable { /// 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}); + + /// List of message ids deleted by the member only for himself. + /// + /// These messages are now marked deleted for this member, but are still + /// kept as regular to other channel members. + final List deletedMessages; + 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, + required this.deletedMessages, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -7275,25 +7882,24 @@ class MemberEntity extends DataClass implements Insertable { } map['is_moderator'] = Variable(isModerator); if (!nullToAbsent || extraData != null) { - map['extra_data'] = - Variable($MembersTable.$converterextraDatan.toSql(extraData)); + map['extra_data'] = Variable($MembersTable.$converterextraDatan.toSql(extraData)); } map['created_at'] = Variable(createdAt); map['updated_at'] = Variable(updatedAt); + { + map['deleted_messages'] = Variable($MembersTable.$converterdeletedMessages.toSql(deletedMessages)); + } return map; } - factory MemberEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + factory MemberEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return MemberEntity( userId: serializer.fromJson(json['userId']), channelCid: serializer.fromJson(json['channelCid']), channelRole: serializer.fromJson(json['channelRole']), - inviteAcceptedAt: - serializer.fromJson(json['inviteAcceptedAt']), - inviteRejectedAt: - serializer.fromJson(json['inviteRejectedAt']), + 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']), @@ -7303,6 +7909,7 @@ class MemberEntity extends DataClass implements Insertable { extraData: serializer.fromJson?>(json['extraData']), createdAt: serializer.fromJson(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), + deletedMessages: serializer.fromJson>(json['deletedMessages']), ); } @override @@ -7323,70 +7930,60 @@ class MemberEntity extends DataClass implements Insertable { 'extraData': serializer.toJson?>(extraData), 'createdAt': serializer.toJson(createdAt), 'updatedAt': serializer.toJson(updatedAt), + 'deletedMessages': serializer.toJson>(deletedMessages), }; } - 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 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, + List? deletedMessages, + }) => 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, + deletedMessages: deletedMessages ?? this.deletedMessages, + ); 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, + 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, + 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, + 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, + deletedMessages: data.deletedMessages.present ? data.deletedMessages.value : this.deletedMessages, ); } @@ -7406,27 +8003,30 @@ class MemberEntity extends DataClass implements Insertable { ..write('isModerator: $isModerator, ') ..write('extraData: $extraData, ') ..write('createdAt: $createdAt, ') - ..write('updatedAt: $updatedAt') + ..write('updatedAt: $updatedAt, ') + ..write('deletedMessages: $deletedMessages') ..write(')')) .toString(); } @override int get hashCode => Object.hash( - userId, - channelCid, - channelRole, - inviteAcceptedAt, - inviteRejectedAt, - invited, - banned, - shadowBanned, - pinnedAt, - archivedAt, - isModerator, - extraData, - createdAt, - updatedAt); + userId, + channelCid, + channelRole, + inviteAcceptedAt, + inviteRejectedAt, + invited, + banned, + shadowBanned, + pinnedAt, + archivedAt, + isModerator, + extraData, + createdAt, + updatedAt, + deletedMessages, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -7444,7 +8044,8 @@ class MemberEntity extends DataClass implements Insertable { other.isModerator == this.isModerator && other.extraData == this.extraData && other.createdAt == this.createdAt && - other.updatedAt == this.updatedAt); + other.updatedAt == this.updatedAt && + other.deletedMessages == this.deletedMessages); } class MembersCompanion extends UpdateCompanion { @@ -7462,6 +8063,7 @@ class MembersCompanion extends UpdateCompanion { final Value?> extraData; final Value createdAt; final Value updatedAt; + final Value> deletedMessages; final Value rowid; const MembersCompanion({ this.userId = const Value.absent(), @@ -7478,6 +8080,7 @@ class MembersCompanion extends UpdateCompanion { this.extraData = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), + this.deletedMessages = const Value.absent(), this.rowid = const Value.absent(), }); MembersCompanion.insert({ @@ -7495,9 +8098,11 @@ class MembersCompanion extends UpdateCompanion { this.extraData = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), + required List deletedMessages, this.rowid = const Value.absent(), - }) : userId = Value(userId), - channelCid = Value(channelCid); + }) : userId = Value(userId), + channelCid = Value(channelCid), + deletedMessages = Value(deletedMessages); static Insertable custom({ Expression? userId, Expression? channelCid, @@ -7513,6 +8118,7 @@ class MembersCompanion extends UpdateCompanion { Expression? extraData, Expression? createdAt, Expression? updatedAt, + Expression? deletedMessages, Expression? rowid, }) { return RawValuesInsertable({ @@ -7530,26 +8136,29 @@ class MembersCompanion extends UpdateCompanion { if (extraData != null) 'extra_data': extraData, if (createdAt != null) 'created_at': createdAt, if (updatedAt != null) 'updated_at': updatedAt, + if (deletedMessages != null) 'deleted_messages': deletedMessages, if (rowid != null) 'rowid': rowid, }); } - MembersCompanion copyWith( - {Value? userId, - 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}) { + MembersCompanion copyWith({ + Value? userId, + 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>? deletedMessages, + Value? rowid, + }) { return MembersCompanion( userId: userId ?? this.userId, channelCid: channelCid ?? this.channelCid, @@ -7565,6 +8174,7 @@ class MembersCompanion extends UpdateCompanion { extraData: extraData ?? this.extraData, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + deletedMessages: deletedMessages ?? this.deletedMessages, rowid: rowid ?? this.rowid, ); } @@ -7606,8 +8216,7 @@ class MembersCompanion extends UpdateCompanion { map['is_moderator'] = Variable(isModerator.value); } if (extraData.present) { - map['extra_data'] = Variable( - $MembersTable.$converterextraDatan.toSql(extraData.value)); + map['extra_data'] = Variable($MembersTable.$converterextraDatan.toSql(extraData.value)); } if (createdAt.present) { map['created_at'] = Variable(createdAt.value); @@ -7615,6 +8224,9 @@ class MembersCompanion extends UpdateCompanion { if (updatedAt.present) { map['updated_at'] = Variable(updatedAt.value); } + if (deletedMessages.present) { + map['deleted_messages'] = Variable($MembersTable.$converterdeletedMessages.toSql(deletedMessages.value)); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -7638,6 +8250,7 @@ class MembersCompanion extends UpdateCompanion { ..write('extraData: $extraData, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') + ..write('deletedMessages: $deletedMessages, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -7649,115 +8262,128 @@ class $ReadsTable extends Reads with TableInfo<$ReadsTable, ReadEntity> { final GeneratedDatabase attachedDatabase; final String? _alias; $ReadsTable(this.attachedDatabase, [this._alias]); - static const VerificationMeta _lastReadMeta = - const VerificationMeta('lastRead'); + static const VerificationMeta _lastReadMeta = const VerificationMeta('lastRead'); @override late final GeneratedColumn lastRead = GeneratedColumn( - 'last_read', aliasedName, false, - type: DriftSqlType.dateTime, requiredDuringInsert: true); + 'last_read', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); 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'); + '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 _unreadMessagesMeta = - const VerificationMeta('unreadMessages'); + '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 unreadMessages = GeneratedColumn( - 'unread_messages', aliasedName, false, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultValue: const Constant(0)); - static const VerificationMeta _lastReadMessageIdMeta = - const VerificationMeta('lastReadMessageId'); - @override - late final GeneratedColumn lastReadMessageId = - GeneratedColumn('last_read_message_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); - static const VerificationMeta _lastDeliveredAtMeta = - const VerificationMeta('lastDeliveredAt'); - @override - late final GeneratedColumn lastDeliveredAt = - GeneratedColumn('last_delivered_at', aliasedName, true, - type: DriftSqlType.dateTime, requiredDuringInsert: false); - static const VerificationMeta _lastDeliveredMessageIdMeta = - const VerificationMeta('lastDeliveredMessageId'); - @override - late final GeneratedColumn lastDeliveredMessageId = - GeneratedColumn('last_delivered_message_id', aliasedName, true, - type: DriftSqlType.string, requiredDuringInsert: false); + 'unread_messages', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + static const VerificationMeta _lastReadMessageIdMeta = const VerificationMeta('lastReadMessageId'); + @override + late final GeneratedColumn lastReadMessageId = GeneratedColumn( + 'last_read_message_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _lastDeliveredAtMeta = const VerificationMeta('lastDeliveredAt'); + @override + late final GeneratedColumn lastDeliveredAt = GeneratedColumn( + 'last_delivered_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); + static const VerificationMeta _lastDeliveredMessageIdMeta = const VerificationMeta('lastDeliveredMessageId'); + @override + late final GeneratedColumn lastDeliveredMessageId = GeneratedColumn( + 'last_delivered_message_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); @override List get $columns => [ - lastRead, - userId, - channelCid, - unreadMessages, - lastReadMessageId, - lastDeliveredAt, - lastDeliveredMessageId - ]; + lastRead, + userId, + channelCid, + unreadMessages, + lastReadMessageId, + lastDeliveredAt, + lastDeliveredMessageId, + ]; @override String get aliasedName => _alias ?? actualTableName; @override String get actualTableName => $name; static const String $name = 'reads'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + 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)); + 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)); + context.handle(_userIdMeta, userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); } else if (isInserting) { context.missing(_userIdMeta); } if (data.containsKey('channel_cid')) { - context.handle( - _channelCidMeta, - channelCid.isAcceptableOrUnknown( - data['channel_cid']!, _channelCidMeta)); + context.handle(_channelCidMeta, channelCid.isAcceptableOrUnknown(data['channel_cid']!, _channelCidMeta)); } else if (isInserting) { context.missing(_channelCidMeta); } if (data.containsKey('unread_messages')) { context.handle( - _unreadMessagesMeta, - unreadMessages.isAcceptableOrUnknown( - data['unread_messages']!, _unreadMessagesMeta)); + _unreadMessagesMeta, + unreadMessages.isAcceptableOrUnknown(data['unread_messages']!, _unreadMessagesMeta), + ); } if (data.containsKey('last_read_message_id')) { context.handle( - _lastReadMessageIdMeta, - lastReadMessageId.isAcceptableOrUnknown( - data['last_read_message_id']!, _lastReadMessageIdMeta)); + _lastReadMessageIdMeta, + lastReadMessageId.isAcceptableOrUnknown(data['last_read_message_id']!, _lastReadMessageIdMeta), + ); } if (data.containsKey('last_delivered_at')) { context.handle( - _lastDeliveredAtMeta, - lastDeliveredAt.isAcceptableOrUnknown( - data['last_delivered_at']!, _lastDeliveredAtMeta)); + _lastDeliveredAtMeta, + lastDeliveredAt.isAcceptableOrUnknown(data['last_delivered_at']!, _lastDeliveredAtMeta), + ); } if (data.containsKey('last_delivered_message_id')) { context.handle( - _lastDeliveredMessageIdMeta, - lastDeliveredMessageId.isAcceptableOrUnknown( - data['last_delivered_message_id']!, _lastDeliveredMessageIdMeta)); + _lastDeliveredMessageIdMeta, + lastDeliveredMessageId.isAcceptableOrUnknown(data['last_delivered_message_id']!, _lastDeliveredMessageIdMeta), + ); } return context; } @@ -7768,21 +8394,22 @@ class $ReadsTable extends Reads with TableInfo<$ReadsTable, ReadEntity> { ReadEntity 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'])!, + 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']), + DriftSqlType.string, + data['${effectivePrefix}last_read_message_id'], + ), lastDeliveredAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, data['${effectivePrefix}last_delivered_at']), + DriftSqlType.dateTime, + data['${effectivePrefix}last_delivered_at'], + ), lastDeliveredMessageId: attachedDatabase.typeMapping.read( - DriftSqlType.string, - data['${effectivePrefix}last_delivered_message_id']), + DriftSqlType.string, + data['${effectivePrefix}last_delivered_message_id'], + ), ); } @@ -7813,14 +8440,15 @@ class ReadEntity extends DataClass implements Insertable { /// Id of the last delivered message final String? lastDeliveredMessageId; - const ReadEntity( - {required this.lastRead, - required this.userId, - required this.channelCid, - required this.unreadMessages, - this.lastReadMessageId, - this.lastDeliveredAt, - this.lastDeliveredMessageId}); + const ReadEntity({ + required this.lastRead, + required this.userId, + required this.channelCid, + required this.unreadMessages, + this.lastReadMessageId, + this.lastDeliveredAt, + this.lastDeliveredMessageId, + }); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -7835,25 +8463,21 @@ class ReadEntity extends DataClass implements Insertable { map['last_delivered_at'] = Variable(lastDeliveredAt); } if (!nullToAbsent || lastDeliveredMessageId != null) { - map['last_delivered_message_id'] = - Variable(lastDeliveredMessageId); + map['last_delivered_message_id'] = Variable(lastDeliveredMessageId); } return map; } - factory ReadEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + 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']), + lastReadMessageId: serializer.fromJson(json['lastReadMessageId']), lastDeliveredAt: serializer.fromJson(json['lastDeliveredAt']), - lastDeliveredMessageId: - serializer.fromJson(json['lastDeliveredMessageId']), + lastDeliveredMessageId: serializer.fromJson(json['lastDeliveredMessageId']), ); } @override @@ -7866,49 +8490,35 @@ class ReadEntity extends DataClass implements Insertable { 'unreadMessages': serializer.toJson(unreadMessages), 'lastReadMessageId': serializer.toJson(lastReadMessageId), 'lastDeliveredAt': serializer.toJson(lastDeliveredAt), - 'lastDeliveredMessageId': - serializer.toJson(lastDeliveredMessageId), + 'lastDeliveredMessageId': serializer.toJson(lastDeliveredMessageId), }; } - ReadEntity copyWith( - {DateTime? lastRead, - String? userId, - String? channelCid, - int? unreadMessages, - Value lastReadMessageId = const Value.absent(), - Value lastDeliveredAt = const Value.absent(), - Value lastDeliveredMessageId = 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, - lastDeliveredAt: lastDeliveredAt.present - ? lastDeliveredAt.value - : this.lastDeliveredAt, - lastDeliveredMessageId: lastDeliveredMessageId.present - ? lastDeliveredMessageId.value - : this.lastDeliveredMessageId, - ); + ReadEntity copyWith({ + DateTime? lastRead, + String? userId, + String? channelCid, + int? unreadMessages, + Value lastReadMessageId = const Value.absent(), + Value lastDeliveredAt = const Value.absent(), + Value lastDeliveredMessageId = 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, + lastDeliveredAt: lastDeliveredAt.present ? lastDeliveredAt.value : this.lastDeliveredAt, + lastDeliveredMessageId: lastDeliveredMessageId.present ? lastDeliveredMessageId.value : this.lastDeliveredMessageId, + ); 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, - lastDeliveredAt: data.lastDeliveredAt.present - ? data.lastDeliveredAt.value - : this.lastDeliveredAt, + 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, + lastDeliveredAt: data.lastDeliveredAt.present ? data.lastDeliveredAt.value : this.lastDeliveredAt, lastDeliveredMessageId: data.lastDeliveredMessageId.present ? data.lastDeliveredMessageId.value : this.lastDeliveredMessageId, @@ -7930,8 +8540,15 @@ class ReadEntity extends DataClass implements Insertable { } @override - int get hashCode => Object.hash(lastRead, userId, channelCid, unreadMessages, - lastReadMessageId, lastDeliveredAt, lastDeliveredMessageId); + int get hashCode => Object.hash( + lastRead, + userId, + channelCid, + unreadMessages, + lastReadMessageId, + lastDeliveredAt, + lastDeliveredMessageId, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -7973,9 +8590,9 @@ class ReadsCompanion extends UpdateCompanion { this.lastDeliveredAt = const Value.absent(), this.lastDeliveredMessageId = const Value.absent(), this.rowid = const Value.absent(), - }) : lastRead = Value(lastRead), - userId = Value(userId), - channelCid = Value(channelCid); + }) : lastRead = Value(lastRead), + userId = Value(userId), + channelCid = Value(channelCid); static Insertable custom({ Expression? lastRead, Expression? userId, @@ -7993,21 +8610,21 @@ class ReadsCompanion extends UpdateCompanion { if (unreadMessages != null) 'unread_messages': unreadMessages, if (lastReadMessageId != null) 'last_read_message_id': lastReadMessageId, if (lastDeliveredAt != null) 'last_delivered_at': lastDeliveredAt, - if (lastDeliveredMessageId != null) - 'last_delivered_message_id': lastDeliveredMessageId, + if (lastDeliveredMessageId != null) 'last_delivered_message_id': lastDeliveredMessageId, if (rowid != null) 'rowid': rowid, }); } - ReadsCompanion copyWith( - {Value? lastRead, - Value? userId, - Value? channelCid, - Value? unreadMessages, - Value? lastReadMessageId, - Value? lastDeliveredAt, - Value? lastDeliveredMessageId, - Value? rowid}) { + ReadsCompanion copyWith({ + Value? lastRead, + Value? userId, + Value? channelCid, + Value? unreadMessages, + Value? lastReadMessageId, + Value? lastDeliveredAt, + Value? lastDeliveredMessageId, + Value? rowid, + }) { return ReadsCompanion( lastRead: lastRead ?? this.lastRead, userId: userId ?? this.userId, @@ -8015,8 +8632,7 @@ class ReadsCompanion extends UpdateCompanion { unreadMessages: unreadMessages ?? this.unreadMessages, lastReadMessageId: lastReadMessageId ?? this.lastReadMessageId, lastDeliveredAt: lastDeliveredAt ?? this.lastDeliveredAt, - lastDeliveredMessageId: - lastDeliveredMessageId ?? this.lastDeliveredMessageId, + lastDeliveredMessageId: lastDeliveredMessageId ?? this.lastDeliveredMessageId, rowid: rowid ?? this.rowid, ); } @@ -8043,8 +8659,7 @@ class ReadsCompanion extends UpdateCompanion { map['last_delivered_at'] = Variable(lastDeliveredAt.value); } if (lastDeliveredMessageId.present) { - map['last_delivered_message_id'] = - Variable(lastDeliveredMessageId.value); + map['last_delivered_message_id'] = Variable(lastDeliveredMessageId.value); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -8068,24 +8683,29 @@ class ReadsCompanion extends UpdateCompanion { } } -class $ChannelQueriesTable extends ChannelQueries - with TableInfo<$ChannelQueriesTable, ChannelQueryEntity> { +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'); + 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'); + '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); + 'channel_cid', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); @override List get $columns => [queryHash, channelCid]; @override @@ -8094,21 +8714,16 @@ class $ChannelQueriesTable extends ChannelQueries String get actualTableName => $name; static const String $name = 'channel_queries'; @override - VerificationContext validateIntegrity(Insertable instance, - {bool isInserting = false}) { + 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)); + 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)); + context.handle(_channelCidMeta, channelCid.isAcceptableOrUnknown(data['channel_cid']!, _channelCidMeta)); } else if (isInserting) { context.missing(_channelCidMeta); } @@ -8121,10 +8736,8 @@ class $ChannelQueriesTable extends ChannelQueries 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'])!, + queryHash: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}query_hash'])!, + channelCid: attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}channel_cid'])!, ); } @@ -8134,8 +8747,7 @@ class $ChannelQueriesTable extends ChannelQueries } } -class ChannelQueryEntity extends DataClass - implements Insertable { +class ChannelQueryEntity extends DataClass implements Insertable { /// The unique hash of this query final String queryHash; @@ -8150,8 +8762,7 @@ class ChannelQueryEntity extends DataClass return map; } - factory ChannelQueryEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + factory ChannelQueryEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return ChannelQueryEntity( queryHash: serializer.fromJson(json['queryHash']), @@ -8167,16 +8778,14 @@ class ChannelQueryEntity extends DataClass }; } - ChannelQueryEntity copyWith({String? queryHash, String? channelCid}) => - ChannelQueryEntity( - queryHash: queryHash ?? this.queryHash, - channelCid: channelCid ?? this.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, + channelCid: data.channelCid.present ? data.channelCid.value : this.channelCid, ); } @@ -8194,9 +8803,7 @@ class ChannelQueryEntity extends DataClass @override bool operator ==(Object other) => identical(this, other) || - (other is ChannelQueryEntity && - other.queryHash == this.queryHash && - other.channelCid == this.channelCid); + (other is ChannelQueryEntity && other.queryHash == this.queryHash && other.channelCid == this.channelCid); } class ChannelQueriesCompanion extends UpdateCompanion { @@ -8212,8 +8819,8 @@ class ChannelQueriesCompanion extends UpdateCompanion { required String queryHash, required String channelCid, this.rowid = const Value.absent(), - }) : queryHash = Value(queryHash), - channelCid = Value(channelCid); + }) : queryHash = Value(queryHash), + channelCid = Value(channelCid); static Insertable custom({ Expression? queryHash, Expression? channelCid, @@ -8226,10 +8833,7 @@ class ChannelQueriesCompanion extends UpdateCompanion { }); } - ChannelQueriesCompanion copyWith( - {Value? queryHash, - Value? channelCid, - Value? rowid}) { + ChannelQueriesCompanion copyWith({Value? queryHash, Value? channelCid, Value? rowid}) { return ChannelQueriesCompanion( queryHash: queryHash ?? this.queryHash, channelCid: channelCid ?? this.channelCid, @@ -8263,8 +8867,7 @@ class ChannelQueriesCompanion extends UpdateCompanion { } } -class $ConnectionEventsTable extends ConnectionEvents - with TableInfo<$ConnectionEventsTable, ConnectionEventEntity> { +class $ConnectionEventsTable extends ConnectionEvents with TableInfo<$ConnectionEventsTable, ConnectionEventEntity> { @override final GeneratedDatabase attachedDatabase; final String? _alias; @@ -8272,99 +8875,101 @@ class $ConnectionEventsTable extends ConnectionEvents static const VerificationMeta _idMeta = const VerificationMeta('id'); @override late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - type: DriftSqlType.int, requiredDuringInsert: false); + '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); - static const VerificationMeta _ownUserMeta = - const VerificationMeta('ownUser'); - @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'); + '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'); + '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'); + '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'); + '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); + 'last_sync_at', + aliasedName, + true, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + ); @override - List get $columns => [ - id, - type, - ownUser, - totalUnreadCount, - unreadChannels, - lastEventAt, - lastSyncAt - ]; + 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}) { + 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)); + context.handle(_typeMeta, type.isAcceptableOrUnknown(data['type']!, _typeMeta)); } else if (isInserting) { context.missing(_typeMeta); } - context.handle(_ownUserMeta, const VerificationResult.success()); if (data.containsKey('total_unread_count')) { context.handle( - _totalUnreadCountMeta, - totalUnreadCount.isAcceptableOrUnknown( - data['total_unread_count']!, _totalUnreadCountMeta)); + _totalUnreadCountMeta, + totalUnreadCount.isAcceptableOrUnknown(data['total_unread_count']!, _totalUnreadCountMeta), + ); } if (data.containsKey('unread_channels')) { context.handle( - _unreadChannelsMeta, - unreadChannels.isAcceptableOrUnknown( - data['unread_channels']!, _unreadChannelsMeta)); + _unreadChannelsMeta, + unreadChannels.isAcceptableOrUnknown(data['unread_channels']!, _unreadChannelsMeta), + ); } if (data.containsKey('last_event_at')) { - context.handle( - _lastEventAtMeta, - lastEventAt.isAcceptableOrUnknown( - data['last_event_at']!, _lastEventAtMeta)); + 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)); + context.handle(_lastSyncAtMeta, lastSyncAt.isAcceptableOrUnknown(data['last_sync_at']!, _lastSyncAtMeta)); } return context; } @@ -8375,21 +8980,18 @@ class $ConnectionEventsTable extends ConnectionEvents 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'])!, + 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']), + 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']), ); } @@ -8398,14 +9000,13 @@ class $ConnectionEventsTable extends ConnectionEvents return $ConnectionEventsTable(attachedDatabase, alias); } - static TypeConverter, String> $converterownUser = - MapConverter(); - static TypeConverter?, String?> $converterownUsern = - NullAwareTypeConverter.wrap($converterownUser); + static TypeConverter, String> $converterownUser = MapConverter(); + static TypeConverter?, String?> $converterownUsern = NullAwareTypeConverter.wrap( + $converterownUser, + ); } -class ConnectionEventEntity extends DataClass - implements Insertable { +class ConnectionEventEntity extends DataClass implements Insertable { /// event id final int id; @@ -8426,22 +9027,22 @@ class ConnectionEventEntity extends DataClass /// 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}); + 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)); + map['own_user'] = Variable($ConnectionEventsTable.$converterownUsern.toSql(ownUser)); } if (!nullToAbsent || totalUnreadCount != null) { map['total_unread_count'] = Variable(totalUnreadCount); @@ -8458,8 +9059,7 @@ class ConnectionEventEntity extends DataClass return map; } - factory ConnectionEventEntity.fromJson(Map json, - {ValueSerializer? serializer}) { + factory ConnectionEventEntity.fromJson(Map json, {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return ConnectionEventEntity( id: serializer.fromJson(json['id']), @@ -8485,41 +9085,32 @@ class ConnectionEventEntity extends DataClass }; } - 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 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, + 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, ); } @@ -8538,8 +9129,7 @@ class ConnectionEventEntity extends DataClass } @override - int get hashCode => Object.hash(id, type, ownUser, totalUnreadCount, - unreadChannels, lastEventAt, lastSyncAt); + int get hashCode => Object.hash(id, type, ownUser, totalUnreadCount, unreadChannels, lastEventAt, lastSyncAt); @override bool operator ==(Object other) => identical(this, other) || @@ -8599,14 +9189,15 @@ class ConnectionEventsCompanion extends UpdateCompanion { }); } - ConnectionEventsCompanion copyWith( - {Value? id, - Value? type, - Value?>? ownUser, - Value? totalUnreadCount, - Value? unreadChannels, - Value? lastEventAt, - Value? lastSyncAt}) { + 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, @@ -8628,8 +9219,7 @@ class ConnectionEventsCompanion extends UpdateCompanion { map['type'] = Variable(type.value); } if (ownUser.present) { - map['own_user'] = Variable( - $ConnectionEventsTable.$converterownUsern.toSql(ownUser.value)); + map['own_user'] = Variable($ConnectionEventsTable.$converterownUsern.toSql(ownUser.value)); } if (totalUnreadCount.present) { map['total_unread_count'] = Variable(totalUnreadCount.value); @@ -8667,222 +9257,239 @@ abstract class _$DriftChatDatabase extends GeneratedDatabase { 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 $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 $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 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); + late final ChannelQueryDao channelQueryDao = ChannelQueryDao(this as DriftChatDatabase); + late final ConnectionEventDao connectionEventDao = ConnectionEventDao(this as DriftChatDatabase); @override - Iterable> get allTables => - allSchemaEntities.whereType>(); + Iterable> get allTables => allSchemaEntities.whereType>(); @override List get allSchemaEntities => [ - channels, - messages, - draftMessages, - pinnedMessages, - polls, - pollVotes, - pinnedMessageReactions, - reactions, - users, - members, - reads, - channelQueries, - connectionEvents - ]; + channels, + messages, + draftMessages, + locations, + 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), - ], - ), - WritePropagation( - on: TableUpdateQuery.onTableName('channels', - limitUpdateKind: UpdateKind.delete), - result: [ - TableUpdate('reads', kind: UpdateKind.delete), - ], - ), + [ + 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), + ], + ), + ], + ); } -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 messageCount, - Value createdById, - Value?> filterTags, - 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 messageCount, - Value createdById, - Value?> filterTags, - Value?> extraData, - Value rowid, -}); - -final class $$ChannelsTableReferences - extends BaseReferences<_$DriftChatDatabase, $ChannelsTable, ChannelEntity> { +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 messageCount, + Value createdById, + Value?> filterTags, + 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 messageCount, + Value createdById, + Value?> filterTags, + Value?> extraData, + Value rowid, + }); + +final class $$ChannelsTableReferences extends BaseReferences<_$DriftChatDatabase, $ChannelsTable, ChannelEntity> { $$ChannelsTableReferences(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 MultiTypedResultKey<$MessagesTable, List> _messagesRefsTable(_$DriftChatDatabase db) => + MultiTypedResultKey.fromTable( + db.messages, + aliasName: $_aliasNameGenerator(db.channels.cid, db.messages.channelCid), + ); $$MessagesTableProcessedTableManager get messagesRefs { - final manager = $$MessagesTableTableManager($_db, $_db.messages) - .filter((f) => f.channelCid.cid($_item.cid)); + 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)); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: cache)); } - static MultiTypedResultKey<$DraftMessagesTable, List> - _draftMessagesRefsTable(_$DriftChatDatabase db) => - MultiTypedResultKey.fromTable(db.draftMessages, - aliasName: $_aliasNameGenerator( - db.channels.cid, db.draftMessages.channelCid)); + 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($_item.cid)); + 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)); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$LocationsTable, List> _locationsRefsTable(_$DriftChatDatabase db) => + MultiTypedResultKey.fromTable( + db.locations, + aliasName: $_aliasNameGenerator(db.channels.cid, db.locations.channelCid), + ); + + $$LocationsTableProcessedTableManager get locationsRefs { + final manager = $$LocationsTableTableManager( + $_db, + $_db.locations, + ).filter((f) => f.channelCid.cid.sqlEquals($_itemColumn('cid')!)); + + final cache = $_typedResult.readTableOrNull(_locationsRefsTable($_db)); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: cache)); } - static MultiTypedResultKey<$MembersTable, List> - _membersRefsTable(_$DriftChatDatabase db) => - MultiTypedResultKey.fromTable(db.members, - aliasName: - $_aliasNameGenerator(db.channels.cid, db.members.channelCid)); + 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($_item.cid)); + 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)); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: cache)); } - static MultiTypedResultKey<$ReadsTable, List> _readsRefsTable( - _$DriftChatDatabase db) => - MultiTypedResultKey.fromTable(db.reads, - aliasName: - $_aliasNameGenerator(db.channels.cid, db.reads.channelCid)); + 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($_item.cid)); + 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)); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: cache)); } } -class $$ChannelsTableFilterComposer - extends Composer<_$DriftChatDatabase, $ChannelsTable> { +class $$ChannelsTableFilterComposer extends Composer<_$DriftChatDatabase, $ChannelsTable> { $$ChannelsTableFilterComposer({ required super.$db, required super.$table, @@ -8890,148 +9497,140 @@ class $$ChannelsTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get type => $composableBuilder( - column: $table.type, 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)); + 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?, 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)); + 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 frozen => + $composableBuilder(column: $table.frozen, builder: (column) => ColumnFilters(column)); - ColumnFilters get lastMessageAt => $composableBuilder( - column: $table.lastMessageAt, 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 createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get updatedAt => $composableBuilder( - column: $table.updatedAt, 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 deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get memberCount => $composableBuilder( - column: $table.memberCount, builder: (column) => ColumnFilters(column)); + ColumnFilters get memberCount => + $composableBuilder(column: $table.memberCount, builder: (column) => ColumnFilters(column)); - ColumnFilters get messageCount => $composableBuilder( - column: $table.messageCount, builder: (column) => ColumnFilters(column)); + ColumnFilters get messageCount => + $composableBuilder(column: $table.messageCount, builder: (column) => ColumnFilters(column)); - ColumnFilters get createdById => $composableBuilder( - column: $table.createdById, builder: (column) => ColumnFilters(column)); + ColumnFilters get createdById => + $composableBuilder(column: $table.createdById, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, List, String> - get filterTags => $composableBuilder( - column: $table.filterTags, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, List, String> get filterTags => + $composableBuilder(column: $table.filterTags, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnWithTypeConverterFilters?, Map, - String> - get extraData => $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, Map, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnWithTypeConverterFilters(column)); - Expression messagesRefs( - Expression Function($$MessagesTableFilterComposer f) f) { + 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, - )); + 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); } - Expression draftMessagesRefs( - Expression Function($$DraftMessagesTableFilterComposer f) f) { + 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, - )); + 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); + } + + 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); } - Expression membersRefs( - Expression Function($$MembersTableFilterComposer f) f) { + 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, - )); + 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) { + 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, - )); + 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 $$ChannelsTableOrderingComposer - extends Composer<_$DriftChatDatabase, $ChannelsTable> { +class $$ChannelsTableOrderingComposer extends Composer<_$DriftChatDatabase, $ChannelsTable> { $$ChannelsTableOrderingComposer({ required super.$db, required super.$table, @@ -9039,57 +9638,52 @@ class $$ChannelsTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get type => $composableBuilder( - column: $table.type, 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 cid => + $composableBuilder(column: $table.cid, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get ownCapabilities => $composableBuilder( - column: $table.ownCapabilities, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get ownCapabilities => + $composableBuilder(column: $table.ownCapabilities, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get config => $composableBuilder( - column: $table.config, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get config => + $composableBuilder(column: $table.config, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get frozen => $composableBuilder( - column: $table.frozen, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get frozen => + $composableBuilder(column: $table.frozen, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get lastMessageAt => $composableBuilder( - column: $table.lastMessageAt, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastMessageAt => + $composableBuilder(column: $table.lastMessageAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, 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 updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get deletedAt => $composableBuilder( - column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get memberCount => $composableBuilder( - column: $table.memberCount, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get memberCount => + $composableBuilder(column: $table.memberCount, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get messageCount => $composableBuilder( - column: $table.messageCount, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get messageCount => + $composableBuilder(column: $table.messageCount, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdById => $composableBuilder( - column: $table.createdById, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdById => + $composableBuilder(column: $table.createdById, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get filterTags => $composableBuilder( - column: $table.filterTags, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get filterTags => + $composableBuilder(column: $table.filterTags, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnOrderings(column)); } -class $$ChannelsTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $ChannelsTable> { +class $$ChannelsTableAnnotationComposer extends Composer<_$DriftChatDatabase, $ChannelsTable> { $$ChannelsTableAnnotationComposer({ required super.$db, required super.$table, @@ -9097,446 +9691,470 @@ class $$ChannelsTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get type => - $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); - GeneratedColumn get cid => - $composableBuilder(column: $table.cid, builder: (column) => column); + GeneratedColumn get cid => $composableBuilder(column: $table.cid, builder: (column) => column); GeneratedColumnWithTypeConverter?, String> get ownCapabilities => - $composableBuilder( - column: $table.ownCapabilities, builder: (column) => column); + $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 frozen => $composableBuilder(column: $table.frozen, builder: (column) => column); - GeneratedColumn get lastMessageAt => $composableBuilder( - column: $table.lastMessageAt, 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 createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); - GeneratedColumn get updatedAt => - $composableBuilder(column: $table.updatedAt, 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 deletedAt => $composableBuilder(column: $table.deletedAt, builder: (column) => column); - GeneratedColumn get memberCount => $composableBuilder( - column: $table.memberCount, builder: (column) => column); + GeneratedColumn get memberCount => $composableBuilder(column: $table.memberCount, builder: (column) => column); - GeneratedColumn get messageCount => $composableBuilder( - column: $table.messageCount, builder: (column) => column); + GeneratedColumn get messageCount => $composableBuilder(column: $table.messageCount, builder: (column) => column); - GeneratedColumn get createdById => $composableBuilder( - column: $table.createdById, builder: (column) => column); + GeneratedColumn get createdById => + $composableBuilder(column: $table.createdById, builder: (column) => column); GeneratedColumnWithTypeConverter?, String> get filterTags => - $composableBuilder( - column: $table.filterTags, builder: (column) => column); + $composableBuilder(column: $table.filterTags, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> - get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => column); - Expression messagesRefs( - Expression Function($$MessagesTableAnnotationComposer a) f) { + 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, - )); + 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); } Expression draftMessagesRefs( - Expression Function($$DraftMessagesTableAnnotationComposer a) f) { + 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, - )); + 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); + } + + 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); } - Expression membersRefs( - Expression Function($$MembersTableAnnotationComposer a) f) { + 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, - )); + 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) { + 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, - )); + 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); } } -class $$ChannelsTableTableManager extends RootTableManager< - _$DriftChatDatabase, - $ChannelsTable, - ChannelEntity, - $$ChannelsTableFilterComposer, - $$ChannelsTableOrderingComposer, - $$ChannelsTableAnnotationComposer, - $$ChannelsTableCreateCompanionBuilder, - $$ChannelsTableUpdateCompanionBuilder, - (ChannelEntity, $$ChannelsTableReferences), - ChannelEntity, - PrefetchHooks Function( - {bool messagesRefs, - bool draftMessagesRefs, - bool membersRefs, - bool readsRefs})> { +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( + : 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 messageCount = const Value.absent(), - Value createdById = const Value.absent(), - Value?> filterTags = 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, - messageCount: messageCount, - createdById: createdById, - filterTags: filterTags, - 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 messageCount = const Value.absent(), - Value createdById = const Value.absent(), - Value?> filterTags = 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, - messageCount: messageCount, - createdById: createdById, - filterTags: filterTags, - extraData: extraData, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => - (e.readTable(table), $$ChannelsTableReferences(db, table, e))) - .toList(), - prefetchHooksCallback: ( - {messagesRefs = false, - draftMessagesRefs = false, - membersRefs = false, - readsRefs = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [ - if (messagesRefs) db.messages, - if (draftMessagesRefs) db.draftMessages, - 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 (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) - ]; + 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 messageCount = const Value.absent(), + Value createdById = const Value.absent(), + Value?> filterTags = 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, + messageCount: messageCount, + createdById: createdById, + filterTags: filterTags, + 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 messageCount = const Value.absent(), + Value createdById = const Value.absent(), + Value?> filterTags = 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, + messageCount: messageCount, + createdById: createdById, + filterTags: filterTags, + 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 $$ChannelsTableProcessedTableManager = ProcessedTableManager< - _$DriftChatDatabase, - $ChannelsTable, - ChannelEntity, - $$ChannelsTableFilterComposer, - $$ChannelsTableOrderingComposer, - $$ChannelsTableAnnotationComposer, - $$ChannelsTableCreateCompanionBuilder, - $$ChannelsTableUpdateCompanionBuilder, - (ChannelEntity, $$ChannelsTableReferences), - ChannelEntity, - PrefetchHooks Function( - {bool messagesRefs, +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, - 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 channelRole, - Value pinned, - Value pinnedAt, - Value pinExpires, - Value pinnedByUserId, - required String channelCid, - Value?> i18n, - Value?> restrictedVisibility, - Value?> extraData, - Value rowid, -}); -typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion 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 channelRole, - Value pinned, - Value pinnedAt, - Value pinExpires, - Value pinnedByUserId, - Value channelCid, - Value?> i18n, - Value?> restrictedVisibility, - Value?> extraData, - Value rowid, -}); - -final class $$MessagesTableReferences - extends BaseReferences<_$DriftChatDatabase, $MessagesTable, MessageEntity> { + bool readsRefs, + }) + >; +typedef $$MessagesTableCreateCompanionBuilder = + MessagesCompanion 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 deletedForMe, + Value messageTextUpdatedAt, + Value userId, + Value channelRole, + Value pinned, + Value pinnedAt, + Value pinExpires, + Value pinnedByUserId, + required String channelCid, + Value?> i18n, + Value?> restrictedVisibility, + Value?> extraData, + Value rowid, + }); +typedef $$MessagesTableUpdateCompanionBuilder = + MessagesCompanion 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 deletedForMe, + Value messageTextUpdatedAt, + Value userId, + Value channelRole, + Value pinned, + Value pinnedAt, + Value pinExpires, + Value pinnedByUserId, + 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); static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => - db.channels.createAlias( - $_aliasNameGenerator(db.messages.channelCid, db.channels.cid)); + db.channels.createAlias($_aliasNameGenerator(db.messages.channelCid, db.channels.cid)); $$ChannelsTableProcessedTableManager get channelCid { - final manager = $$ChannelsTableTableManager($_db, $_db.channels) - .filter((f) => f.cid($_item.channelCid!)); + final $_column = $_itemColumn('channel_cid')!; + + 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])); + 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)); + 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($_item.id)); + 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)); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: cache)); + } + + static MultiTypedResultKey<$LocationsTable, List> _locationsRefsTable(_$DriftChatDatabase db) => + MultiTypedResultKey.fromTable( + db.locations, + aliasName: $_aliasNameGenerator(db.messages.id, db.locations.messageId), + ); + + $$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)); + 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($_item.id)); + 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)); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: cache)); } } -class $$MessagesTableFilterComposer - extends Composer<_$DriftChatDatabase, $MessagesTable> { +class $$MessagesTableFilterComposer extends Composer<_$DriftChatDatabase, $MessagesTable> { $$MessagesTableFilterComposer({ required super.$db, required super.$table, @@ -9544,185 +10162,173 @@ class $$MessagesTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get messageText => $composableBuilder( - column: $table.messageText, 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)); + 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 state => + $composableBuilder(column: $table.state, builder: (column) => ColumnFilters(column)); - ColumnFilters get type => $composableBuilder( - column: $table.type, 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, 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)); + 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 parentId => + $composableBuilder(column: $table.parentId, builder: (column) => ColumnFilters(column)); - ColumnFilters get quotedMessageId => $composableBuilder( - column: $table.quotedMessageId, - 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 pollId => + $composableBuilder(column: $table.pollId, builder: (column) => ColumnFilters(column)); - ColumnFilters get replyCount => $composableBuilder( - column: $table.replyCount, 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 showInChannel => + $composableBuilder(column: $table.showInChannel, builder: (column) => ColumnFilters(column)); - ColumnFilters get shadowed => $composableBuilder( - column: $table.shadowed, 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 command => + $composableBuilder(column: $table.command, builder: (column) => ColumnFilters(column)); - ColumnFilters get localCreatedAt => $composableBuilder( - column: $table.localCreatedAt, - 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 remoteCreatedAt => + $composableBuilder(column: $table.remoteCreatedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get localUpdatedAt => $composableBuilder( - column: $table.localUpdatedAt, - 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 remoteUpdatedAt => + $composableBuilder(column: $table.remoteUpdatedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get localDeletedAt => $composableBuilder( - column: $table.localDeletedAt, - 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 remoteDeletedAt => + $composableBuilder(column: $table.remoteDeletedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get messageTextUpdatedAt => $composableBuilder( - column: $table.messageTextUpdatedAt, - builder: (column) => ColumnFilters(column)); + ColumnFilters get deletedForMe => + $composableBuilder(column: $table.deletedForMe, builder: (column) => ColumnFilters(column)); - ColumnFilters get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnFilters(column)); + ColumnFilters get messageTextUpdatedAt => + $composableBuilder(column: $table.messageTextUpdatedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get channelRole => $composableBuilder( - column: $table.channelRole, 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 channelRole => + $composableBuilder(column: $table.channelRole, builder: (column) => ColumnFilters(column)); - ColumnFilters get pinnedAt => $composableBuilder( - column: $table.pinnedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get pinned => + $composableBuilder(column: $table.pinned, builder: (column) => ColumnFilters(column)); - ColumnFilters get pinExpires => $composableBuilder( - column: $table.pinExpires, builder: (column) => ColumnFilters(column)); + ColumnFilters get pinnedAt => + $composableBuilder(column: $table.pinnedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get pinnedByUserId => $composableBuilder( - column: $table.pinnedByUserId, - builder: (column) => ColumnFilters(column)); + ColumnFilters get pinExpires => + $composableBuilder(column: $table.pinExpires, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, Map, - String> - get i18n => $composableBuilder( - column: $table.i18n, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get pinnedByUserId => + $composableBuilder(column: $table.pinnedByUserId, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, List, String> - get restrictedVisibility => $composableBuilder( - column: $table.restrictedVisibility, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, Map, String> get i18n => + $composableBuilder(column: $table.i18n, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnWithTypeConverterFilters?, Map, - String> - get extraData => $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, List, String> get restrictedVisibility => $composableBuilder( + column: $table.restrictedVisibility, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + + ColumnWithTypeConverterFilters?, Map, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnWithTypeConverterFilters(column)); $$ChannelsTableFilterComposer get channelCid { final $$ChannelsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableFilterComposer( - $db: $db, - $table: $db.channels, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$ChannelsTableFilterComposer( + $db: $db, + $table: $db.channels, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } - Expression draftMessagesRefs( - Expression Function($$DraftMessagesTableFilterComposer f) f) { + 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, - )); + 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 locationsRefs(Expression Function($$LocationsTableFilterComposer f) f) { + final $$LocationsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.locations, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$LocationsTableFilterComposer( + $db: $db, + $table: $db.locations, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return f(composer); } - Expression reactionsRefs( - Expression Function($$ReactionsTableFilterComposer f) f) { + Expression reactionsRefs(Expression Function($$ReactionsTableFilterComposer f) f) { final $$ReactionsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.reactions, - getReferencedColumn: (t) => t.messageId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ReactionsTableFilterComposer( - $db: $db, - $table: $db.reactions, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.reactions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$ReactionsTableFilterComposer( + $db: $db, + $table: $db.reactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return f(composer); } } -class $$MessagesTableOrderingComposer - extends Composer<_$DriftChatDatabase, $MessagesTable> { +class $$MessagesTableOrderingComposer extends Composer<_$DriftChatDatabase, $MessagesTable> { $$MessagesTableOrderingComposer({ required super.$db, required super.$table, @@ -9730,132 +10336,118 @@ class $$MessagesTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get messageText => $composableBuilder( - column: $table.messageText, 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 attachments => + $composableBuilder(column: $table.attachments, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get state => $composableBuilder( - column: $table.state, 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 type => + $composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get mentionedUsers => $composableBuilder( - column: $table.mentionedUsers, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get mentionedUsers => + $composableBuilder(column: $table.mentionedUsers, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get reactionGroups => $composableBuilder( - column: $table.reactionGroups, - 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 parentId => + $composableBuilder(column: $table.parentId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get quotedMessageId => $composableBuilder( - column: $table.quotedMessageId, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get quotedMessageId => + $composableBuilder(column: $table.quotedMessageId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pollId => $composableBuilder( - column: $table.pollId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get pollId => + $composableBuilder(column: $table.pollId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get replyCount => $composableBuilder( - column: $table.replyCount, 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 showInChannel => + $composableBuilder(column: $table.showInChannel, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get shadowed => $composableBuilder( - column: $table.shadowed, 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 command => + $composableBuilder(column: $table.command, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get localCreatedAt => $composableBuilder( - column: $table.localCreatedAt, - 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 remoteCreatedAt => + $composableBuilder(column: $table.remoteCreatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get localUpdatedAt => $composableBuilder( - column: $table.localUpdatedAt, - 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 remoteUpdatedAt => + $composableBuilder(column: $table.remoteUpdatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get localDeletedAt => $composableBuilder( - column: $table.localDeletedAt, - 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 remoteDeletedAt => + $composableBuilder(column: $table.remoteDeletedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get messageTextUpdatedAt => $composableBuilder( - column: $table.messageTextUpdatedAt, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get deletedForMe => + $composableBuilder(column: $table.deletedForMe, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get messageTextUpdatedAt => + $composableBuilder(column: $table.messageTextUpdatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get channelRole => $composableBuilder( - column: $table.channelRole, 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 channelRole => + $composableBuilder(column: $table.channelRole, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pinnedAt => $composableBuilder( - column: $table.pinnedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get pinned => + $composableBuilder(column: $table.pinned, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pinExpires => $composableBuilder( - column: $table.pinExpires, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get pinnedAt => + $composableBuilder(column: $table.pinnedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pinnedByUserId => $composableBuilder( - column: $table.pinnedByUserId, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get pinExpires => + $composableBuilder(column: $table.pinExpires, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get i18n => $composableBuilder( - column: $table.i18n, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get pinnedByUserId => + $composableBuilder(column: $table.pinnedByUserId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get restrictedVisibility => $composableBuilder( - column: $table.restrictedVisibility, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get i18n => + $composableBuilder(column: $table.i18n, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, 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, - )); + 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 $$MessagesTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $MessagesTable> { +class $$MessagesTableAnnotationComposer extends Composer<_$DriftChatDatabase, $MessagesTable> { $$MessagesTableAnnotationComposer({ required super.$db, required super.$table, @@ -9863,341 +10455,817 @@ class $$MessagesTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get messageText => $composableBuilder( - column: $table.messageText, builder: (column) => column); + GeneratedColumn get messageText => + $composableBuilder(column: $table.messageText, builder: (column) => column); GeneratedColumnWithTypeConverter, String> get attachments => - $composableBuilder( - column: $table.attachments, builder: (column) => column); + $composableBuilder(column: $table.attachments, builder: (column) => column); - GeneratedColumn get state => - $composableBuilder(column: $table.state, 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 type => $composableBuilder(column: $table.type, builder: (column) => column); GeneratedColumnWithTypeConverter, String> get mentionedUsers => - $composableBuilder( - column: $table.mentionedUsers, builder: (column) => column); + $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); - GeneratedColumnWithTypeConverter?, String> - get reactionGroups => $composableBuilder( - column: $table.reactionGroups, builder: (column) => column); + GeneratedColumn get pollId => $composableBuilder(column: $table.pollId, builder: (column) => column); - GeneratedColumn get parentId => - $composableBuilder(column: $table.parentId, builder: (column) => column); + GeneratedColumn get replyCount => $composableBuilder(column: $table.replyCount, builder: (column) => column); - GeneratedColumn get quotedMessageId => $composableBuilder( - column: $table.quotedMessageId, builder: (column) => column); + GeneratedColumn get showInChannel => + $composableBuilder(column: $table.showInChannel, builder: (column) => column); - GeneratedColumn get pollId => - $composableBuilder(column: $table.pollId, builder: (column) => column); + GeneratedColumn get shadowed => $composableBuilder(column: $table.shadowed, builder: (column) => column); - GeneratedColumn get replyCount => $composableBuilder( - column: $table.replyCount, builder: (column) => column); + GeneratedColumn get command => $composableBuilder(column: $table.command, builder: (column) => column); - GeneratedColumn get showInChannel => $composableBuilder( - column: $table.showInChannel, builder: (column) => column); + GeneratedColumn get localCreatedAt => + $composableBuilder(column: $table.localCreatedAt, builder: (column) => column); - GeneratedColumn get shadowed => - $composableBuilder(column: $table.shadowed, builder: (column) => column); + GeneratedColumn get remoteCreatedAt => + $composableBuilder(column: $table.remoteCreatedAt, builder: (column) => column); - GeneratedColumn get command => - $composableBuilder(column: $table.command, builder: (column) => column); + GeneratedColumn get localUpdatedAt => + $composableBuilder(column: $table.localUpdatedAt, builder: (column) => column); - GeneratedColumn get localCreatedAt => $composableBuilder( - column: $table.localCreatedAt, builder: (column) => column); + GeneratedColumn get remoteUpdatedAt => + $composableBuilder(column: $table.remoteUpdatedAt, builder: (column) => column); - GeneratedColumn get remoteCreatedAt => $composableBuilder( - column: $table.remoteCreatedAt, builder: (column) => column); + GeneratedColumn get localDeletedAt => + $composableBuilder(column: $table.localDeletedAt, builder: (column) => column); - GeneratedColumn get localUpdatedAt => $composableBuilder( - column: $table.localUpdatedAt, builder: (column) => column); + GeneratedColumn get remoteDeletedAt => + $composableBuilder(column: $table.remoteDeletedAt, builder: (column) => column); + + GeneratedColumn get deletedForMe => + $composableBuilder(column: $table.deletedForMe, 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 channelRole => + $composableBuilder(column: $table.channelRole, 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); + + $$ChannelsTableAnnotationComposer get channelCid { + final $$ChannelsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$ChannelsTableAnnotationComposer( + $db: $db, + $table: $db.channels, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); + 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 locationsRefs(Expression Function($$LocationsTableAnnotationComposer a) f) { + final $$LocationsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.locations, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$LocationsTableAnnotationComposer( + $db: $db, + $table: $db.locations, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression reactionsRefs(Expression Function($$ReactionsTableAnnotationComposer a) f) { + final $$ReactionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.reactions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$ReactionsTableAnnotationComposer( + $db: $db, + $table: $db.reactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$MessagesTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $MessagesTable, + MessageEntity, + $$MessagesTableFilterComposer, + $$MessagesTableOrderingComposer, + $$MessagesTableAnnotationComposer, + $$MessagesTableCreateCompanionBuilder, + $$MessagesTableUpdateCompanionBuilder, + (MessageEntity, $$MessagesTableReferences), + MessageEntity, + PrefetchHooks Function({bool channelCid, bool draftMessagesRefs, bool locationsRefs, bool reactionsRefs}) + > { + $$MessagesTableTableManager(_$DriftChatDatabase db, $MessagesTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => $$MessagesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$MessagesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$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> 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 deletedForMe = const Value.absent(), + Value messageTextUpdatedAt = const Value.absent(), + Value userId = const Value.absent(), + Value channelRole = 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(), + }) => MessagesCompanion( + 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, + deletedForMe: deletedForMe, + messageTextUpdatedAt: messageTextUpdatedAt, + userId: userId, + channelRole: channelRole, + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedByUserId: pinnedByUserId, + channelCid: channelCid, + i18n: i18n, + restrictedVisibility: restrictedVisibility, + extraData: extraData, + rowid: rowid, + ), + createCompanionCallback: + ({ + 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 deletedForMe = const Value.absent(), + Value messageTextUpdatedAt = const Value.absent(), + Value userId = const Value.absent(), + Value channelRole = 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(), + }) => MessagesCompanion.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, + deletedForMe: deletedForMe, + messageTextUpdatedAt: messageTextUpdatedAt, + userId: userId, + channelRole: channelRole, + 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), $$MessagesTableReferences(db, table, e))).toList(), + prefetchHooksCallback: + ({channelCid = false, draftMessagesRefs = false, locationsRefs = false, reactionsRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (draftMessagesRefs) db.draftMessages, + if (locationsRefs) db.locations, + if (reactionsRefs) db.reactions, + ], + 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 (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 (locationsRefs) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$MessagesTableReferences._locationsRefsTable(db), + managerFromTypedResult: (p0) => $$MessagesTableReferences(db, table, p0).locationsRefs, + referencedItemsForCurrentItem: (item, referencedItems) => + referencedItems.where((e) => e.messageId == 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, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$MessagesTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $MessagesTable, + MessageEntity, + $$MessagesTableFilterComposer, + $$MessagesTableOrderingComposer, + $$MessagesTableAnnotationComposer, + $$MessagesTableCreateCompanionBuilder, + $$MessagesTableUpdateCompanionBuilder, + (MessageEntity, $$MessagesTableReferences), + MessageEntity, + PrefetchHooks Function({bool channelCid, bool draftMessagesRefs, bool locationsRefs, 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, + Value createdAt, + required String channelCid, + Value?> extraData, + 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, + Value createdAt, + Value channelCid, + Value?> extraData, + Value rowid, + }); + +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.draftMessages.channelCid, db.channels.cid)); + + $$ChannelsTableProcessedTableManager get channelCid { + final $_column = $_itemColumn('channel_cid')!; + + 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])); + } +} + +class $$DraftMessagesTableFilterComposer extends Composer<_$DriftChatDatabase, $DraftMessagesTable> { + $$DraftMessagesTableFilterComposer({ + 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 type => $composableBuilder(column: $table.type, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters, List, String> get mentionedUsers => + $composableBuilder(column: $table.mentionedUsers, builder: (column) => ColumnWithTypeConverterFilters(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 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 createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters?, Map, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnWithTypeConverterFilters(column)); + + $$MessagesTableFilterComposer get parentId { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.parentId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$ChannelsTableFilterComposer get channelCid { + final $$ChannelsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$ChannelsTableFilterComposer( + $db: $db, + $table: $db.channels, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$DraftMessagesTableOrderingComposer extends Composer<_$DriftChatDatabase, $DraftMessagesTable> { + $$DraftMessagesTableOrderingComposer({ + 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 quotedMessageId => + $composableBuilder(column: $table.quotedMessageId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get pollId => + $composableBuilder(column: $table.pollId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get showInChannel => + $composableBuilder(column: $table.showInChannel, 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 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, + 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; + } +} - GeneratedColumn get remoteUpdatedAt => $composableBuilder( - column: $table.remoteUpdatedAt, builder: (column) => column); +class $$DraftMessagesTableAnnotationComposer extends Composer<_$DriftChatDatabase, $DraftMessagesTable> { + $$DraftMessagesTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get localDeletedAt => $composableBuilder( - column: $table.localDeletedAt, builder: (column) => column); + GeneratedColumn get messageText => + $composableBuilder(column: $table.messageText, builder: (column) => column); - GeneratedColumn get remoteDeletedAt => $composableBuilder( - column: $table.remoteDeletedAt, builder: (column) => column); + GeneratedColumnWithTypeConverter, String> get attachments => + $composableBuilder(column: $table.attachments, builder: (column) => column); - GeneratedColumn get messageTextUpdatedAt => $composableBuilder( - column: $table.messageTextUpdatedAt, builder: (column) => column); + GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); - GeneratedColumn get userId => - $composableBuilder(column: $table.userId, builder: (column) => column); + GeneratedColumnWithTypeConverter, String> get mentionedUsers => + $composableBuilder(column: $table.mentionedUsers, builder: (column) => column); - GeneratedColumn get channelRole => $composableBuilder( - column: $table.channelRole, builder: (column) => column); + GeneratedColumn get quotedMessageId => + $composableBuilder(column: $table.quotedMessageId, builder: (column) => column); - GeneratedColumn get pinned => - $composableBuilder(column: $table.pinned, builder: (column) => column); + GeneratedColumn get pollId => $composableBuilder(column: $table.pollId, builder: (column) => column); - GeneratedColumn get pinnedAt => - $composableBuilder(column: $table.pinnedAt, builder: (column) => column); + GeneratedColumn get showInChannel => + $composableBuilder(column: $table.showInChannel, builder: (column) => column); - GeneratedColumn get pinExpires => $composableBuilder( - column: $table.pinExpires, builder: (column) => column); + GeneratedColumn get command => $composableBuilder(column: $table.command, builder: (column) => column); - GeneratedColumn get pinnedByUserId => $composableBuilder( - column: $table.pinnedByUserId, builder: (column) => column); + GeneratedColumn get silent => $composableBuilder(column: $table.silent, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> get i18n => - $composableBuilder(column: $table.i18n, builder: (column) => column); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, 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); - GeneratedColumnWithTypeConverter?, String> - get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => column); + $$MessagesTableAnnotationComposer get parentId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.parentId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } $$ChannelsTableAnnotationComposer get channelCid { final $$ChannelsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableAnnotationComposer( - $db: $db, - $table: $db.channels, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$ChannelsTableAnnotationComposer( + $db: $db, + $table: $db.channels, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); 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( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.reactions, - getReferencedColumn: (t) => t.messageId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ReactionsTableAnnotationComposer( - $db: $db, - $table: $db.reactions, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); - return f(composer); - } } -class $$MessagesTableTableManager 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) - : super(TableManagerState( +class $$DraftMessagesTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $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), - createOrderingComposer: () => - $$MessagesTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$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> 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 channelRole = 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(), - }) => - MessagesCompanion( - 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, - channelRole: channelRole, - pinned: pinned, - pinnedAt: pinnedAt, - pinExpires: pinExpires, - pinnedByUserId: pinnedByUserId, - channelCid: channelCid, - i18n: i18n, - restrictedVisibility: restrictedVisibility, - extraData: extraData, - rowid: rowid, - ), - createCompanionCallback: ({ - 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 channelRole = 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(), - }) => - MessagesCompanion.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, - channelRole: channelRole, - 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), $$MessagesTableReferences(db, table, e))) - .toList(), - prefetchHooksCallback: ( - {channelCid = false, - draftMessagesRefs = false, - reactionsRefs = false}) { + createFilteringComposer: () => $$DraftMessagesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$DraftMessagesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$DraftMessagesTableAnnotationComposer($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 createdAt = const Value.absent(), + Value channelCid = const Value.absent(), + Value?> extraData = 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, + channelCid: channelCid, + extraData: extraData, + 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 createdAt = const Value.absent(), + required String channelCid, + Value?> extraData = 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, + channelCid: channelCid, + extraData: extraData, + rowid: rowid, + ), + withReferenceMapper: (p0) => + p0.map((e) => (e.readTable(table), $$DraftMessagesTableReferences(db, table, e))).toList(), + prefetchHooksCallback: ({parentId = false, channelCid = false}) { return PrefetchHooks( db: db, - explicitlyWatchedTables: [ - if (draftMessagesRefs) db.draftMessages, - if (reactionsRefs) db.reactions - ], - addJoins: < - T extends TableManagerState< + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< dynamic, dynamic, dynamic, @@ -10208,511 +11276,382 @@ class $$MessagesTableTableManager extends RootTableManager< 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; - }, + 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: $$DraftMessagesTableReferences._channelCidTable(db), + referencedColumn: $$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< - _$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, - Value createdAt, - required String channelCid, - Value?> extraData, - 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, - Value createdAt, - Value channelCid, - Value?> extraData, - Value rowid, -}); - -final class $$DraftMessagesTableReferences extends BaseReferences< - _$DriftChatDatabase, $DraftMessagesTable, DraftMessageEntity> { - $$DraftMessagesTableReferences( - super.$_db, super.$_table, super.$_typedResult); +typedef $$DraftMessagesTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $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, + Value updatedAt, + Value rowid, + }); +typedef $$LocationsTableUpdateCompanionBuilder = + LocationsCompanion Function({ + Value channelCid, + Value messageId, + Value userId, + Value latitude, + Value longitude, + Value createdByDeviceId, + Value endAt, + Value createdAt, + Value updatedAt, + Value rowid, + }); - static $MessagesTable _parentIdTable(_$DriftChatDatabase db) => - db.messages.createAlias( - $_aliasNameGenerator(db.draftMessages.parentId, db.messages.id)); +final class $$LocationsTableReferences extends BaseReferences<_$DriftChatDatabase, $LocationsTable, LocationEntity> { + $$LocationsTableReferences(super.$_db, super.$_table, super.$_typedResult); - $$MessagesTableProcessedTableManager? get parentId { - if ($_item.parentId == null) return null; - final manager = $$MessagesTableTableManager($_db, $_db.messages) - .filter((f) => f.id($_item.parentId!)); - final item = $_typedResult.readTableOrNull(_parentIdTable($_db)); + static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => + db.channels.createAlias($_aliasNameGenerator(db.locations.channelCid, db.channels.cid)); + + $$ChannelsTableProcessedTableManager? get channelCid { + final $_column = $_itemColumn('channel_cid'); + if ($_column == null) return null; + 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])); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: [item])); } - static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => - db.channels.createAlias( - $_aliasNameGenerator(db.draftMessages.channelCid, db.channels.cid)); + static $MessagesTable _messageIdTable(_$DriftChatDatabase db) => + db.messages.createAlias($_aliasNameGenerator(db.locations.messageId, db.messages.id)); - $$ChannelsTableProcessedTableManager get channelCid { - final manager = $$ChannelsTableTableManager($_db, $_db.channels) - .filter((f) => f.cid($_item.channelCid!)); - 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])); + 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, - builder: (column) => ColumnFilters(column)); + ColumnFilters get createdByDeviceId => + $composableBuilder(column: $table.createdByDeviceId, builder: (column) => ColumnFilters(column)); - ColumnFilters get pollId => $composableBuilder( - column: $table.pollId, builder: (column) => ColumnFilters(column)); + ColumnFilters get endAt => + $composableBuilder(column: $table.endAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get showInChannel => $composableBuilder( - column: $table.showInChannel, builder: (column) => ColumnFilters(column)); + ColumnFilters get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get command => $composableBuilder( - column: $table.command, builder: (column) => ColumnFilters(column)); + ColumnFilters get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get silent => $composableBuilder( - column: $table.silent, 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)); - - $$MessagesTableFilterComposer get parentId { - final $$MessagesTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.parentId, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$MessagesTableFilterComposer( - $db: $db, - $table: $db.messages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + $$ChannelsTableFilterComposer get channelCid { + final $$ChannelsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$ChannelsTableFilterComposer( + $db: $db, + $table: $db.channels, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } - $$ChannelsTableFilterComposer get channelCid { - final $$ChannelsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableFilterComposer( - $db: $db, - $table: $db.channels, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + $$MessagesTableFilterComposer get messageId { + final $$MessagesTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } -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, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdByDeviceId => + $composableBuilder(column: $table.createdByDeviceId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get command => $composableBuilder( - column: $table.command, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get endAt => + $composableBuilder(column: $table.endAt, 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 createdAt => $composableBuilder( - column: $table.createdAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get updatedAt => + $composableBuilder(column: $table.updatedAt, 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, - )); + $$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; } - $$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, - )); + $$MessagesTableOrderingComposer get messageId { + final $$MessagesTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + 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; } } -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 userId => $composableBuilder(column: $table.userId, builder: (column) => column); - GeneratedColumn get quotedMessageId => $composableBuilder( - column: $table.quotedMessageId, builder: (column) => column); + GeneratedColumn get latitude => $composableBuilder(column: $table.latitude, builder: (column) => column); - GeneratedColumn get pollId => - $composableBuilder(column: $table.pollId, builder: (column) => column); + GeneratedColumn get longitude => $composableBuilder(column: $table.longitude, builder: (column) => column); - GeneratedColumn get showInChannel => $composableBuilder( - column: $table.showInChannel, builder: (column) => column); + GeneratedColumn get createdByDeviceId => + $composableBuilder(column: $table.createdByDeviceId, builder: (column) => column); - GeneratedColumn get command => - $composableBuilder(column: $table.command, builder: (column) => column); + GeneratedColumn get endAt => $composableBuilder(column: $table.endAt, builder: (column) => column); - GeneratedColumn get silent => - $composableBuilder(column: $table.silent, builder: (column) => column); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); - GeneratedColumn get createdAt => - $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> - get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => column); - - $$MessagesTableAnnotationComposer get parentId { - final $$MessagesTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.parentId, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$MessagesTableAnnotationComposer( - $db: $db, - $table: $db.messages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + $$ChannelsTableAnnotationComposer get channelCid { + final $$ChannelsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$ChannelsTableAnnotationComposer( + $db: $db, + $table: $db.channels, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } - $$ChannelsTableAnnotationComposer get channelCid { - final $$ChannelsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableAnnotationComposer( - $db: $db, - $table: $db.channels, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + $$MessagesTableAnnotationComposer get messageId { + final $$MessagesTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } -class $$DraftMessagesTableTableManager extends RootTableManager< - _$DriftChatDatabase, - $DraftMessagesTable, - DraftMessageEntity, - $$DraftMessagesTableFilterComposer, - $$DraftMessagesTableOrderingComposer, - $$DraftMessagesTableAnnotationComposer, - $$DraftMessagesTableCreateCompanionBuilder, - $$DraftMessagesTableUpdateCompanionBuilder, - (DraftMessageEntity, $$DraftMessagesTableReferences), - DraftMessageEntity, - PrefetchHooks Function({bool parentId, bool channelCid})> { - $$DraftMessagesTableTableManager( - _$DriftChatDatabase db, $DraftMessagesTable table) - : super(TableManagerState( +class $$LocationsTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $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), - createOrderingComposer: () => - $$DraftMessagesTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$DraftMessagesTableAnnotationComposer($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 createdAt = const Value.absent(), - Value channelCid = const Value.absent(), - Value?> extraData = 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, - channelCid: channelCid, - extraData: extraData, - 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 createdAt = const Value.absent(), - required String channelCid, - Value?> extraData = 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, - channelCid: channelCid, - extraData: extraData, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => ( - e.readTable(table), - $$DraftMessagesTableReferences(db, table, e) - )) - .toList(), - prefetchHooksCallback: ({parentId = false, channelCid = false}) { + createFilteringComposer: () => $$LocationsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$LocationsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$LocationsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + 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 updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => LocationsCompanion( + channelCid: channelCid, + messageId: messageId, + userId: userId, + latitude: latitude, + longitude: longitude, + createdByDeviceId: createdByDeviceId, + endAt: endAt, + createdAt: createdAt, + updatedAt: updatedAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + 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(), + Value updatedAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => LocationsCompanion.insert( + channelCid: channelCid, + 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), $$LocationsTableReferences(db, table, e))).toList(), + prefetchHooksCallback: ({channelCid = false, messageId = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], - addJoins: < - T extends TableManagerState< + addJoins: + < + T extends TableManagerState< dynamic, dynamic, dynamic, @@ -10723,148 +11662,150 @@ class $$DraftMessagesTableTableManager extends RootTableManager< dynamic, 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: - $$DraftMessagesTableReferences._channelCidTable(db), - referencedColumn: - $$DraftMessagesTableReferences._channelCidTable(db).cid, - ) as T; - } - - return state; - }, + dynamic + > + >(state) { + if (channelCid) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.channelCid, + referencedTable: $$LocationsTableReferences._channelCidTable(db), + referencedColumn: $$LocationsTableReferences._channelCidTable(db).cid, + ) + as T; + } + if (messageId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.messageId, + referencedTable: $$LocationsTableReferences._messageIdTable(db), + referencedColumn: $$LocationsTableReferences._messageIdTable(db).id, + ) + as T; + } + + return state; + }, getPrefetchedDataCallback: (items) async { return []; }, ); }, - )); + ), + ); } -typedef $$DraftMessagesTableProcessedTableManager = ProcessedTableManager< - _$DriftChatDatabase, - $DraftMessagesTable, - DraftMessageEntity, - $$DraftMessagesTableFilterComposer, - $$DraftMessagesTableOrderingComposer, - $$DraftMessagesTableAnnotationComposer, - $$DraftMessagesTableCreateCompanionBuilder, - $$DraftMessagesTableUpdateCompanionBuilder, - (DraftMessageEntity, $$DraftMessagesTableReferences), - DraftMessageEntity, - PrefetchHooks Function({bool parentId, bool channelCid})>; -typedef $$PinnedMessagesTableCreateCompanionBuilder = PinnedMessagesCompanion - 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 channelRole, - Value pinned, - Value pinnedAt, - Value pinExpires, - Value pinnedByUserId, - required String channelCid, - Value?> i18n, - Value?> restrictedVisibility, - Value?> extraData, - Value rowid, -}); -typedef $$PinnedMessagesTableUpdateCompanionBuilder = PinnedMessagesCompanion - 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 channelRole, - Value pinned, - Value pinnedAt, - Value pinExpires, - Value pinnedByUserId, - Value channelCid, - Value?> i18n, - Value?> restrictedVisibility, - Value?> extraData, - Value rowid, -}); - -final class $$PinnedMessagesTableReferences extends BaseReferences< - _$DriftChatDatabase, $PinnedMessagesTable, PinnedMessageEntity> { - $$PinnedMessagesTableReferences( - super.$_db, super.$_table, super.$_typedResult); - - static MultiTypedResultKey<$PinnedMessageReactionsTable, - List> _pinnedMessageReactionsRefsTable( - _$DriftChatDatabase db) => - MultiTypedResultKey.fromTable(db.pinnedMessageReactions, - aliasName: $_aliasNameGenerator( - db.pinnedMessages.id, db.pinnedMessageReactions.messageId)); - - $$PinnedMessageReactionsTableProcessedTableManager - get pinnedMessageReactionsRefs { +typedef $$LocationsTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $LocationsTable, + LocationEntity, + $$LocationsTableFilterComposer, + $$LocationsTableOrderingComposer, + $$LocationsTableAnnotationComposer, + $$LocationsTableCreateCompanionBuilder, + $$LocationsTableUpdateCompanionBuilder, + (LocationEntity, $$LocationsTableReferences), + LocationEntity, + PrefetchHooks Function({bool channelCid, bool messageId}) + >; +typedef $$PinnedMessagesTableCreateCompanionBuilder = + PinnedMessagesCompanion 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 deletedForMe, + Value messageTextUpdatedAt, + Value userId, + Value channelRole, + Value pinned, + Value pinnedAt, + Value pinExpires, + Value pinnedByUserId, + required String channelCid, + Value?> i18n, + Value?> restrictedVisibility, + Value?> extraData, + Value rowid, + }); +typedef $$PinnedMessagesTableUpdateCompanionBuilder = + PinnedMessagesCompanion 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 deletedForMe, + Value messageTextUpdatedAt, + Value userId, + Value channelRole, + Value pinned, + Value pinnedAt, + Value pinExpires, + Value pinnedByUserId, + Value channelCid, + Value?> i18n, + Value?> restrictedVisibility, + Value?> extraData, + Value rowid, + }); + +final class $$PinnedMessagesTableReferences + extends BaseReferences<_$DriftChatDatabase, $PinnedMessagesTable, PinnedMessageEntity> { + $$PinnedMessagesTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$PinnedMessageReactionsTable, List> + _pinnedMessageReactionsRefsTable(_$DriftChatDatabase db) => MultiTypedResultKey.fromTable( + db.pinnedMessageReactions, + aliasName: $_aliasNameGenerator(db.pinnedMessages.id, db.pinnedMessageReactions.messageId), + ); + + $$PinnedMessageReactionsTableProcessedTableManager get pinnedMessageReactionsRefs { final manager = $$PinnedMessageReactionsTableTableManager( - $_db, $_db.pinnedMessageReactions) - .filter((f) => f.messageId.id($_item.id)); + $_db, + $_db.pinnedMessageReactions, + ).filter((f) => f.messageId.id.sqlEquals($_itemColumn('id')!)); - final cache = - $_typedResult.readTableOrNull(_pinnedMessageReactionsRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); + final cache = $_typedResult.readTableOrNull(_pinnedMessageReactionsRefsTable($_db)); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: cache)); } } -class $$PinnedMessagesTableFilterComposer - extends Composer<_$DriftChatDatabase, $PinnedMessagesTable> { +class $$PinnedMessagesTableFilterComposer extends Composer<_$DriftChatDatabase, $PinnedMessagesTable> { $$PinnedMessagesTableFilterComposer({ required super.$db, required super.$table, @@ -10872,149 +11813,124 @@ class $$PinnedMessagesTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get messageText => + $composableBuilder(column: $table.messageText, 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)); - 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 state => $composableBuilder( - column: $table.state, builder: (column) => ColumnFilters(column)); + ColumnFilters get type => $composableBuilder(column: $table.type, 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, 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)); - 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 parentId => $composableBuilder( - column: $table.parentId, builder: (column) => ColumnFilters(column)); + ColumnFilters get quotedMessageId => + $composableBuilder(column: $table.quotedMessageId, 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 pollId => $composableBuilder( - column: $table.pollId, builder: (column) => ColumnFilters(column)); + ColumnFilters get replyCount => + $composableBuilder(column: $table.replyCount, 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 showInChannel => $composableBuilder( - column: $table.showInChannel, builder: (column) => ColumnFilters(column)); + ColumnFilters get shadowed => + $composableBuilder(column: $table.shadowed, 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 command => $composableBuilder( - column: $table.command, builder: (column) => ColumnFilters(column)); + ColumnFilters get localCreatedAt => + $composableBuilder(column: $table.localCreatedAt, 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 remoteCreatedAt => $composableBuilder( - column: $table.remoteCreatedAt, - builder: (column) => ColumnFilters(column)); + ColumnFilters get localUpdatedAt => + $composableBuilder(column: $table.localUpdatedAt, 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 remoteUpdatedAt => $composableBuilder( - column: $table.remoteUpdatedAt, - builder: (column) => ColumnFilters(column)); + ColumnFilters get localDeletedAt => + $composableBuilder(column: $table.localDeletedAt, 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 remoteDeletedAt => $composableBuilder( - column: $table.remoteDeletedAt, - builder: (column) => ColumnFilters(column)); + ColumnFilters get deletedForMe => + $composableBuilder(column: $table.deletedForMe, builder: (column) => ColumnFilters(column)); - ColumnFilters get messageTextUpdatedAt => $composableBuilder( - column: $table.messageTextUpdatedAt, - 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 userId => + $composableBuilder(column: $table.userId, builder: (column) => ColumnFilters(column)); - ColumnFilters get channelRole => $composableBuilder( - column: $table.channelRole, builder: (column) => ColumnFilters(column)); + ColumnFilters get channelRole => + $composableBuilder(column: $table.channelRole, builder: (column) => ColumnFilters(column)); - ColumnFilters get pinned => $composableBuilder( - column: $table.pinned, 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 pinnedAt => + $composableBuilder(column: $table.pinnedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get pinExpires => $composableBuilder( - column: $table.pinExpires, 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)); + ColumnFilters get pinnedByUserId => + $composableBuilder(column: $table.pinnedByUserId, builder: (column) => ColumnFilters(column)); - ColumnFilters get channelCid => $composableBuilder( - column: $table.channelCid, builder: (column) => ColumnFilters(column)); + ColumnFilters get channelCid => + $composableBuilder(column: $table.channelCid, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, Map, - String> - get i18n => $composableBuilder( - column: $table.i18n, - builder: (column) => ColumnWithTypeConverterFilters(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?, List, String> get restrictedVisibility => $composableBuilder( + column: $table.restrictedVisibility, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); - ColumnWithTypeConverterFilters?, Map, - String> - get extraData => $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, Map, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnWithTypeConverterFilters(column)); Expression pinnedMessageReactionsRefs( - Expression Function($$PinnedMessageReactionsTableFilterComposer f) - f) { - final $$PinnedMessageReactionsTableFilterComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.pinnedMessageReactions, - getReferencedColumn: (t) => t.messageId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PinnedMessageReactionsTableFilterComposer( - $db: $db, - $table: $db.pinnedMessageReactions, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + Expression Function($$PinnedMessageReactionsTableFilterComposer f) f, + ) { + final $$PinnedMessageReactionsTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.pinnedMessageReactions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$PinnedMessageReactionsTableFilterComposer( + $db: $db, + $table: $db.pinnedMessageReactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return f(composer); } } -class $$PinnedMessagesTableOrderingComposer - extends Composer<_$DriftChatDatabase, $PinnedMessagesTable> { +class $$PinnedMessagesTableOrderingComposer extends Composer<_$DriftChatDatabase, $PinnedMessagesTable> { $$PinnedMessagesTableOrderingComposer({ required super.$db, required super.$table, @@ -11022,115 +11938,103 @@ class $$PinnedMessagesTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get messageText => $composableBuilder( - column: $table.messageText, 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 attachments => + $composableBuilder(column: $table.attachments, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get state => $composableBuilder( - column: $table.state, 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 type => + $composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get mentionedUsers => $composableBuilder( - column: $table.mentionedUsers, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get mentionedUsers => + $composableBuilder(column: $table.mentionedUsers, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get reactionGroups => $composableBuilder( - column: $table.reactionGroups, - 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 parentId => + $composableBuilder(column: $table.parentId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get quotedMessageId => $composableBuilder( - column: $table.quotedMessageId, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get quotedMessageId => + $composableBuilder(column: $table.quotedMessageId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pollId => $composableBuilder( - column: $table.pollId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get pollId => + $composableBuilder(column: $table.pollId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get replyCount => $composableBuilder( - column: $table.replyCount, 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 showInChannel => + $composableBuilder(column: $table.showInChannel, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get shadowed => $composableBuilder( - column: $table.shadowed, 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 command => + $composableBuilder(column: $table.command, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get localCreatedAt => $composableBuilder( - column: $table.localCreatedAt, - 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 remoteCreatedAt => + $composableBuilder(column: $table.remoteCreatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get localUpdatedAt => $composableBuilder( - column: $table.localUpdatedAt, - 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 remoteUpdatedAt => + $composableBuilder(column: $table.remoteUpdatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get localDeletedAt => $composableBuilder( - column: $table.localDeletedAt, - 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 remoteDeletedAt => + $composableBuilder(column: $table.remoteDeletedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get messageTextUpdatedAt => $composableBuilder( - column: $table.messageTextUpdatedAt, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get deletedForMe => + $composableBuilder(column: $table.deletedForMe, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get messageTextUpdatedAt => + $composableBuilder(column: $table.messageTextUpdatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get channelRole => $composableBuilder( - column: $table.channelRole, 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 channelRole => + $composableBuilder(column: $table.channelRole, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pinnedAt => $composableBuilder( - column: $table.pinnedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get pinned => + $composableBuilder(column: $table.pinned, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pinExpires => $composableBuilder( - column: $table.pinExpires, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get pinnedAt => + $composableBuilder(column: $table.pinnedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pinnedByUserId => $composableBuilder( - column: $table.pinnedByUserId, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get pinExpires => + $composableBuilder(column: $table.pinExpires, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get channelCid => $composableBuilder( - column: $table.channelCid, 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 channelCid => + $composableBuilder(column: $table.channelCid, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get restrictedVisibility => $composableBuilder( - column: $table.restrictedVisibility, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get i18n => + $composableBuilder(column: $table.i18n, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, 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)); } -class $$PinnedMessagesTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $PinnedMessagesTable> { +class $$PinnedMessagesTableAnnotationComposer extends Composer<_$DriftChatDatabase, $PinnedMessagesTable> { $$PinnedMessagesTableAnnotationComposer({ required super.$db, required super.$table, @@ -11138,398 +12042,376 @@ class $$PinnedMessagesTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get messageText => $composableBuilder( - column: $table.messageText, builder: (column) => column); + GeneratedColumn get messageText => + $composableBuilder(column: $table.messageText, builder: (column) => column); GeneratedColumnWithTypeConverter, String> get attachments => - $composableBuilder( - column: $table.attachments, builder: (column) => column); + $composableBuilder(column: $table.attachments, builder: (column) => column); - GeneratedColumn get state => - $composableBuilder(column: $table.state, 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 type => $composableBuilder(column: $table.type, builder: (column) => column); GeneratedColumnWithTypeConverter, String> get mentionedUsers => - $composableBuilder( - column: $table.mentionedUsers, builder: (column) => column); + $composableBuilder(column: $table.mentionedUsers, builder: (column) => column); + + GeneratedColumnWithTypeConverter?, String> get reactionGroups => + $composableBuilder(column: $table.reactionGroups, 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 parentId => - $composableBuilder(column: $table.parentId, builder: (column) => column); + GeneratedColumn get quotedMessageId => + $composableBuilder(column: $table.quotedMessageId, 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 pollId => - $composableBuilder(column: $table.pollId, builder: (column) => column); + GeneratedColumn get replyCount => $composableBuilder(column: $table.replyCount, 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 showInChannel => $composableBuilder( - column: $table.showInChannel, builder: (column) => column); + GeneratedColumn get shadowed => $composableBuilder(column: $table.shadowed, 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 command => - $composableBuilder(column: $table.command, builder: (column) => column); + GeneratedColumn get localCreatedAt => + $composableBuilder(column: $table.localCreatedAt, 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 remoteCreatedAt => $composableBuilder( - column: $table.remoteCreatedAt, builder: (column) => column); + GeneratedColumn get localUpdatedAt => + $composableBuilder(column: $table.localUpdatedAt, 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 remoteUpdatedAt => $composableBuilder( - column: $table.remoteUpdatedAt, builder: (column) => column); + GeneratedColumn get localDeletedAt => + $composableBuilder(column: $table.localDeletedAt, 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 remoteDeletedAt => $composableBuilder( - column: $table.remoteDeletedAt, builder: (column) => column); + GeneratedColumn get deletedForMe => + $composableBuilder(column: $table.deletedForMe, builder: (column) => column); - GeneratedColumn get messageTextUpdatedAt => $composableBuilder( - column: $table.messageTextUpdatedAt, 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 userId => $composableBuilder(column: $table.userId, builder: (column) => column); - GeneratedColumn get channelRole => $composableBuilder( - column: $table.channelRole, builder: (column) => column); + GeneratedColumn get channelRole => + $composableBuilder(column: $table.channelRole, builder: (column) => column); - GeneratedColumn get pinned => - $composableBuilder(column: $table.pinned, 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 pinnedAt => $composableBuilder(column: $table.pinnedAt, builder: (column) => column); - GeneratedColumn get pinExpires => $composableBuilder( - column: $table.pinExpires, builder: (column) => column); + GeneratedColumn get pinExpires => + $composableBuilder(column: $table.pinExpires, builder: (column) => column); - GeneratedColumn get pinnedByUserId => $composableBuilder( - column: $table.pinnedByUserId, builder: (column) => column); + GeneratedColumn get pinnedByUserId => + $composableBuilder(column: $table.pinnedByUserId, builder: (column) => column); - GeneratedColumn get channelCid => $composableBuilder( - column: $table.channelCid, builder: (column) => column); + GeneratedColumn get channelCid => $composableBuilder(column: $table.channelCid, 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 restrictedVisibility => + $composableBuilder(column: $table.restrictedVisibility, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> - get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => column); Expression pinnedMessageReactionsRefs( - Expression Function($$PinnedMessageReactionsTableAnnotationComposer a) - f) { - final $$PinnedMessageReactionsTableAnnotationComposer composer = - $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.pinnedMessageReactions, - getReferencedColumn: (t) => t.messageId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PinnedMessageReactionsTableAnnotationComposer( - $db: $db, - $table: $db.pinnedMessageReactions, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + Expression Function($$PinnedMessageReactionsTableAnnotationComposer a) f, + ) { + final $$PinnedMessageReactionsTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.pinnedMessageReactions, + getReferencedColumn: (t) => t.messageId, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$PinnedMessageReactionsTableAnnotationComposer( + $db: $db, + $table: $db.pinnedMessageReactions, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return f(composer); } } -class $$PinnedMessagesTableTableManager extends RootTableManager< - _$DriftChatDatabase, - $PinnedMessagesTable, - PinnedMessageEntity, - $$PinnedMessagesTableFilterComposer, - $$PinnedMessagesTableOrderingComposer, - $$PinnedMessagesTableAnnotationComposer, - $$PinnedMessagesTableCreateCompanionBuilder, - $$PinnedMessagesTableUpdateCompanionBuilder, - (PinnedMessageEntity, $$PinnedMessagesTableReferences), - PinnedMessageEntity, - PrefetchHooks Function({bool pinnedMessageReactionsRefs})> { - $$PinnedMessagesTableTableManager( - _$DriftChatDatabase db, $PinnedMessagesTable table) - : super(TableManagerState( +class $$PinnedMessagesTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $PinnedMessagesTable, + PinnedMessageEntity, + $$PinnedMessagesTableFilterComposer, + $$PinnedMessagesTableOrderingComposer, + $$PinnedMessagesTableAnnotationComposer, + $$PinnedMessagesTableCreateCompanionBuilder, + $$PinnedMessagesTableUpdateCompanionBuilder, + (PinnedMessageEntity, $$PinnedMessagesTableReferences), + PinnedMessageEntity, + PrefetchHooks Function({bool pinnedMessageReactionsRefs}) + > { + $$PinnedMessagesTableTableManager(_$DriftChatDatabase db, $PinnedMessagesTable table) + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$PinnedMessagesTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$PinnedMessagesTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$PinnedMessagesTableAnnotationComposer($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 channelRole = 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(), - }) => - PinnedMessagesCompanion( - 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, - channelRole: channelRole, - pinned: pinned, - pinnedAt: pinnedAt, - pinExpires: pinExpires, - pinnedByUserId: pinnedByUserId, - channelCid: channelCid, - i18n: i18n, - restrictedVisibility: restrictedVisibility, - extraData: extraData, - rowid: rowid, - ), - createCompanionCallback: ({ - 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 channelRole = 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(), - }) => - PinnedMessagesCompanion.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, - channelRole: channelRole, - 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), - $$PinnedMessagesTableReferences(db, table, e) - )) - .toList(), + createFilteringComposer: () => $$PinnedMessagesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$PinnedMessagesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$PinnedMessagesTableAnnotationComposer($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 deletedForMe = const Value.absent(), + Value messageTextUpdatedAt = const Value.absent(), + Value userId = const Value.absent(), + Value channelRole = 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(), + }) => PinnedMessagesCompanion( + 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, + deletedForMe: deletedForMe, + messageTextUpdatedAt: messageTextUpdatedAt, + userId: userId, + channelRole: channelRole, + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedByUserId: pinnedByUserId, + channelCid: channelCid, + i18n: i18n, + restrictedVisibility: restrictedVisibility, + extraData: extraData, + rowid: rowid, + ), + createCompanionCallback: + ({ + 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 deletedForMe = const Value.absent(), + Value messageTextUpdatedAt = const Value.absent(), + Value userId = const Value.absent(), + Value channelRole = 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(), + }) => PinnedMessagesCompanion.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, + deletedForMe: deletedForMe, + messageTextUpdatedAt: messageTextUpdatedAt, + userId: userId, + channelRole: channelRole, + 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), $$PinnedMessagesTableReferences(db, table, e))).toList(), prefetchHooksCallback: ({pinnedMessageReactionsRefs = false}) { return PrefetchHooks( db: db, - explicitlyWatchedTables: [ - if (pinnedMessageReactionsRefs) db.pinnedMessageReactions - ], + explicitlyWatchedTables: [if (pinnedMessageReactionsRefs) db.pinnedMessageReactions], addJoins: null, getPrefetchedDataCallback: (items) async { return [ if (pinnedMessageReactionsRefs) - await $_getPrefetchedData( - currentTable: table, - referencedTable: $$PinnedMessagesTableReferences - ._pinnedMessageReactionsRefsTable(db), - managerFromTypedResult: (p0) => - $$PinnedMessagesTableReferences(db, table, p0) - .pinnedMessageReactionsRefs, - referencedItemsForCurrentItem: - (item, referencedItems) => referencedItems - .where((e) => e.messageId == item.id), - typedResults: items) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$PinnedMessagesTableReferences._pinnedMessageReactionsRefsTable(db), + managerFromTypedResult: (p0) => + $$PinnedMessagesTableReferences(db, table, p0).pinnedMessageReactionsRefs, + referencedItemsForCurrentItem: (item, referencedItems) => + referencedItems.where((e) => e.messageId == item.id), + typedResults: items, + ), ]; }, ); }, - )); + ), + ); } -typedef $$PinnedMessagesTableProcessedTableManager = ProcessedTableManager< - _$DriftChatDatabase, - $PinnedMessagesTable, - PinnedMessageEntity, - $$PinnedMessagesTableFilterComposer, - $$PinnedMessagesTableOrderingComposer, - $$PinnedMessagesTableAnnotationComposer, - $$PinnedMessagesTableCreateCompanionBuilder, - $$PinnedMessagesTableUpdateCompanionBuilder, - (PinnedMessageEntity, $$PinnedMessagesTableReferences), - PinnedMessageEntity, - PrefetchHooks Function({bool pinnedMessageReactionsRefs})>; -typedef $$PollsTableCreateCompanionBuilder = PollsCompanion Function({ - required String id, - required String name, - Value description, - required List options, - Value votingVisibility, - Value enforceUniqueVote, - Value maxVotesAllowed, - Value allowUserSuggestedOptions, - Value allowAnswers, - Value isClosed, - Value answersCount, - required Map voteCountsByOption, - Value voteCount, - Value createdById, - Value createdAt, - Value updatedAt, - Value?> extraData, - Value rowid, -}); -typedef $$PollsTableUpdateCompanionBuilder = PollsCompanion Function({ - 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, -}); - -final class $$PollsTableReferences - extends BaseReferences<_$DriftChatDatabase, $PollsTable, PollEntity> { +typedef $$PinnedMessagesTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $PinnedMessagesTable, + PinnedMessageEntity, + $$PinnedMessagesTableFilterComposer, + $$PinnedMessagesTableOrderingComposer, + $$PinnedMessagesTableAnnotationComposer, + $$PinnedMessagesTableCreateCompanionBuilder, + $$PinnedMessagesTableUpdateCompanionBuilder, + (PinnedMessageEntity, $$PinnedMessagesTableReferences), + PinnedMessageEntity, + PrefetchHooks Function({bool pinnedMessageReactionsRefs}) + >; +typedef $$PollsTableCreateCompanionBuilder = + PollsCompanion Function({ + required String id, + required String name, + Value description, + required List options, + Value votingVisibility, + Value enforceUniqueVote, + Value maxVotesAllowed, + Value allowUserSuggestedOptions, + Value allowAnswers, + Value isClosed, + Value answersCount, + required Map voteCountsByOption, + Value voteCount, + Value createdById, + Value createdAt, + Value updatedAt, + Value?> extraData, + Value rowid, + }); +typedef $$PollsTableUpdateCompanionBuilder = + PollsCompanion Function({ + 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, + }); + +final class $$PollsTableReferences extends BaseReferences<_$DriftChatDatabase, $PollsTable, PollEntity> { $$PollsTableReferences(super.$_db, super.$_table, super.$_typedResult); - static MultiTypedResultKey<$PollVotesTable, List> - _pollVotesRefsTable(_$DriftChatDatabase db) => - MultiTypedResultKey.fromTable(db.pollVotes, - aliasName: - $_aliasNameGenerator(db.polls.id, db.pollVotes.pollId)); + static MultiTypedResultKey<$PollVotesTable, List> _pollVotesRefsTable(_$DriftChatDatabase db) => + MultiTypedResultKey.fromTable(db.pollVotes, aliasName: $_aliasNameGenerator(db.polls.id, db.pollVotes.pollId)); $$PollVotesTableProcessedTableManager get pollVotesRefs { - final manager = $$PollVotesTableTableManager($_db, $_db.pollVotes) - .filter((f) => f.pollId.id($_item.id)); + final manager = $$PollVotesTableTableManager( + $_db, + $_db.pollVotes, + ).filter((f) => f.pollId.id.sqlEquals($_itemColumn('id')!)); final cache = $_typedResult.readTableOrNull(_pollVotesRefsTable($_db)); - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: cache)); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: cache)); } } -class $$PollsTableFilterComposer - extends Composer<_$DriftChatDatabase, $PollsTable> { +class $$PollsTableFilterComposer extends Composer<_$DriftChatDatabase, $PollsTable> { $$PollsTableFilterComposer({ required super.$db, required super.$table, @@ -11537,93 +12419,78 @@ class $$PollsTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnFilters(column)); + ColumnFilters get name => $composableBuilder(column: $table.name, builder: (column) => ColumnFilters(column)); - ColumnFilters get description => $composableBuilder( - column: $table.description, builder: (column) => ColumnFilters(column)); + ColumnFilters get description => + $composableBuilder(column: $table.description, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters, List, String> - get options => $composableBuilder( - column: $table.options, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters, List, String> get options => + $composableBuilder(column: $table.options, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnWithTypeConverterFilters - get votingVisibility => $composableBuilder( - column: $table.votingVisibility, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters get votingVisibility => + $composableBuilder(column: $table.votingVisibility, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get enforceUniqueVote => $composableBuilder( - column: $table.enforceUniqueVote, - builder: (column) => ColumnFilters(column)); + ColumnFilters get enforceUniqueVote => + $composableBuilder(column: $table.enforceUniqueVote, builder: (column) => ColumnFilters(column)); - ColumnFilters get maxVotesAllowed => $composableBuilder( - column: $table.maxVotesAllowed, - builder: (column) => ColumnFilters(column)); + ColumnFilters get maxVotesAllowed => + $composableBuilder(column: $table.maxVotesAllowed, builder: (column) => ColumnFilters(column)); - ColumnFilters get allowUserSuggestedOptions => $composableBuilder( - column: $table.allowUserSuggestedOptions, - builder: (column) => ColumnFilters(column)); + ColumnFilters get allowUserSuggestedOptions => + $composableBuilder(column: $table.allowUserSuggestedOptions, builder: (column) => ColumnFilters(column)); - ColumnFilters get allowAnswers => $composableBuilder( - column: $table.allowAnswers, builder: (column) => ColumnFilters(column)); + ColumnFilters get allowAnswers => + $composableBuilder(column: $table.allowAnswers, builder: (column) => ColumnFilters(column)); - ColumnFilters get isClosed => $composableBuilder( - column: $table.isClosed, builder: (column) => ColumnFilters(column)); + ColumnFilters get isClosed => + $composableBuilder(column: $table.isClosed, builder: (column) => ColumnFilters(column)); - ColumnFilters get answersCount => $composableBuilder( - column: $table.answersCount, builder: (column) => ColumnFilters(column)); + ColumnFilters get answersCount => + $composableBuilder(column: $table.answersCount, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters, Map, String> - get voteCountsByOption => $composableBuilder( - column: $table.voteCountsByOption, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters, Map, String> get voteCountsByOption => + $composableBuilder( + column: $table.voteCountsByOption, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); - ColumnFilters get voteCount => $composableBuilder( - column: $table.voteCount, builder: (column) => ColumnFilters(column)); + ColumnFilters get voteCount => + $composableBuilder(column: $table.voteCount, builder: (column) => ColumnFilters(column)); - ColumnFilters get createdById => $composableBuilder( - column: $table.createdById, builder: (column) => ColumnFilters(column)); + ColumnFilters get createdById => + $composableBuilder(column: $table.createdById, builder: (column) => ColumnFilters(column)); - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, 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 updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, Map, - String> - get extraData => $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, Map, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnWithTypeConverterFilters(column)); - Expression pollVotesRefs( - Expression Function($$PollVotesTableFilterComposer f) f) { + Expression pollVotesRefs(Expression Function($$PollVotesTableFilterComposer f) f) { final $$PollVotesTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.pollVotes, - getReferencedColumn: (t) => t.pollId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PollVotesTableFilterComposer( - $db: $db, - $table: $db.pollVotes, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.pollVotes, + getReferencedColumn: (t) => t.pollId, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$PollVotesTableFilterComposer( + $db: $db, + $table: $db.pollVotes, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return f(composer); } } -class $$PollsTableOrderingComposer - extends Composer<_$DriftChatDatabase, $PollsTable> { +class $$PollsTableOrderingComposer extends Composer<_$DriftChatDatabase, $PollsTable> { $$PollsTableOrderingComposer({ required super.$db, required super.$table, @@ -11631,67 +12498,58 @@ class $$PollsTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get name => $composableBuilder( - column: $table.name, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get name => + $composableBuilder(column: $table.name, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get description => $composableBuilder( - column: $table.description, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get description => + $composableBuilder(column: $table.description, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get options => $composableBuilder( - column: $table.options, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get options => + $composableBuilder(column: $table.options, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get votingVisibility => $composableBuilder( - column: $table.votingVisibility, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get votingVisibility => + $composableBuilder(column: $table.votingVisibility, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get enforceUniqueVote => $composableBuilder( - column: $table.enforceUniqueVote, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get enforceUniqueVote => + $composableBuilder(column: $table.enforceUniqueVote, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get maxVotesAllowed => $composableBuilder( - column: $table.maxVotesAllowed, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get maxVotesAllowed => + $composableBuilder(column: $table.maxVotesAllowed, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get allowUserSuggestedOptions => $composableBuilder( - column: $table.allowUserSuggestedOptions, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get allowUserSuggestedOptions => + $composableBuilder(column: $table.allowUserSuggestedOptions, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get allowAnswers => $composableBuilder( - column: $table.allowAnswers, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get allowAnswers => + $composableBuilder(column: $table.allowAnswers, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get isClosed => $composableBuilder( - column: $table.isClosed, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get isClosed => + $composableBuilder(column: $table.isClosed, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get answersCount => $composableBuilder( - column: $table.answersCount, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get answersCount => + $composableBuilder(column: $table.answersCount, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get voteCountsByOption => $composableBuilder( - column: $table.voteCountsByOption, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get voteCountsByOption => + $composableBuilder(column: $table.voteCountsByOption, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get voteCount => $composableBuilder( - column: $table.voteCount, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get voteCount => + $composableBuilder(column: $table.voteCount, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdById => $composableBuilder( - column: $table.createdById, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdById => + $composableBuilder(column: $table.createdById, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, 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 updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnOrderings(column)); } -class $$PollsTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $PollsTable> { +class $$PollsTableAnnotationComposer extends Composer<_$DriftChatDatabase, $PollsTable> { $$PollsTableAnnotationComposer({ required super.$db, required super.$table, @@ -11699,188 +12557,174 @@ class $$PollsTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get name => - $composableBuilder(column: $table.name, builder: (column) => column); + GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); - GeneratedColumn get description => $composableBuilder( - column: $table.description, builder: (column) => column); + GeneratedColumn get description => + $composableBuilder(column: $table.description, builder: (column) => column); GeneratedColumnWithTypeConverter, String> get options => $composableBuilder(column: $table.options, builder: (column) => column); - GeneratedColumnWithTypeConverter - get votingVisibility => $composableBuilder( - column: $table.votingVisibility, builder: (column) => column); + GeneratedColumnWithTypeConverter get votingVisibility => + $composableBuilder(column: $table.votingVisibility, builder: (column) => column); - GeneratedColumn get enforceUniqueVote => $composableBuilder( - column: $table.enforceUniqueVote, builder: (column) => column); + GeneratedColumn get enforceUniqueVote => + $composableBuilder(column: $table.enforceUniqueVote, builder: (column) => column); - GeneratedColumn get maxVotesAllowed => $composableBuilder( - column: $table.maxVotesAllowed, builder: (column) => column); + GeneratedColumn get maxVotesAllowed => + $composableBuilder(column: $table.maxVotesAllowed, builder: (column) => column); - GeneratedColumn get allowUserSuggestedOptions => $composableBuilder( - column: $table.allowUserSuggestedOptions, builder: (column) => column); + GeneratedColumn get allowUserSuggestedOptions => + $composableBuilder(column: $table.allowUserSuggestedOptions, builder: (column) => column); - GeneratedColumn get allowAnswers => $composableBuilder( - column: $table.allowAnswers, builder: (column) => column); + GeneratedColumn get allowAnswers => + $composableBuilder(column: $table.allowAnswers, builder: (column) => column); - GeneratedColumn get isClosed => - $composableBuilder(column: $table.isClosed, builder: (column) => column); + GeneratedColumn get isClosed => $composableBuilder(column: $table.isClosed, builder: (column) => column); - GeneratedColumn get answersCount => $composableBuilder( - column: $table.answersCount, builder: (column) => column); + GeneratedColumn get answersCount => $composableBuilder(column: $table.answersCount, builder: (column) => column); - GeneratedColumnWithTypeConverter, String> - get voteCountsByOption => $composableBuilder( - column: $table.voteCountsByOption, builder: (column) => column); + GeneratedColumnWithTypeConverter, String> get voteCountsByOption => + $composableBuilder(column: $table.voteCountsByOption, builder: (column) => column); - GeneratedColumn get voteCount => - $composableBuilder(column: $table.voteCount, builder: (column) => column); + GeneratedColumn get voteCount => $composableBuilder(column: $table.voteCount, builder: (column) => column); - GeneratedColumn get createdById => $composableBuilder( - column: $table.createdById, builder: (column) => column); + GeneratedColumn get createdById => + $composableBuilder(column: $table.createdById, builder: (column) => column); - GeneratedColumn get createdAt => - $composableBuilder(column: $table.createdAt, 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 updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> - get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => column); - Expression pollVotesRefs( - Expression Function($$PollVotesTableAnnotationComposer a) f) { + Expression pollVotesRefs(Expression Function($$PollVotesTableAnnotationComposer a) f) { final $$PollVotesTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.id, - referencedTable: $db.pollVotes, - getReferencedColumn: (t) => t.pollId, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PollVotesTableAnnotationComposer( - $db: $db, - $table: $db.pollVotes, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.pollVotes, + getReferencedColumn: (t) => t.pollId, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$PollVotesTableAnnotationComposer( + $db: $db, + $table: $db.pollVotes, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return f(composer); } } -class $$PollsTableTableManager extends RootTableManager< - _$DriftChatDatabase, - $PollsTable, - PollEntity, - $$PollsTableFilterComposer, - $$PollsTableOrderingComposer, - $$PollsTableAnnotationComposer, - $$PollsTableCreateCompanionBuilder, - $$PollsTableUpdateCompanionBuilder, - (PollEntity, $$PollsTableReferences), - PollEntity, - PrefetchHooks Function({bool pollVotesRefs})> { +class $$PollsTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $PollsTable, + PollEntity, + $$PollsTableFilterComposer, + $$PollsTableOrderingComposer, + $$PollsTableAnnotationComposer, + $$PollsTableCreateCompanionBuilder, + $$PollsTableUpdateCompanionBuilder, + (PollEntity, $$PollsTableReferences), + PollEntity, + PrefetchHooks Function({bool pollVotesRefs}) + > { $$PollsTableTableManager(_$DriftChatDatabase db, $PollsTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$PollsTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$PollsTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$PollsTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value name = const Value.absent(), - Value description = const Value.absent(), - Value> options = const Value.absent(), - Value votingVisibility = const Value.absent(), - Value enforceUniqueVote = const Value.absent(), - Value maxVotesAllowed = const Value.absent(), - Value allowUserSuggestedOptions = const Value.absent(), - Value allowAnswers = const Value.absent(), - Value isClosed = const Value.absent(), - Value answersCount = const Value.absent(), - Value> voteCountsByOption = const Value.absent(), - Value voteCount = const Value.absent(), - Value createdById = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value?> extraData = const Value.absent(), - Value rowid = const Value.absent(), - }) => - PollsCompanion( - id: id, - name: name, - description: description, - options: options, - votingVisibility: votingVisibility, - enforceUniqueVote: enforceUniqueVote, - maxVotesAllowed: maxVotesAllowed, - allowUserSuggestedOptions: allowUserSuggestedOptions, - allowAnswers: allowAnswers, - isClosed: isClosed, - answersCount: answersCount, - voteCountsByOption: voteCountsByOption, - voteCount: voteCount, - createdById: createdById, - createdAt: createdAt, - updatedAt: updatedAt, - extraData: extraData, - rowid: rowid, - ), - createCompanionCallback: ({ - required String id, - required String name, - Value description = const Value.absent(), - required List options, - Value votingVisibility = const Value.absent(), - Value enforceUniqueVote = const Value.absent(), - Value maxVotesAllowed = const Value.absent(), - Value allowUserSuggestedOptions = const Value.absent(), - Value allowAnswers = const Value.absent(), - Value isClosed = const Value.absent(), - Value answersCount = const Value.absent(), - required Map voteCountsByOption, - Value voteCount = const Value.absent(), - Value createdById = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value?> extraData = const Value.absent(), - Value rowid = const Value.absent(), - }) => - PollsCompanion.insert( - id: id, - name: name, - description: description, - options: options, - votingVisibility: votingVisibility, - enforceUniqueVote: enforceUniqueVote, - maxVotesAllowed: maxVotesAllowed, - allowUserSuggestedOptions: allowUserSuggestedOptions, - allowAnswers: allowAnswers, - isClosed: isClosed, - answersCount: answersCount, - voteCountsByOption: voteCountsByOption, - voteCount: voteCount, - createdById: createdById, - createdAt: createdAt, - updatedAt: updatedAt, - extraData: extraData, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => - (e.readTable(table), $$PollsTableReferences(db, table, e))) - .toList(), + createFilteringComposer: () => $$PollsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$PollsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$PollsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value description = const Value.absent(), + Value> options = const Value.absent(), + Value votingVisibility = const Value.absent(), + Value enforceUniqueVote = const Value.absent(), + Value maxVotesAllowed = const Value.absent(), + Value allowUserSuggestedOptions = const Value.absent(), + Value allowAnswers = const Value.absent(), + Value isClosed = const Value.absent(), + Value answersCount = const Value.absent(), + Value> voteCountsByOption = const Value.absent(), + Value voteCount = const Value.absent(), + Value createdById = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value?> extraData = const Value.absent(), + Value rowid = const Value.absent(), + }) => PollsCompanion( + id: id, + name: name, + description: description, + options: options, + votingVisibility: votingVisibility, + enforceUniqueVote: enforceUniqueVote, + maxVotesAllowed: maxVotesAllowed, + allowUserSuggestedOptions: allowUserSuggestedOptions, + allowAnswers: allowAnswers, + isClosed: isClosed, + answersCount: answersCount, + voteCountsByOption: voteCountsByOption, + voteCount: voteCount, + createdById: createdById, + createdAt: createdAt, + updatedAt: updatedAt, + extraData: extraData, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String name, + Value description = const Value.absent(), + required List options, + Value votingVisibility = const Value.absent(), + Value enforceUniqueVote = const Value.absent(), + Value maxVotesAllowed = const Value.absent(), + Value allowUserSuggestedOptions = const Value.absent(), + Value allowAnswers = const Value.absent(), + Value isClosed = const Value.absent(), + Value answersCount = const Value.absent(), + required Map voteCountsByOption, + Value voteCount = const Value.absent(), + Value createdById = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value?> extraData = const Value.absent(), + Value rowid = const Value.absent(), + }) => PollsCompanion.insert( + id: id, + name: name, + description: description, + options: options, + votingVisibility: votingVisibility, + enforceUniqueVote: enforceUniqueVote, + maxVotesAllowed: maxVotesAllowed, + allowUserSuggestedOptions: allowUserSuggestedOptions, + allowAnswers: allowAnswers, + isClosed: isClosed, + answersCount: answersCount, + voteCountsByOption: voteCountsByOption, + voteCount: voteCount, + createdById: createdById, + createdAt: createdAt, + updatedAt: updatedAt, + extraData: extraData, + rowid: rowid, + ), + withReferenceMapper: (p0) => + p0.map((e) => (e.readTable(table), $$PollsTableReferences(db, table, e))).toList(), prefetchHooksCallback: ({pollVotesRefs = false}) { return PrefetchHooks( db: db, @@ -11889,76 +12733,76 @@ class $$PollsTableTableManager extends RootTableManager< getPrefetchedDataCallback: (items) async { return [ if (pollVotesRefs) - await $_getPrefetchedData( - currentTable: table, - referencedTable: - $$PollsTableReferences._pollVotesRefsTable(db), - managerFromTypedResult: (p0) => - $$PollsTableReferences(db, table, p0).pollVotesRefs, - referencedItemsForCurrentItem: (item, - referencedItems) => - referencedItems.where((e) => e.pollId == item.id), - typedResults: items) + await $_getPrefetchedData( + currentTable: table, + referencedTable: $$PollsTableReferences._pollVotesRefsTable(db), + managerFromTypedResult: (p0) => $$PollsTableReferences(db, table, p0).pollVotesRefs, + referencedItemsForCurrentItem: (item, referencedItems) => + referencedItems.where((e) => e.pollId == item.id), + typedResults: items, + ), ]; }, ); }, - )); + ), + ); } -typedef $$PollsTableProcessedTableManager = ProcessedTableManager< - _$DriftChatDatabase, - $PollsTable, - PollEntity, - $$PollsTableFilterComposer, - $$PollsTableOrderingComposer, - $$PollsTableAnnotationComposer, - $$PollsTableCreateCompanionBuilder, - $$PollsTableUpdateCompanionBuilder, - (PollEntity, $$PollsTableReferences), - PollEntity, - PrefetchHooks Function({bool pollVotesRefs})>; -typedef $$PollVotesTableCreateCompanionBuilder = PollVotesCompanion Function({ - Value id, - Value pollId, - Value optionId, - Value answerText, - Value createdAt, - Value updatedAt, - Value userId, - Value rowid, -}); -typedef $$PollVotesTableUpdateCompanionBuilder = PollVotesCompanion Function({ - Value id, - Value pollId, - Value optionId, - Value answerText, - Value createdAt, - Value updatedAt, - Value userId, - Value rowid, -}); - -final class $$PollVotesTableReferences extends BaseReferences< - _$DriftChatDatabase, $PollVotesTable, PollVoteEntity> { +typedef $$PollsTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $PollsTable, + PollEntity, + $$PollsTableFilterComposer, + $$PollsTableOrderingComposer, + $$PollsTableAnnotationComposer, + $$PollsTableCreateCompanionBuilder, + $$PollsTableUpdateCompanionBuilder, + (PollEntity, $$PollsTableReferences), + PollEntity, + PrefetchHooks Function({bool pollVotesRefs}) + >; +typedef $$PollVotesTableCreateCompanionBuilder = + PollVotesCompanion Function({ + Value id, + Value pollId, + Value optionId, + Value answerText, + Value createdAt, + Value updatedAt, + Value userId, + Value rowid, + }); +typedef $$PollVotesTableUpdateCompanionBuilder = + PollVotesCompanion Function({ + Value id, + Value pollId, + Value optionId, + Value answerText, + Value createdAt, + Value updatedAt, + Value userId, + Value rowid, + }); + +final class $$PollVotesTableReferences extends BaseReferences<_$DriftChatDatabase, $PollVotesTable, PollVoteEntity> { $$PollVotesTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $PollsTable _pollIdTable(_$DriftChatDatabase db) => db.polls - .createAlias($_aliasNameGenerator(db.pollVotes.pollId, db.polls.id)); + static $PollsTable _pollIdTable(_$DriftChatDatabase db) => + db.polls.createAlias($_aliasNameGenerator(db.pollVotes.pollId, db.polls.id)); $$PollsTableProcessedTableManager? get pollId { - if ($_item.pollId == null) return null; - final manager = $$PollsTableTableManager($_db, $_db.polls) - .filter((f) => f.id($_item.pollId!)); + final $_column = $_itemColumn('poll_id'); + if ($_column == null) return null; + final manager = $$PollsTableTableManager($_db, $_db.polls).filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_pollIdTable($_db)); if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: [item])); } } -class $$PollVotesTableFilterComposer - extends Composer<_$DriftChatDatabase, $PollVotesTable> { +class $$PollVotesTableFilterComposer extends Composer<_$DriftChatDatabase, $PollVotesTable> { $$PollVotesTableFilterComposer({ required super.$db, required super.$table, @@ -11966,47 +12810,43 @@ class $$PollVotesTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get optionId => $composableBuilder( - column: $table.optionId, builder: (column) => ColumnFilters(column)); + ColumnFilters get optionId => + $composableBuilder(column: $table.optionId, builder: (column) => ColumnFilters(column)); - ColumnFilters get answerText => $composableBuilder( - column: $table.answerText, builder: (column) => ColumnFilters(column)); + ColumnFilters get answerText => + $composableBuilder(column: $table.answerText, builder: (column) => ColumnFilters(column)); - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, 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 updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnFilters(column)); + ColumnFilters get userId => + $composableBuilder(column: $table.userId, builder: (column) => ColumnFilters(column)); $$PollsTableFilterComposer get pollId { final $$PollsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.pollId, - referencedTable: $db.polls, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PollsTableFilterComposer( - $db: $db, - $table: $db.polls, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.pollId, + referencedTable: $db.polls, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$PollsTableFilterComposer( + $db: $db, + $table: $db.polls, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } -class $$PollVotesTableOrderingComposer - extends Composer<_$DriftChatDatabase, $PollVotesTable> { +class $$PollVotesTableOrderingComposer extends Composer<_$DriftChatDatabase, $PollVotesTable> { $$PollVotesTableOrderingComposer({ required super.$db, required super.$table, @@ -12014,47 +12854,43 @@ class $$PollVotesTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get optionId => $composableBuilder( - column: $table.optionId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get optionId => + $composableBuilder(column: $table.optionId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get answerText => $composableBuilder( - column: $table.answerText, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get answerText => + $composableBuilder(column: $table.answerText, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, 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 updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get userId => + $composableBuilder(column: $table.userId, builder: (column) => ColumnOrderings(column)); $$PollsTableOrderingComposer get pollId { final $$PollsTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.pollId, - referencedTable: $db.polls, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PollsTableOrderingComposer( - $db: $db, - $table: $db.polls, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.pollId, + referencedTable: $db.polls, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$PollsTableOrderingComposer( + $db: $db, + $table: $db.polls, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } -class $$PollVotesTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $PollVotesTable> { +class $$PollVotesTableAnnotationComposer extends Composer<_$DriftChatDatabase, $PollVotesTable> { $$PollVotesTableAnnotationComposer({ required super.$db, required super.$table, @@ -12062,119 +12898,109 @@ class $$PollVotesTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get optionId => - $composableBuilder(column: $table.optionId, builder: (column) => column); + GeneratedColumn get optionId => $composableBuilder(column: $table.optionId, builder: (column) => column); - GeneratedColumn get answerText => $composableBuilder( - column: $table.answerText, builder: (column) => column); + GeneratedColumn get answerText => $composableBuilder(column: $table.answerText, builder: (column) => column); - GeneratedColumn get createdAt => - $composableBuilder(column: $table.createdAt, 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 updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); - GeneratedColumn get userId => - $composableBuilder(column: $table.userId, builder: (column) => column); + GeneratedColumn get userId => $composableBuilder(column: $table.userId, builder: (column) => column); $$PollsTableAnnotationComposer get pollId { final $$PollsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.pollId, - referencedTable: $db.polls, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PollsTableAnnotationComposer( - $db: $db, - $table: $db.polls, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.pollId, + referencedTable: $db.polls, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$PollsTableAnnotationComposer( + $db: $db, + $table: $db.polls, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } -class $$PollVotesTableTableManager extends RootTableManager< - _$DriftChatDatabase, - $PollVotesTable, - PollVoteEntity, - $$PollVotesTableFilterComposer, - $$PollVotesTableOrderingComposer, - $$PollVotesTableAnnotationComposer, - $$PollVotesTableCreateCompanionBuilder, - $$PollVotesTableUpdateCompanionBuilder, - (PollVoteEntity, $$PollVotesTableReferences), - PollVoteEntity, - PrefetchHooks Function({bool pollId})> { +class $$PollVotesTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $PollVotesTable, + PollVoteEntity, + $$PollVotesTableFilterComposer, + $$PollVotesTableOrderingComposer, + $$PollVotesTableAnnotationComposer, + $$PollVotesTableCreateCompanionBuilder, + $$PollVotesTableUpdateCompanionBuilder, + (PollVoteEntity, $$PollVotesTableReferences), + PollVoteEntity, + PrefetchHooks Function({bool pollId}) + > { $$PollVotesTableTableManager(_$DriftChatDatabase db, $PollVotesTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$PollVotesTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$PollVotesTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$PollVotesTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value pollId = const Value.absent(), - Value optionId = const Value.absent(), - Value answerText = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value userId = const Value.absent(), - Value rowid = const Value.absent(), - }) => - PollVotesCompanion( - id: id, - pollId: pollId, - optionId: optionId, - answerText: answerText, - createdAt: createdAt, - updatedAt: updatedAt, - userId: userId, - rowid: rowid, - ), - createCompanionCallback: ({ - Value id = const Value.absent(), - Value pollId = const Value.absent(), - Value optionId = const Value.absent(), - Value answerText = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value userId = const Value.absent(), - Value rowid = const Value.absent(), - }) => - PollVotesCompanion.insert( - id: id, - pollId: pollId, - optionId: optionId, - answerText: answerText, - createdAt: createdAt, - updatedAt: updatedAt, - userId: userId, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => ( - e.readTable(table), - $$PollVotesTableReferences(db, table, e) - )) - .toList(), + createFilteringComposer: () => $$PollVotesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$PollVotesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$PollVotesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value pollId = const Value.absent(), + Value optionId = const Value.absent(), + Value answerText = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value userId = const Value.absent(), + Value rowid = const Value.absent(), + }) => PollVotesCompanion( + id: id, + pollId: pollId, + optionId: optionId, + answerText: answerText, + createdAt: createdAt, + updatedAt: updatedAt, + userId: userId, + rowid: rowid, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + Value pollId = const Value.absent(), + Value optionId = const Value.absent(), + Value answerText = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value userId = const Value.absent(), + Value rowid = const Value.absent(), + }) => PollVotesCompanion.insert( + id: id, + pollId: pollId, + optionId: optionId, + answerText: answerText, + createdAt: createdAt, + updatedAt: updatedAt, + userId: userId, + rowid: rowid, + ), + withReferenceMapper: (p0) => + p0.map((e) => (e.readTable(table), $$PollVotesTableReferences(db, table, e))).toList(), prefetchHooksCallback: ({pollId = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], - addJoins: < - T extends TableManagerState< + addJoins: + < + T extends TableManagerState< dynamic, dynamic, dynamic, @@ -12185,84 +13011,91 @@ class $$PollVotesTableTableManager extends RootTableManager< dynamic, dynamic, dynamic, - dynamic>>(state) { - if (pollId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.pollId, - referencedTable: - $$PollVotesTableReferences._pollIdTable(db), - referencedColumn: - $$PollVotesTableReferences._pollIdTable(db).id, - ) as T; - } - - return state; - }, + dynamic + > + >(state) { + if (pollId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.pollId, + referencedTable: $$PollVotesTableReferences._pollIdTable(db), + referencedColumn: $$PollVotesTableReferences._pollIdTable(db).id, + ) + as T; + } + + return state; + }, getPrefetchedDataCallback: (items) async { return []; }, ); }, - )); + ), + ); } -typedef $$PollVotesTableProcessedTableManager = ProcessedTableManager< - _$DriftChatDatabase, - $PollVotesTable, - PollVoteEntity, - $$PollVotesTableFilterComposer, - $$PollVotesTableOrderingComposer, - $$PollVotesTableAnnotationComposer, - $$PollVotesTableCreateCompanionBuilder, - $$PollVotesTableUpdateCompanionBuilder, - (PollVoteEntity, $$PollVotesTableReferences), - PollVoteEntity, - PrefetchHooks Function({bool pollId})>; -typedef $$PinnedMessageReactionsTableCreateCompanionBuilder - = PinnedMessageReactionsCompanion Function({ - required String userId, - required String messageId, - required String type, - Value createdAt, - Value score, - Value?> extraData, - Value rowid, -}); -typedef $$PinnedMessageReactionsTableUpdateCompanionBuilder - = PinnedMessageReactionsCompanion Function({ - Value userId, - Value messageId, - Value type, - Value createdAt, - Value score, - Value?> extraData, - Value rowid, -}); - -final class $$PinnedMessageReactionsTableReferences extends BaseReferences< - _$DriftChatDatabase, - $PinnedMessageReactionsTable, - PinnedMessageReactionEntity> { - $$PinnedMessageReactionsTableReferences( - super.$_db, super.$_table, super.$_typedResult); +typedef $$PollVotesTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $PollVotesTable, + PollVoteEntity, + $$PollVotesTableFilterComposer, + $$PollVotesTableOrderingComposer, + $$PollVotesTableAnnotationComposer, + $$PollVotesTableCreateCompanionBuilder, + $$PollVotesTableUpdateCompanionBuilder, + (PollVoteEntity, $$PollVotesTableReferences), + PollVoteEntity, + PrefetchHooks Function({bool pollId}) + >; +typedef $$PinnedMessageReactionsTableCreateCompanionBuilder = + PinnedMessageReactionsCompanion Function({ + 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 type, + Value emojiCode, + Value createdAt, + Value updatedAt, + Value score, + Value?> extraData, + Value rowid, + }); + +final class $$PinnedMessageReactionsTableReferences + extends BaseReferences<_$DriftChatDatabase, $PinnedMessageReactionsTable, PinnedMessageReactionEntity> { + $$PinnedMessageReactionsTableReferences(super.$_db, super.$_table, super.$_typedResult); static $PinnedMessagesTable _messageIdTable(_$DriftChatDatabase db) => - db.pinnedMessages.createAlias($_aliasNameGenerator( - db.pinnedMessageReactions.messageId, db.pinnedMessages.id)); - - $$PinnedMessagesTableProcessedTableManager get messageId { - final manager = $$PinnedMessagesTableTableManager($_db, $_db.pinnedMessages) - .filter((f) => f.id($_item.messageId!)); + db.pinnedMessages.createAlias($_aliasNameGenerator(db.pinnedMessageReactions.messageId, db.pinnedMessages.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)); if (item == null) return manager; - return ProcessedTableManager( - manager.$state.copyWith(prefetchedData: [item])); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: [item])); } } -class $$PinnedMessageReactionsTableFilterComposer - extends Composer<_$DriftChatDatabase, $PinnedMessageReactionsTable> { +class $$PinnedMessageReactionsTableFilterComposer extends Composer<_$DriftChatDatabase, $PinnedMessageReactionsTable> { $$PinnedMessageReactionsTableFilterComposer({ required super.$db, required super.$table, @@ -12270,41 +13103,40 @@ class $$PinnedMessageReactionsTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnFilters(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 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 createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get score => $composableBuilder( - column: $table.score, builder: (column) => ColumnFilters(column)); + ColumnFilters get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, Map, - String> - get extraData => $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnFilters get score => $composableBuilder(column: $table.score, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters?, Map, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnWithTypeConverterFilters(column)); $$PinnedMessagesTableFilterComposer get messageId { final $$PinnedMessagesTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.messageId, - referencedTable: $db.pinnedMessages, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PinnedMessagesTableFilterComposer( - $db: $db, - $table: $db.pinnedMessages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.pinnedMessages, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$PinnedMessagesTableFilterComposer( + $db: $db, + $table: $db.pinnedMessages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } @@ -12318,38 +13150,42 @@ class $$PinnedMessageReactionsTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get userId => + $composableBuilder(column: $table.userId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => + $composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column)); - 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 createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get score => $composableBuilder( - column: $table.score, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get score => + $composableBuilder(column: $table.score, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnOrderings(column)); $$PinnedMessagesTableOrderingComposer get messageId { final $$PinnedMessagesTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.messageId, - referencedTable: $db.pinnedMessages, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PinnedMessagesTableOrderingComposer( - $db: $db, - $table: $db.pinnedMessages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.pinnedMessages, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$PinnedMessagesTableOrderingComposer( + $db: $db, + $table: $db.pinnedMessages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } @@ -12363,117 +13199,116 @@ class $$PinnedMessageReactionsTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get userId => - $composableBuilder(column: $table.userId, builder: (column) => column); + GeneratedColumn get userId => $composableBuilder(column: $table.userId, builder: (column) => column); + + GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); - 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 createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); - GeneratedColumn get score => - $composableBuilder(column: $table.score, builder: (column) => column); + GeneratedColumn get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> - get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => column); + GeneratedColumn get score => $composableBuilder(column: $table.score, builder: (column) => column); + + GeneratedColumnWithTypeConverter?, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => column); $$PinnedMessagesTableAnnotationComposer get messageId { final $$PinnedMessagesTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.messageId, - referencedTable: $db.pinnedMessages, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$PinnedMessagesTableAnnotationComposer( - $db: $db, - $table: $db.pinnedMessages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.pinnedMessages, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$PinnedMessagesTableAnnotationComposer( + $db: $db, + $table: $db.pinnedMessages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } -class $$PinnedMessageReactionsTableTableManager extends RootTableManager< - _$DriftChatDatabase, - $PinnedMessageReactionsTable, - PinnedMessageReactionEntity, - $$PinnedMessageReactionsTableFilterComposer, - $$PinnedMessageReactionsTableOrderingComposer, - $$PinnedMessageReactionsTableAnnotationComposer, - $$PinnedMessageReactionsTableCreateCompanionBuilder, - $$PinnedMessageReactionsTableUpdateCompanionBuilder, - (PinnedMessageReactionEntity, $$PinnedMessageReactionsTableReferences), - PinnedMessageReactionEntity, - PrefetchHooks Function({bool messageId})> { - $$PinnedMessageReactionsTableTableManager( - _$DriftChatDatabase db, $PinnedMessageReactionsTable table) - : super(TableManagerState( +class $$PinnedMessageReactionsTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $PinnedMessageReactionsTable, + PinnedMessageReactionEntity, + $$PinnedMessageReactionsTableFilterComposer, + $$PinnedMessageReactionsTableOrderingComposer, + $$PinnedMessageReactionsTableAnnotationComposer, + $$PinnedMessageReactionsTableCreateCompanionBuilder, + $$PinnedMessageReactionsTableUpdateCompanionBuilder, + (PinnedMessageReactionEntity, $$PinnedMessageReactionsTableReferences), + PinnedMessageReactionEntity, + PrefetchHooks Function({bool messageId}) + > { + $$PinnedMessageReactionsTableTableManager(_$DriftChatDatabase db, $PinnedMessageReactionsTable table) + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$PinnedMessageReactionsTableFilterComposer( - $db: db, $table: table), - createOrderingComposer: () => - $$PinnedMessageReactionsTableOrderingComposer( - $db: db, $table: table), - createComputedFieldComposer: () => - $$PinnedMessageReactionsTableAnnotationComposer( - $db: db, $table: table), - updateCompanionCallback: ({ - Value userId = const Value.absent(), - Value messageId = const Value.absent(), - Value type = const Value.absent(), - Value createdAt = const Value.absent(), - Value score = const Value.absent(), - Value?> extraData = const Value.absent(), - Value rowid = const Value.absent(), - }) => - PinnedMessageReactionsCompanion( - userId: userId, - messageId: messageId, - type: type, - createdAt: createdAt, - score: score, - extraData: extraData, - rowid: rowid, - ), - createCompanionCallback: ({ - required String userId, - required String messageId, - required String type, - Value createdAt = const Value.absent(), - Value score = const Value.absent(), - Value?> extraData = const Value.absent(), - Value rowid = const Value.absent(), - }) => - PinnedMessageReactionsCompanion.insert( - userId: userId, - messageId: messageId, - type: type, - createdAt: createdAt, - score: score, - extraData: extraData, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => ( - e.readTable(table), - $$PinnedMessageReactionsTableReferences(db, table, e) - )) - .toList(), + createFilteringComposer: () => $$PinnedMessageReactionsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$PinnedMessageReactionsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$PinnedMessageReactionsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + 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(), + }) => PinnedMessageReactionsCompanion( + userId: userId, + messageId: messageId, + type: type, + emojiCode: emojiCode, + createdAt: createdAt, + updatedAt: updatedAt, + score: score, + extraData: extraData, + rowid: rowid, + ), + createCompanionCallback: + ({ + 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(), + }) => PinnedMessageReactionsCompanion.insert( + userId: userId, + messageId: messageId, + type: type, + emojiCode: emojiCode, + createdAt: createdAt, + updatedAt: updatedAt, + score: score, + extraData: extraData, + rowid: rowid, + ), + withReferenceMapper: (p0) => + p0.map((e) => (e.readTable(table), $$PinnedMessageReactionsTableReferences(db, table, e))).toList(), prefetchHooksCallback: ({messageId = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], - addJoins: < - T extends TableManagerState< + addJoins: + < + T extends TableManagerState< dynamic, dynamic, dynamic, @@ -12484,81 +13319,87 @@ class $$PinnedMessageReactionsTableTableManager extends RootTableManager< dynamic, dynamic, dynamic, - dynamic>>(state) { - if (messageId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.messageId, - referencedTable: $$PinnedMessageReactionsTableReferences - ._messageIdTable(db), - referencedColumn: $$PinnedMessageReactionsTableReferences - ._messageIdTable(db) - .id, - ) as T; - } - - return state; - }, + dynamic + > + >(state) { + if (messageId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.messageId, + referencedTable: $$PinnedMessageReactionsTableReferences._messageIdTable(db), + referencedColumn: $$PinnedMessageReactionsTableReferences._messageIdTable(db).id, + ) + as T; + } + + return state; + }, getPrefetchedDataCallback: (items) async { return []; }, ); }, - )); + ), + ); } -typedef $$PinnedMessageReactionsTableProcessedTableManager - = ProcessedTableManager< - _$DriftChatDatabase, - $PinnedMessageReactionsTable, - PinnedMessageReactionEntity, - $$PinnedMessageReactionsTableFilterComposer, - $$PinnedMessageReactionsTableOrderingComposer, - $$PinnedMessageReactionsTableAnnotationComposer, - $$PinnedMessageReactionsTableCreateCompanionBuilder, - $$PinnedMessageReactionsTableUpdateCompanionBuilder, - (PinnedMessageReactionEntity, $$PinnedMessageReactionsTableReferences), - PinnedMessageReactionEntity, - PrefetchHooks Function({bool messageId})>; -typedef $$ReactionsTableCreateCompanionBuilder = ReactionsCompanion Function({ - required String userId, - required String messageId, - required String type, - Value createdAt, - Value score, - Value?> extraData, - Value rowid, -}); -typedef $$ReactionsTableUpdateCompanionBuilder = ReactionsCompanion Function({ - Value userId, - Value messageId, - Value type, - Value createdAt, - Value score, - Value?> extraData, - Value rowid, -}); - -final class $$ReactionsTableReferences extends BaseReferences< - _$DriftChatDatabase, $ReactionsTable, ReactionEntity> { +typedef $$PinnedMessageReactionsTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $PinnedMessageReactionsTable, + PinnedMessageReactionEntity, + $$PinnedMessageReactionsTableFilterComposer, + $$PinnedMessageReactionsTableOrderingComposer, + $$PinnedMessageReactionsTableAnnotationComposer, + $$PinnedMessageReactionsTableCreateCompanionBuilder, + $$PinnedMessageReactionsTableUpdateCompanionBuilder, + (PinnedMessageReactionEntity, $$PinnedMessageReactionsTableReferences), + PinnedMessageReactionEntity, + PrefetchHooks Function({bool messageId}) + >; +typedef $$ReactionsTableCreateCompanionBuilder = + ReactionsCompanion Function({ + 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 type, + Value emojiCode, + Value createdAt, + Value updatedAt, + Value score, + Value?> extraData, + Value rowid, + }); + +final class $$ReactionsTableReferences extends BaseReferences<_$DriftChatDatabase, $ReactionsTable, ReactionEntity> { $$ReactionsTableReferences(super.$_db, super.$_table, super.$_typedResult); static $MessagesTable _messageIdTable(_$DriftChatDatabase db) => - db.messages.createAlias( - $_aliasNameGenerator(db.reactions.messageId, db.messages.id)); + db.messages.createAlias($_aliasNameGenerator(db.reactions.messageId, db.messages.id)); - $$MessagesTableProcessedTableManager get messageId { - final manager = $$MessagesTableTableManager($_db, $_db.messages) - .filter((f) => f.id($_item.messageId!)); + $$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])); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: [item])); } } -class $$ReactionsTableFilterComposer - extends Composer<_$DriftChatDatabase, $ReactionsTable> { +class $$ReactionsTableFilterComposer extends Composer<_$DriftChatDatabase, $ReactionsTable> { $$ReactionsTableFilterComposer({ required super.$db, required super.$table, @@ -12566,47 +13407,45 @@ class $$ReactionsTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnFilters(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 emojiCode => + $composableBuilder(column: $table.emojiCode, builder: (column) => ColumnFilters(column)); - ColumnFilters get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnFilters(column)); + ColumnFilters get createdAt => + $composableBuilder(column: $table.createdAt, 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)); + ColumnFilters get score => $composableBuilder(column: $table.score, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, Map, - String> - get extraData => $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, Map, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnWithTypeConverterFilters(column)); $$MessagesTableFilterComposer get messageId { final $$MessagesTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.messageId, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$MessagesTableFilterComposer( - $db: $db, - $table: $db.messages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$MessagesTableFilterComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } -class $$ReactionsTableOrderingComposer - extends Composer<_$DriftChatDatabase, $ReactionsTable> { +class $$ReactionsTableOrderingComposer extends Composer<_$DriftChatDatabase, $ReactionsTable> { $$ReactionsTableOrderingComposer({ required super.$db, required super.$table, @@ -12614,44 +13453,47 @@ class $$ReactionsTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get userId => + $composableBuilder(column: $table.userId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get type => + $composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get emojiCode => + $composableBuilder(column: $table.emojiCode, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdAt => + $composableBuilder(column: $table.createdAt, 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)); + ColumnOrderings get score => + $composableBuilder(column: $table.score, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnOrderings(column)); $$MessagesTableOrderingComposer get messageId { final $$MessagesTableOrderingComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.messageId, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$MessagesTableOrderingComposer( - $db: $db, - $table: $db.messages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.messageId, + 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; } } -class $$ReactionsTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $ReactionsTable> { +class $$ReactionsTableAnnotationComposer extends Composer<_$DriftChatDatabase, $ReactionsTable> { $$ReactionsTableAnnotationComposer({ required super.$db, required super.$table, @@ -12659,113 +13501,116 @@ class $$ReactionsTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get userId => - $composableBuilder(column: $table.userId, builder: (column) => column); + GeneratedColumn get userId => $composableBuilder(column: $table.userId, builder: (column) => column); + + GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get emojiCode => $composableBuilder(column: $table.emojiCode, builder: (column) => column); - GeneratedColumn get type => - $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, 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); + GeneratedColumn get score => $composableBuilder(column: $table.score, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> - get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => column); $$MessagesTableAnnotationComposer get messageId { final $$MessagesTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.messageId, - referencedTable: $db.messages, - getReferencedColumn: (t) => t.id, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$MessagesTableAnnotationComposer( - $db: $db, - $table: $db.messages, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.messageId, + referencedTable: $db.messages, + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$MessagesTableAnnotationComposer( + $db: $db, + $table: $db.messages, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } -class $$ReactionsTableTableManager extends RootTableManager< - _$DriftChatDatabase, - $ReactionsTable, - ReactionEntity, - $$ReactionsTableFilterComposer, - $$ReactionsTableOrderingComposer, - $$ReactionsTableAnnotationComposer, - $$ReactionsTableCreateCompanionBuilder, - $$ReactionsTableUpdateCompanionBuilder, - (ReactionEntity, $$ReactionsTableReferences), - ReactionEntity, - PrefetchHooks Function({bool messageId})> { +class $$ReactionsTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $ReactionsTable, + ReactionEntity, + $$ReactionsTableFilterComposer, + $$ReactionsTableOrderingComposer, + $$ReactionsTableAnnotationComposer, + $$ReactionsTableCreateCompanionBuilder, + $$ReactionsTableUpdateCompanionBuilder, + (ReactionEntity, $$ReactionsTableReferences), + ReactionEntity, + PrefetchHooks Function({bool messageId}) + > { $$ReactionsTableTableManager(_$DriftChatDatabase db, $ReactionsTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$ReactionsTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$ReactionsTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$ReactionsTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value userId = const Value.absent(), - Value messageId = const Value.absent(), - Value type = const Value.absent(), - Value createdAt = const Value.absent(), - Value score = const Value.absent(), - Value?> extraData = const Value.absent(), - Value rowid = const Value.absent(), - }) => - ReactionsCompanion( - userId: userId, - messageId: messageId, - type: type, - createdAt: createdAt, - score: score, - extraData: extraData, - rowid: rowid, - ), - createCompanionCallback: ({ - required String userId, - required String messageId, - required String type, - Value createdAt = const Value.absent(), - Value score = const Value.absent(), - Value?> extraData = const Value.absent(), - Value rowid = const Value.absent(), - }) => - ReactionsCompanion.insert( - userId: userId, - messageId: messageId, - type: type, - createdAt: createdAt, - score: score, - extraData: extraData, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => ( - e.readTable(table), - $$ReactionsTableReferences(db, table, e) - )) - .toList(), + createFilteringComposer: () => $$ReactionsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$ReactionsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$ReactionsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + 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(), + }) => ReactionsCompanion( + userId: userId, + messageId: messageId, + type: type, + emojiCode: emojiCode, + createdAt: createdAt, + updatedAt: updatedAt, + score: score, + extraData: extraData, + rowid: rowid, + ), + createCompanionCallback: + ({ + 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(), + }) => ReactionsCompanion.insert( + userId: userId, + messageId: messageId, + type: type, + emojiCode: emojiCode, + createdAt: createdAt, + updatedAt: updatedAt, + score: score, + extraData: extraData, + rowid: rowid, + ), + withReferenceMapper: (p0) => + p0.map((e) => (e.readTable(table), $$ReactionsTableReferences(db, table, e))).toList(), prefetchHooksCallback: ({messageId = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], - addJoins: < - T extends TableManagerState< + addJoins: + < + T extends TableManagerState< dynamic, dynamic, dynamic, @@ -12776,71 +13621,77 @@ class $$ReactionsTableTableManager extends RootTableManager< dynamic, dynamic, dynamic, - dynamic>>(state) { - if (messageId) { - state = state.withJoin( - currentTable: table, - currentColumn: table.messageId, - referencedTable: - $$ReactionsTableReferences._messageIdTable(db), - referencedColumn: - $$ReactionsTableReferences._messageIdTable(db).id, - ) as T; - } - - return state; - }, + dynamic + > + >(state) { + if (messageId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.messageId, + referencedTable: $$ReactionsTableReferences._messageIdTable(db), + referencedColumn: $$ReactionsTableReferences._messageIdTable(db).id, + ) + as T; + } + + return state; + }, getPrefetchedDataCallback: (items) async { return []; }, ); }, - )); + ), + ); } -typedef $$ReactionsTableProcessedTableManager = ProcessedTableManager< - _$DriftChatDatabase, - $ReactionsTable, - ReactionEntity, - $$ReactionsTableFilterComposer, - $$ReactionsTableOrderingComposer, - $$ReactionsTableAnnotationComposer, - $$ReactionsTableCreateCompanionBuilder, - $$ReactionsTableUpdateCompanionBuilder, - (ReactionEntity, $$ReactionsTableReferences), - ReactionEntity, - PrefetchHooks Function({bool messageId})>; -typedef $$UsersTableCreateCompanionBuilder = UsersCompanion Function({ - required String id, - Value role, - Value language, - Value createdAt, - Value updatedAt, - Value lastActive, - Value online, - Value banned, - Value?> teamsRole, - Value avgResponseTime, - required Map extraData, - Value rowid, -}); -typedef $$UsersTableUpdateCompanionBuilder = UsersCompanion Function({ - Value id, - Value role, - Value language, - Value createdAt, - Value updatedAt, - Value lastActive, - Value online, - Value banned, - Value?> teamsRole, - Value avgResponseTime, - Value> extraData, - Value rowid, -}); - -class $$UsersTableFilterComposer - extends Composer<_$DriftChatDatabase, $UsersTable> { +typedef $$ReactionsTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $ReactionsTable, + ReactionEntity, + $$ReactionsTableFilterComposer, + $$ReactionsTableOrderingComposer, + $$ReactionsTableAnnotationComposer, + $$ReactionsTableCreateCompanionBuilder, + $$ReactionsTableUpdateCompanionBuilder, + (ReactionEntity, $$ReactionsTableReferences), + ReactionEntity, + PrefetchHooks Function({bool messageId}) + >; +typedef $$UsersTableCreateCompanionBuilder = + UsersCompanion Function({ + required String id, + Value role, + Value language, + Value createdAt, + Value updatedAt, + Value lastActive, + Value online, + Value banned, + Value?> teamsRole, + Value avgResponseTime, + required Map extraData, + Value rowid, + }); +typedef $$UsersTableUpdateCompanionBuilder = + UsersCompanion Function({ + Value id, + Value role, + Value language, + Value createdAt, + Value updatedAt, + Value lastActive, + Value online, + Value banned, + Value?> teamsRole, + Value avgResponseTime, + Value> extraData, + Value rowid, + }); + +class $$UsersTableFilterComposer extends Composer<_$DriftChatDatabase, $UsersTable> { $$UsersTableFilterComposer({ required super.$db, required super.$table, @@ -12848,49 +13699,39 @@ class $$UsersTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get role => $composableBuilder( - column: $table.role, builder: (column) => ColumnFilters(column)); + ColumnFilters get role => $composableBuilder(column: $table.role, builder: (column) => ColumnFilters(column)); - ColumnFilters get language => $composableBuilder( - column: $table.language, builder: (column) => ColumnFilters(column)); + ColumnFilters get language => + $composableBuilder(column: $table.language, builder: (column) => ColumnFilters(column)); - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, 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 updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get lastActive => $composableBuilder( - column: $table.lastActive, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastActive => + $composableBuilder(column: $table.lastActive, builder: (column) => ColumnFilters(column)); - ColumnFilters get online => $composableBuilder( - column: $table.online, builder: (column) => ColumnFilters(column)); + ColumnFilters get online => + $composableBuilder(column: $table.online, builder: (column) => ColumnFilters(column)); - ColumnFilters get banned => $composableBuilder( - column: $table.banned, builder: (column) => ColumnFilters(column)); + ColumnFilters get banned => + $composableBuilder(column: $table.banned, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, Map, - String> - get teamsRole => $composableBuilder( - column: $table.teamsRole, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, Map, String> get teamsRole => + $composableBuilder(column: $table.teamsRole, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get avgResponseTime => $composableBuilder( - column: $table.avgResponseTime, - builder: (column) => ColumnFilters(column)); + ColumnFilters get avgResponseTime => + $composableBuilder(column: $table.avgResponseTime, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters, Map, - String> - get extraData => $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters, Map, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnWithTypeConverterFilters(column)); } -class $$UsersTableOrderingComposer - extends Composer<_$DriftChatDatabase, $UsersTable> { +class $$UsersTableOrderingComposer extends Composer<_$DriftChatDatabase, $UsersTable> { $$UsersTableOrderingComposer({ required super.$db, required super.$table, @@ -12898,43 +13739,40 @@ class $$UsersTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get role => $composableBuilder( - column: $table.role, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get role => + $composableBuilder(column: $table.role, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get language => $composableBuilder( - column: $table.language, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get language => + $composableBuilder(column: $table.language, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, 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 updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get lastActive => $composableBuilder( - column: $table.lastActive, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastActive => + $composableBuilder(column: $table.lastActive, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get online => $composableBuilder( - column: $table.online, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get online => + $composableBuilder(column: $table.online, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get banned => $composableBuilder( - column: $table.banned, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get banned => + $composableBuilder(column: $table.banned, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get teamsRole => $composableBuilder( - column: $table.teamsRole, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get teamsRole => + $composableBuilder(column: $table.teamsRole, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get avgResponseTime => $composableBuilder( - column: $table.avgResponseTime, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get avgResponseTime => + $composableBuilder(column: $table.avgResponseTime, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnOrderings(column)); } -class $$UsersTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $UsersTable> { +class $$UsersTableAnnotationComposer extends Composer<_$DriftChatDatabase, $UsersTable> { $$UsersTableAnnotationComposer({ required super.$db, required super.$table, @@ -12942,194 +13780,188 @@ class $$UsersTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get role => - $composableBuilder(column: $table.role, builder: (column) => column); + GeneratedColumn get role => $composableBuilder(column: $table.role, builder: (column) => column); - GeneratedColumn get language => - $composableBuilder(column: $table.language, builder: (column) => column); + GeneratedColumn get language => $composableBuilder(column: $table.language, builder: (column) => column); - GeneratedColumn get createdAt => - $composableBuilder(column: $table.createdAt, 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 updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); - GeneratedColumn get lastActive => $composableBuilder( - column: $table.lastActive, builder: (column) => column); + GeneratedColumn get lastActive => + $composableBuilder(column: $table.lastActive, builder: (column) => column); - GeneratedColumn get online => - $composableBuilder(column: $table.online, builder: (column) => column); + GeneratedColumn get online => $composableBuilder(column: $table.online, builder: (column) => column); - GeneratedColumn get banned => - $composableBuilder(column: $table.banned, builder: (column) => column); + GeneratedColumn get banned => $composableBuilder(column: $table.banned, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> - get teamsRole => $composableBuilder( - column: $table.teamsRole, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get teamsRole => + $composableBuilder(column: $table.teamsRole, builder: (column) => column); - GeneratedColumn get avgResponseTime => $composableBuilder( - column: $table.avgResponseTime, builder: (column) => column); + GeneratedColumn get avgResponseTime => + $composableBuilder(column: $table.avgResponseTime, builder: (column) => column); - GeneratedColumnWithTypeConverter, String> - get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => column); + GeneratedColumnWithTypeConverter, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => column); } -class $$UsersTableTableManager extends RootTableManager< - _$DriftChatDatabase, - $UsersTable, - UserEntity, - $$UsersTableFilterComposer, - $$UsersTableOrderingComposer, - $$UsersTableAnnotationComposer, - $$UsersTableCreateCompanionBuilder, - $$UsersTableUpdateCompanionBuilder, - (UserEntity, BaseReferences<_$DriftChatDatabase, $UsersTable, UserEntity>), - UserEntity, - PrefetchHooks Function()> { +class $$UsersTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $UsersTable, + UserEntity, + $$UsersTableFilterComposer, + $$UsersTableOrderingComposer, + $$UsersTableAnnotationComposer, + $$UsersTableCreateCompanionBuilder, + $$UsersTableUpdateCompanionBuilder, + (UserEntity, BaseReferences<_$DriftChatDatabase, $UsersTable, UserEntity>), + UserEntity, + PrefetchHooks Function() + > { $$UsersTableTableManager(_$DriftChatDatabase db, $UsersTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$UsersTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$UsersTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$UsersTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - 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(), - Value online = const Value.absent(), - Value banned = const Value.absent(), - Value?> teamsRole = const Value.absent(), - Value avgResponseTime = const Value.absent(), - Value> extraData = const Value.absent(), - Value rowid = const Value.absent(), - }) => - UsersCompanion( - id: id, - role: role, - language: language, - createdAt: createdAt, - updatedAt: updatedAt, - lastActive: lastActive, - online: online, - banned: banned, - teamsRole: teamsRole, - avgResponseTime: avgResponseTime, - extraData: extraData, - rowid: rowid, - ), - createCompanionCallback: ({ - required 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(), - Value online = const Value.absent(), - Value banned = const Value.absent(), - Value?> teamsRole = const Value.absent(), - Value avgResponseTime = const Value.absent(), - required Map extraData, - Value rowid = const Value.absent(), - }) => - UsersCompanion.insert( - id: id, - role: role, - language: language, - createdAt: createdAt, - updatedAt: updatedAt, - lastActive: lastActive, - online: online, - banned: banned, - teamsRole: teamsRole, - avgResponseTime: avgResponseTime, - extraData: extraData, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: () => $$UsersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$UsersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$UsersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + 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(), + Value online = const Value.absent(), + Value banned = const Value.absent(), + Value?> teamsRole = const Value.absent(), + Value avgResponseTime = const Value.absent(), + Value> extraData = const Value.absent(), + Value rowid = const Value.absent(), + }) => UsersCompanion( + id: id, + role: role, + language: language, + createdAt: createdAt, + updatedAt: updatedAt, + lastActive: lastActive, + online: online, + banned: banned, + teamsRole: teamsRole, + avgResponseTime: avgResponseTime, + extraData: extraData, + rowid: rowid, + ), + createCompanionCallback: + ({ + required 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(), + Value online = const Value.absent(), + Value banned = const Value.absent(), + Value?> teamsRole = const Value.absent(), + Value avgResponseTime = const Value.absent(), + required Map extraData, + Value rowid = const Value.absent(), + }) => UsersCompanion.insert( + id: id, + role: role, + language: language, + createdAt: createdAt, + updatedAt: updatedAt, + lastActive: lastActive, + online: online, + banned: banned, + teamsRole: teamsRole, + avgResponseTime: avgResponseTime, + extraData: extraData, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$UsersTableProcessedTableManager = ProcessedTableManager< - _$DriftChatDatabase, - $UsersTable, - UserEntity, - $$UsersTableFilterComposer, - $$UsersTableOrderingComposer, - $$UsersTableAnnotationComposer, - $$UsersTableCreateCompanionBuilder, - $$UsersTableUpdateCompanionBuilder, - (UserEntity, BaseReferences<_$DriftChatDatabase, $UsersTable, UserEntity>), - UserEntity, - PrefetchHooks Function()>; -typedef $$MembersTableCreateCompanionBuilder = MembersCompanion Function({ - required String userId, - required String 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, -}); -typedef $$MembersTableUpdateCompanionBuilder = MembersCompanion Function({ - Value userId, - 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, -}); - -final class $$MembersTableReferences - extends BaseReferences<_$DriftChatDatabase, $MembersTable, MemberEntity> { +typedef $$UsersTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $UsersTable, + UserEntity, + $$UsersTableFilterComposer, + $$UsersTableOrderingComposer, + $$UsersTableAnnotationComposer, + $$UsersTableCreateCompanionBuilder, + $$UsersTableUpdateCompanionBuilder, + (UserEntity, BaseReferences<_$DriftChatDatabase, $UsersTable, UserEntity>), + UserEntity, + PrefetchHooks Function() + >; +typedef $$MembersTableCreateCompanionBuilder = + MembersCompanion Function({ + required String userId, + required String 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, + required List deletedMessages, + Value rowid, + }); +typedef $$MembersTableUpdateCompanionBuilder = + MembersCompanion Function({ + Value userId, + 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> deletedMessages, + Value rowid, + }); + +final class $$MembersTableReferences extends BaseReferences<_$DriftChatDatabase, $MembersTable, MemberEntity> { $$MembersTableReferences(super.$_db, super.$_table, super.$_typedResult); static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => - db.channels.createAlias( - $_aliasNameGenerator(db.members.channelCid, db.channels.cid)); + db.channels.createAlias($_aliasNameGenerator(db.members.channelCid, db.channels.cid)); $$ChannelsTableProcessedTableManager get channelCid { - final manager = $$ChannelsTableTableManager($_db, $_db.channels) - .filter((f) => f.cid($_item.channelCid!)); + final $_column = $_itemColumn('channel_cid')!; + + 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])); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: [item])); } } -class $$MembersTableFilterComposer - extends Composer<_$DriftChatDatabase, $MembersTable> { +class $$MembersTableFilterComposer extends Composer<_$DriftChatDatabase, $MembersTable> { $$MembersTableFilterComposer({ required super.$db, required super.$table, @@ -13137,73 +13969,68 @@ class $$MembersTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnFilters(column)); + ColumnFilters get userId => + $composableBuilder(column: $table.userId, builder: (column) => ColumnFilters(column)); - ColumnFilters get channelRole => $composableBuilder( - column: $table.channelRole, builder: (column) => ColumnFilters(column)); + ColumnFilters get channelRole => + $composableBuilder(column: $table.channelRole, builder: (column) => ColumnFilters(column)); - ColumnFilters get inviteAcceptedAt => $composableBuilder( - column: $table.inviteAcceptedAt, - builder: (column) => ColumnFilters(column)); + ColumnFilters get inviteAcceptedAt => + $composableBuilder(column: $table.inviteAcceptedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get inviteRejectedAt => $composableBuilder( - column: $table.inviteRejectedAt, - builder: (column) => ColumnFilters(column)); + ColumnFilters get inviteRejectedAt => + $composableBuilder(column: $table.inviteRejectedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get invited => $composableBuilder( - column: $table.invited, builder: (column) => ColumnFilters(column)); + ColumnFilters get invited => + $composableBuilder(column: $table.invited, builder: (column) => ColumnFilters(column)); - ColumnFilters get banned => $composableBuilder( - column: $table.banned, builder: (column) => ColumnFilters(column)); + ColumnFilters get banned => + $composableBuilder(column: $table.banned, builder: (column) => ColumnFilters(column)); - ColumnFilters get shadowBanned => $composableBuilder( - column: $table.shadowBanned, builder: (column) => ColumnFilters(column)); + ColumnFilters get shadowBanned => + $composableBuilder(column: $table.shadowBanned, builder: (column) => ColumnFilters(column)); - ColumnFilters get pinnedAt => $composableBuilder( - column: $table.pinnedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get pinnedAt => + $composableBuilder(column: $table.pinnedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get archivedAt => $composableBuilder( - column: $table.archivedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get archivedAt => + $composableBuilder(column: $table.archivedAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get isModerator => $composableBuilder( - column: $table.isModerator, builder: (column) => ColumnFilters(column)); + ColumnFilters get isModerator => + $composableBuilder(column: $table.isModerator, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, Map, - String> - get extraData => $composableBuilder( - column: $table.extraData, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, Map, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, 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 updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters, List, String> get deletedMessages => + $composableBuilder(column: $table.deletedMessages, builder: (column) => ColumnWithTypeConverterFilters(column)); $$ChannelsTableFilterComposer get channelCid { final $$ChannelsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableFilterComposer( - $db: $db, - $table: $db.channels, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$ChannelsTableFilterComposer( + $db: $db, + $table: $db.channels, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } -class $$MembersTableOrderingComposer - extends Composer<_$DriftChatDatabase, $MembersTable> { +class $$MembersTableOrderingComposer extends Composer<_$DriftChatDatabase, $MembersTable> { $$MembersTableOrderingComposer({ required super.$db, required super.$table, @@ -13211,71 +14038,68 @@ class $$MembersTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get userId => + $composableBuilder(column: $table.userId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get channelRole => + $composableBuilder(column: $table.channelRole, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get channelRole => $composableBuilder( - column: $table.channelRole, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get inviteAcceptedAt => + $composableBuilder(column: $table.inviteAcceptedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get inviteAcceptedAt => $composableBuilder( - column: $table.inviteAcceptedAt, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get inviteRejectedAt => + $composableBuilder(column: $table.inviteRejectedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get inviteRejectedAt => $composableBuilder( - column: $table.inviteRejectedAt, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get invited => + $composableBuilder(column: $table.invited, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get invited => $composableBuilder( - column: $table.invited, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get banned => + $composableBuilder(column: $table.banned, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get banned => $composableBuilder( - column: $table.banned, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get shadowBanned => + $composableBuilder(column: $table.shadowBanned, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get shadowBanned => $composableBuilder( - column: $table.shadowBanned, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get pinnedAt => + $composableBuilder(column: $table.pinnedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get pinnedAt => $composableBuilder( - column: $table.pinnedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get archivedAt => + $composableBuilder(column: $table.archivedAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get archivedAt => $composableBuilder( - column: $table.archivedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get isModerator => + $composableBuilder(column: $table.isModerator, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get isModerator => $composableBuilder( - column: $table.isModerator, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get createdAt => + $composableBuilder(column: $table.createdAt, 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 updatedAt => $composableBuilder( - column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get deletedMessages => + $composableBuilder(column: $table.deletedMessages, 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, - )); + 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 $$MembersTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $MembersTable> { +class $$MembersTableAnnotationComposer extends Composer<_$DriftChatDatabase, $MembersTable> { $$MembersTableAnnotationComposer({ required super.$db, required super.$table, @@ -13283,167 +14107,164 @@ class $$MembersTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get userId => - $composableBuilder(column: $table.userId, builder: (column) => column); + GeneratedColumn get userId => $composableBuilder(column: $table.userId, builder: (column) => column); - GeneratedColumn get channelRole => $composableBuilder( - column: $table.channelRole, builder: (column) => column); + GeneratedColumn get channelRole => + $composableBuilder(column: $table.channelRole, builder: (column) => column); - GeneratedColumn get inviteAcceptedAt => $composableBuilder( - column: $table.inviteAcceptedAt, builder: (column) => column); + GeneratedColumn get inviteAcceptedAt => + $composableBuilder(column: $table.inviteAcceptedAt, builder: (column) => column); - GeneratedColumn get inviteRejectedAt => $composableBuilder( - column: $table.inviteRejectedAt, builder: (column) => column); + GeneratedColumn get inviteRejectedAt => + $composableBuilder(column: $table.inviteRejectedAt, builder: (column) => column); - GeneratedColumn get invited => - $composableBuilder(column: $table.invited, builder: (column) => column); + GeneratedColumn get invited => $composableBuilder(column: $table.invited, builder: (column) => column); - GeneratedColumn get banned => - $composableBuilder(column: $table.banned, builder: (column) => column); + GeneratedColumn get banned => $composableBuilder(column: $table.banned, builder: (column) => column); - GeneratedColumn get shadowBanned => $composableBuilder( - column: $table.shadowBanned, builder: (column) => column); + GeneratedColumn get shadowBanned => + $composableBuilder(column: $table.shadowBanned, builder: (column) => column); - GeneratedColumn get pinnedAt => - $composableBuilder(column: $table.pinnedAt, builder: (column) => column); + GeneratedColumn get pinnedAt => $composableBuilder(column: $table.pinnedAt, builder: (column) => column); - GeneratedColumn get archivedAt => $composableBuilder( - column: $table.archivedAt, builder: (column) => column); + GeneratedColumn get archivedAt => + $composableBuilder(column: $table.archivedAt, builder: (column) => column); - GeneratedColumn get isModerator => $composableBuilder( - column: $table.isModerator, builder: (column) => column); + GeneratedColumn get isModerator => $composableBuilder(column: $table.isModerator, builder: (column) => column); - GeneratedColumnWithTypeConverter?, String> - get extraData => $composableBuilder( - column: $table.extraData, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get extraData => + $composableBuilder(column: $table.extraData, builder: (column) => column); - GeneratedColumn get createdAt => - $composableBuilder(column: $table.createdAt, 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 updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + GeneratedColumnWithTypeConverter, String> get deletedMessages => + $composableBuilder(column: $table.deletedMessages, builder: (column) => column); $$ChannelsTableAnnotationComposer get channelCid { final $$ChannelsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableAnnotationComposer( - $db: $db, - $table: $db.channels, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$ChannelsTableAnnotationComposer( + $db: $db, + $table: $db.channels, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } -class $$MembersTableTableManager extends RootTableManager< - _$DriftChatDatabase, - $MembersTable, - MemberEntity, - $$MembersTableFilterComposer, - $$MembersTableOrderingComposer, - $$MembersTableAnnotationComposer, - $$MembersTableCreateCompanionBuilder, - $$MembersTableUpdateCompanionBuilder, - (MemberEntity, $$MembersTableReferences), - MemberEntity, - PrefetchHooks Function({bool channelCid})> { +class $$MembersTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $MembersTable, + MemberEntity, + $$MembersTableFilterComposer, + $$MembersTableOrderingComposer, + $$MembersTableAnnotationComposer, + $$MembersTableCreateCompanionBuilder, + $$MembersTableUpdateCompanionBuilder, + (MemberEntity, $$MembersTableReferences), + MemberEntity, + PrefetchHooks Function({bool channelCid}) + > { $$MembersTableTableManager(_$DriftChatDatabase db, $MembersTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$MembersTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$MembersTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$MembersTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value userId = const Value.absent(), - Value channelCid = const Value.absent(), - Value channelRole = const Value.absent(), - Value inviteAcceptedAt = const Value.absent(), - Value inviteRejectedAt = const Value.absent(), - Value invited = const Value.absent(), - Value banned = const Value.absent(), - Value shadowBanned = const Value.absent(), - Value pinnedAt = const Value.absent(), - Value archivedAt = const Value.absent(), - Value isModerator = const Value.absent(), - Value?> extraData = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value rowid = const Value.absent(), - }) => - MembersCompanion( - userId: userId, - channelCid: channelCid, - channelRole: channelRole, - inviteAcceptedAt: inviteAcceptedAt, - inviteRejectedAt: inviteRejectedAt, - invited: invited, - banned: banned, - shadowBanned: shadowBanned, - pinnedAt: pinnedAt, - archivedAt: archivedAt, - isModerator: isModerator, - extraData: extraData, - createdAt: createdAt, - updatedAt: updatedAt, - rowid: rowid, - ), - createCompanionCallback: ({ - required String userId, - required String channelCid, - Value channelRole = const Value.absent(), - Value inviteAcceptedAt = const Value.absent(), - Value inviteRejectedAt = const Value.absent(), - Value invited = const Value.absent(), - Value banned = const Value.absent(), - Value shadowBanned = const Value.absent(), - Value pinnedAt = const Value.absent(), - Value archivedAt = const Value.absent(), - Value isModerator = const Value.absent(), - Value?> extraData = const Value.absent(), - Value createdAt = const Value.absent(), - Value updatedAt = const Value.absent(), - Value rowid = const Value.absent(), - }) => - MembersCompanion.insert( - userId: userId, - channelCid: channelCid, - channelRole: channelRole, - inviteAcceptedAt: inviteAcceptedAt, - inviteRejectedAt: inviteRejectedAt, - invited: invited, - banned: banned, - shadowBanned: shadowBanned, - pinnedAt: pinnedAt, - archivedAt: archivedAt, - isModerator: isModerator, - extraData: extraData, - createdAt: createdAt, - updatedAt: updatedAt, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => - (e.readTable(table), $$MembersTableReferences(db, table, e))) - .toList(), + createFilteringComposer: () => $$MembersTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$MembersTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$MembersTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value userId = const Value.absent(), + Value channelCid = const Value.absent(), + Value channelRole = const Value.absent(), + Value inviteAcceptedAt = const Value.absent(), + Value inviteRejectedAt = const Value.absent(), + Value invited = const Value.absent(), + Value banned = const Value.absent(), + Value shadowBanned = const Value.absent(), + Value pinnedAt = const Value.absent(), + Value archivedAt = const Value.absent(), + Value isModerator = const Value.absent(), + Value?> extraData = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + Value> deletedMessages = const Value.absent(), + Value rowid = const Value.absent(), + }) => MembersCompanion( + userId: userId, + channelCid: channelCid, + channelRole: channelRole, + inviteAcceptedAt: inviteAcceptedAt, + inviteRejectedAt: inviteRejectedAt, + invited: invited, + banned: banned, + shadowBanned: shadowBanned, + pinnedAt: pinnedAt, + archivedAt: archivedAt, + isModerator: isModerator, + extraData: extraData, + createdAt: createdAt, + updatedAt: updatedAt, + deletedMessages: deletedMessages, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String userId, + required String channelCid, + Value channelRole = const Value.absent(), + Value inviteAcceptedAt = const Value.absent(), + Value inviteRejectedAt = const Value.absent(), + Value invited = const Value.absent(), + Value banned = const Value.absent(), + Value shadowBanned = const Value.absent(), + Value pinnedAt = const Value.absent(), + Value archivedAt = const Value.absent(), + Value isModerator = const Value.absent(), + Value?> extraData = const Value.absent(), + Value createdAt = const Value.absent(), + Value updatedAt = const Value.absent(), + required List deletedMessages, + Value rowid = const Value.absent(), + }) => MembersCompanion.insert( + userId: userId, + channelCid: channelCid, + channelRole: channelRole, + inviteAcceptedAt: inviteAcceptedAt, + inviteRejectedAt: inviteRejectedAt, + invited: invited, + banned: banned, + shadowBanned: shadowBanned, + pinnedAt: pinnedAt, + archivedAt: archivedAt, + isModerator: isModerator, + extraData: extraData, + createdAt: createdAt, + updatedAt: updatedAt, + deletedMessages: deletedMessages, + rowid: rowid, + ), + withReferenceMapper: (p0) => + p0.map((e) => (e.readTable(table), $$MembersTableReferences(db, table, e))).toList(), prefetchHooksCallback: ({channelCid = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], - addJoins: < - T extends TableManagerState< + addJoins: + < + T extends TableManagerState< dynamic, dynamic, dynamic, @@ -13454,80 +14275,85 @@ class $$MembersTableTableManager extends RootTableManager< dynamic, dynamic, dynamic, - dynamic>>(state) { - if (channelCid) { - state = state.withJoin( - currentTable: table, - currentColumn: table.channelCid, - referencedTable: - $$MembersTableReferences._channelCidTable(db), - referencedColumn: - $$MembersTableReferences._channelCidTable(db).cid, - ) as T; - } - - return state; - }, + dynamic + > + >(state) { + if (channelCid) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.channelCid, + referencedTable: $$MembersTableReferences._channelCidTable(db), + referencedColumn: $$MembersTableReferences._channelCidTable(db).cid, + ) + as T; + } + + return state; + }, getPrefetchedDataCallback: (items) async { return []; }, ); }, - )); + ), + ); } -typedef $$MembersTableProcessedTableManager = ProcessedTableManager< - _$DriftChatDatabase, - $MembersTable, - MemberEntity, - $$MembersTableFilterComposer, - $$MembersTableOrderingComposer, - $$MembersTableAnnotationComposer, - $$MembersTableCreateCompanionBuilder, - $$MembersTableUpdateCompanionBuilder, - (MemberEntity, $$MembersTableReferences), - MemberEntity, - PrefetchHooks Function({bool channelCid})>; -typedef $$ReadsTableCreateCompanionBuilder = ReadsCompanion Function({ - required DateTime lastRead, - required String userId, - required String channelCid, - Value unreadMessages, - Value lastReadMessageId, - Value lastDeliveredAt, - Value lastDeliveredMessageId, - Value rowid, -}); -typedef $$ReadsTableUpdateCompanionBuilder = ReadsCompanion Function({ - Value lastRead, - Value userId, - Value channelCid, - Value unreadMessages, - Value lastReadMessageId, - Value lastDeliveredAt, - Value lastDeliveredMessageId, - Value rowid, -}); - -final class $$ReadsTableReferences - extends BaseReferences<_$DriftChatDatabase, $ReadsTable, ReadEntity> { +typedef $$MembersTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $MembersTable, + MemberEntity, + $$MembersTableFilterComposer, + $$MembersTableOrderingComposer, + $$MembersTableAnnotationComposer, + $$MembersTableCreateCompanionBuilder, + $$MembersTableUpdateCompanionBuilder, + (MemberEntity, $$MembersTableReferences), + MemberEntity, + PrefetchHooks Function({bool channelCid}) + >; +typedef $$ReadsTableCreateCompanionBuilder = + ReadsCompanion Function({ + required DateTime lastRead, + required String userId, + required String channelCid, + Value unreadMessages, + Value lastReadMessageId, + Value lastDeliveredAt, + Value lastDeliveredMessageId, + Value rowid, + }); +typedef $$ReadsTableUpdateCompanionBuilder = + ReadsCompanion Function({ + Value lastRead, + Value userId, + Value channelCid, + Value unreadMessages, + Value lastReadMessageId, + Value lastDeliveredAt, + Value lastDeliveredMessageId, + Value rowid, + }); + +final class $$ReadsTableReferences extends BaseReferences<_$DriftChatDatabase, $ReadsTable, ReadEntity> { $$ReadsTableReferences(super.$_db, super.$_table, super.$_typedResult); - static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => db.channels - .createAlias($_aliasNameGenerator(db.reads.channelCid, db.channels.cid)); + static $ChannelsTable _channelCidTable(_$DriftChatDatabase db) => + db.channels.createAlias($_aliasNameGenerator(db.reads.channelCid, db.channels.cid)); $$ChannelsTableProcessedTableManager get channelCid { - final manager = $$ChannelsTableTableManager($_db, $_db.channels) - .filter((f) => f.cid($_item.channelCid!)); + final $_column = $_itemColumn('channel_cid')!; + + 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])); + return ProcessedTableManager(manager.$state.copyWith(prefetchedData: [item])); } } -class $$ReadsTableFilterComposer - extends Composer<_$DriftChatDatabase, $ReadsTable> { +class $$ReadsTableFilterComposer extends Composer<_$DriftChatDatabase, $ReadsTable> { $$ReadsTableFilterComposer({ required super.$db, required super.$table, @@ -13535,51 +14361,44 @@ class $$ReadsTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get lastRead => $composableBuilder( - column: $table.lastRead, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastRead => + $composableBuilder(column: $table.lastRead, builder: (column) => ColumnFilters(column)); - ColumnFilters get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnFilters(column)); + ColumnFilters get userId => + $composableBuilder(column: $table.userId, builder: (column) => ColumnFilters(column)); - ColumnFilters get unreadMessages => $composableBuilder( - column: $table.unreadMessages, - builder: (column) => ColumnFilters(column)); + ColumnFilters get unreadMessages => + $composableBuilder(column: $table.unreadMessages, builder: (column) => ColumnFilters(column)); - ColumnFilters get lastReadMessageId => $composableBuilder( - column: $table.lastReadMessageId, - builder: (column) => ColumnFilters(column)); + ColumnFilters get lastReadMessageId => + $composableBuilder(column: $table.lastReadMessageId, builder: (column) => ColumnFilters(column)); - ColumnFilters get lastDeliveredAt => $composableBuilder( - column: $table.lastDeliveredAt, - builder: (column) => ColumnFilters(column)); + ColumnFilters get lastDeliveredAt => + $composableBuilder(column: $table.lastDeliveredAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get lastDeliveredMessageId => $composableBuilder( - column: $table.lastDeliveredMessageId, - builder: (column) => ColumnFilters(column)); + ColumnFilters get lastDeliveredMessageId => + $composableBuilder(column: $table.lastDeliveredMessageId, builder: (column) => ColumnFilters(column)); $$ChannelsTableFilterComposer get channelCid { final $$ChannelsTableFilterComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableFilterComposer( - $db: $db, - $table: $db.channels, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$ChannelsTableFilterComposer( + $db: $db, + $table: $db.channels, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } -class $$ReadsTableOrderingComposer - extends Composer<_$DriftChatDatabase, $ReadsTable> { +class $$ReadsTableOrderingComposer extends Composer<_$DriftChatDatabase, $ReadsTable> { $$ReadsTableOrderingComposer({ required super.$db, required super.$table, @@ -13587,51 +14406,44 @@ class $$ReadsTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get lastRead => $composableBuilder( - column: $table.lastRead, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastRead => + $composableBuilder(column: $table.lastRead, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get userId => $composableBuilder( - column: $table.userId, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get userId => + $composableBuilder(column: $table.userId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get unreadMessages => $composableBuilder( - column: $table.unreadMessages, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get unreadMessages => + $composableBuilder(column: $table.unreadMessages, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get lastReadMessageId => $composableBuilder( - column: $table.lastReadMessageId, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastReadMessageId => + $composableBuilder(column: $table.lastReadMessageId, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get lastDeliveredAt => $composableBuilder( - column: $table.lastDeliveredAt, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastDeliveredAt => + $composableBuilder(column: $table.lastDeliveredAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get lastDeliveredMessageId => $composableBuilder( - column: $table.lastDeliveredMessageId, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastDeliveredMessageId => + $composableBuilder(column: $table.lastDeliveredMessageId, 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, - )); + 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 $$ReadsTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $ReadsTable> { +class $$ReadsTableAnnotationComposer extends Composer<_$DriftChatDatabase, $ReadsTable> { $$ReadsTableAnnotationComposer({ required super.$db, required super.$table, @@ -13639,117 +14451,113 @@ class $$ReadsTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get lastRead => - $composableBuilder(column: $table.lastRead, builder: (column) => column); + GeneratedColumn get lastRead => $composableBuilder(column: $table.lastRead, builder: (column) => column); - GeneratedColumn get userId => - $composableBuilder(column: $table.userId, builder: (column) => column); + GeneratedColumn get userId => $composableBuilder(column: $table.userId, builder: (column) => column); - GeneratedColumn get unreadMessages => $composableBuilder( - column: $table.unreadMessages, builder: (column) => column); + GeneratedColumn get unreadMessages => + $composableBuilder(column: $table.unreadMessages, builder: (column) => column); - GeneratedColumn get lastReadMessageId => $composableBuilder( - column: $table.lastReadMessageId, builder: (column) => column); + GeneratedColumn get lastReadMessageId => + $composableBuilder(column: $table.lastReadMessageId, builder: (column) => column); - GeneratedColumn get lastDeliveredAt => $composableBuilder( - column: $table.lastDeliveredAt, builder: (column) => column); + GeneratedColumn get lastDeliveredAt => + $composableBuilder(column: $table.lastDeliveredAt, builder: (column) => column); - GeneratedColumn get lastDeliveredMessageId => $composableBuilder( - column: $table.lastDeliveredMessageId, builder: (column) => column); + GeneratedColumn get lastDeliveredMessageId => + $composableBuilder(column: $table.lastDeliveredMessageId, builder: (column) => column); $$ChannelsTableAnnotationComposer get channelCid { final $$ChannelsTableAnnotationComposer composer = $composerBuilder( - composer: this, - getCurrentColumn: (t) => t.channelCid, - referencedTable: $db.channels, - getReferencedColumn: (t) => t.cid, - builder: (joinBuilder, - {$addJoinBuilderToRootComposer, - $removeJoinBuilderFromRootComposer}) => - $$ChannelsTableAnnotationComposer( - $db: $db, - $table: $db.channels, - $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, - joinBuilder: joinBuilder, - $removeJoinBuilderFromRootComposer: - $removeJoinBuilderFromRootComposer, - )); + composer: this, + getCurrentColumn: (t) => t.channelCid, + referencedTable: $db.channels, + getReferencedColumn: (t) => t.cid, + builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => + $$ChannelsTableAnnotationComposer( + $db: $db, + $table: $db.channels, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: $removeJoinBuilderFromRootComposer, + ), + ); return composer; } } -class $$ReadsTableTableManager extends RootTableManager< - _$DriftChatDatabase, - $ReadsTable, - ReadEntity, - $$ReadsTableFilterComposer, - $$ReadsTableOrderingComposer, - $$ReadsTableAnnotationComposer, - $$ReadsTableCreateCompanionBuilder, - $$ReadsTableUpdateCompanionBuilder, - (ReadEntity, $$ReadsTableReferences), - ReadEntity, - PrefetchHooks Function({bool channelCid})> { +class $$ReadsTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $ReadsTable, + ReadEntity, + $$ReadsTableFilterComposer, + $$ReadsTableOrderingComposer, + $$ReadsTableAnnotationComposer, + $$ReadsTableCreateCompanionBuilder, + $$ReadsTableUpdateCompanionBuilder, + (ReadEntity, $$ReadsTableReferences), + ReadEntity, + PrefetchHooks Function({bool channelCid}) + > { $$ReadsTableTableManager(_$DriftChatDatabase db, $ReadsTable table) - : super(TableManagerState( + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$ReadsTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$ReadsTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$ReadsTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value lastRead = const Value.absent(), - Value userId = const Value.absent(), - Value channelCid = const Value.absent(), - Value unreadMessages = const Value.absent(), - Value lastReadMessageId = const Value.absent(), - Value lastDeliveredAt = const Value.absent(), - Value lastDeliveredMessageId = const Value.absent(), - Value rowid = const Value.absent(), - }) => - ReadsCompanion( - lastRead: lastRead, - userId: userId, - channelCid: channelCid, - unreadMessages: unreadMessages, - lastReadMessageId: lastReadMessageId, - lastDeliveredAt: lastDeliveredAt, - lastDeliveredMessageId: lastDeliveredMessageId, - rowid: rowid, - ), - createCompanionCallback: ({ - required DateTime lastRead, - required String userId, - required String channelCid, - Value unreadMessages = const Value.absent(), - Value lastReadMessageId = const Value.absent(), - Value lastDeliveredAt = const Value.absent(), - Value lastDeliveredMessageId = const Value.absent(), - Value rowid = const Value.absent(), - }) => - ReadsCompanion.insert( - lastRead: lastRead, - userId: userId, - channelCid: channelCid, - unreadMessages: unreadMessages, - lastReadMessageId: lastReadMessageId, - lastDeliveredAt: lastDeliveredAt, - lastDeliveredMessageId: lastDeliveredMessageId, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => - (e.readTable(table), $$ReadsTableReferences(db, table, e))) - .toList(), + createFilteringComposer: () => $$ReadsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$ReadsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$ReadsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value lastRead = const Value.absent(), + Value userId = const Value.absent(), + Value channelCid = const Value.absent(), + Value unreadMessages = const Value.absent(), + Value lastReadMessageId = const Value.absent(), + Value lastDeliveredAt = const Value.absent(), + Value lastDeliveredMessageId = const Value.absent(), + Value rowid = const Value.absent(), + }) => ReadsCompanion( + lastRead: lastRead, + userId: userId, + channelCid: channelCid, + unreadMessages: unreadMessages, + lastReadMessageId: lastReadMessageId, + lastDeliveredAt: lastDeliveredAt, + lastDeliveredMessageId: lastDeliveredMessageId, + rowid: rowid, + ), + createCompanionCallback: + ({ + required DateTime lastRead, + required String userId, + required String channelCid, + Value unreadMessages = const Value.absent(), + Value lastReadMessageId = const Value.absent(), + Value lastDeliveredAt = const Value.absent(), + Value lastDeliveredMessageId = const Value.absent(), + Value rowid = const Value.absent(), + }) => ReadsCompanion.insert( + lastRead: lastRead, + userId: userId, + channelCid: channelCid, + unreadMessages: unreadMessages, + lastReadMessageId: lastReadMessageId, + lastDeliveredAt: lastDeliveredAt, + lastDeliveredMessageId: lastDeliveredMessageId, + rowid: rowid, + ), + withReferenceMapper: (p0) => + p0.map((e) => (e.readTable(table), $$ReadsTableReferences(db, table, e))).toList(), prefetchHooksCallback: ({channelCid = false}) { return PrefetchHooks( db: db, explicitlyWatchedTables: [], - addJoins: < - T extends TableManagerState< + addJoins: + < + T extends TableManagerState< dynamic, dynamic, dynamic, @@ -13760,55 +14568,59 @@ class $$ReadsTableTableManager extends RootTableManager< dynamic, dynamic, dynamic, - dynamic>>(state) { - if (channelCid) { - state = state.withJoin( - currentTable: table, - currentColumn: table.channelCid, - referencedTable: - $$ReadsTableReferences._channelCidTable(db), - referencedColumn: - $$ReadsTableReferences._channelCidTable(db).cid, - ) as T; - } - - return state; - }, + dynamic + > + >(state) { + if (channelCid) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.channelCid, + referencedTable: $$ReadsTableReferences._channelCidTable(db), + referencedColumn: $$ReadsTableReferences._channelCidTable(db).cid, + ) + as T; + } + + return state; + }, getPrefetchedDataCallback: (items) async { return []; }, ); }, - )); + ), + ); } -typedef $$ReadsTableProcessedTableManager = ProcessedTableManager< - _$DriftChatDatabase, - $ReadsTable, - ReadEntity, - $$ReadsTableFilterComposer, - $$ReadsTableOrderingComposer, - $$ReadsTableAnnotationComposer, - $$ReadsTableCreateCompanionBuilder, - $$ReadsTableUpdateCompanionBuilder, - (ReadEntity, $$ReadsTableReferences), - ReadEntity, - PrefetchHooks Function({bool channelCid})>; -typedef $$ChannelQueriesTableCreateCompanionBuilder = ChannelQueriesCompanion - Function({ - required String queryHash, - required String channelCid, - Value rowid, -}); -typedef $$ChannelQueriesTableUpdateCompanionBuilder = ChannelQueriesCompanion - Function({ - Value queryHash, - Value channelCid, - Value rowid, -}); - -class $$ChannelQueriesTableFilterComposer - extends Composer<_$DriftChatDatabase, $ChannelQueriesTable> { +typedef $$ReadsTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $ReadsTable, + ReadEntity, + $$ReadsTableFilterComposer, + $$ReadsTableOrderingComposer, + $$ReadsTableAnnotationComposer, + $$ReadsTableCreateCompanionBuilder, + $$ReadsTableUpdateCompanionBuilder, + (ReadEntity, $$ReadsTableReferences), + ReadEntity, + PrefetchHooks Function({bool channelCid}) + >; +typedef $$ChannelQueriesTableCreateCompanionBuilder = + ChannelQueriesCompanion Function({ + required String queryHash, + required String channelCid, + Value rowid, + }); +typedef $$ChannelQueriesTableUpdateCompanionBuilder = + ChannelQueriesCompanion Function({ + Value queryHash, + Value channelCid, + Value rowid, + }); + +class $$ChannelQueriesTableFilterComposer extends Composer<_$DriftChatDatabase, $ChannelQueriesTable> { $$ChannelQueriesTableFilterComposer({ required super.$db, required super.$table, @@ -13816,15 +14628,14 @@ class $$ChannelQueriesTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get queryHash => $composableBuilder( - column: $table.queryHash, builder: (column) => ColumnFilters(column)); + ColumnFilters get queryHash => + $composableBuilder(column: $table.queryHash, builder: (column) => ColumnFilters(column)); - ColumnFilters get channelCid => $composableBuilder( - column: $table.channelCid, builder: (column) => ColumnFilters(column)); + ColumnFilters get channelCid => + $composableBuilder(column: $table.channelCid, builder: (column) => ColumnFilters(column)); } -class $$ChannelQueriesTableOrderingComposer - extends Composer<_$DriftChatDatabase, $ChannelQueriesTable> { +class $$ChannelQueriesTableOrderingComposer extends Composer<_$DriftChatDatabase, $ChannelQueriesTable> { $$ChannelQueriesTableOrderingComposer({ required super.$db, required super.$table, @@ -13832,15 +14643,14 @@ class $$ChannelQueriesTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get queryHash => $composableBuilder( - column: $table.queryHash, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get queryHash => + $composableBuilder(column: $table.queryHash, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get channelCid => $composableBuilder( - column: $table.channelCid, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get channelCid => + $composableBuilder(column: $table.channelCid, builder: (column) => ColumnOrderings(column)); } -class $$ChannelQueriesTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $ChannelQueriesTable> { +class $$ChannelQueriesTableAnnotationComposer extends Composer<_$DriftChatDatabase, $ChannelQueriesTable> { $$ChannelQueriesTableAnnotationComposer({ required super.$db, required super.$table, @@ -13848,106 +14658,96 @@ class $$ChannelQueriesTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get queryHash => - $composableBuilder(column: $table.queryHash, builder: (column) => column); + GeneratedColumn get queryHash => $composableBuilder(column: $table.queryHash, builder: (column) => column); - GeneratedColumn get channelCid => $composableBuilder( - column: $table.channelCid, builder: (column) => column); + GeneratedColumn get channelCid => $composableBuilder(column: $table.channelCid, builder: (column) => column); } -class $$ChannelQueriesTableTableManager extends RootTableManager< - _$DriftChatDatabase, - $ChannelQueriesTable, - ChannelQueryEntity, - $$ChannelQueriesTableFilterComposer, - $$ChannelQueriesTableOrderingComposer, - $$ChannelQueriesTableAnnotationComposer, - $$ChannelQueriesTableCreateCompanionBuilder, - $$ChannelQueriesTableUpdateCompanionBuilder, - ( - ChannelQueryEntity, - BaseReferences<_$DriftChatDatabase, $ChannelQueriesTable, - ChannelQueryEntity> - ), - ChannelQueryEntity, - PrefetchHooks Function()> { - $$ChannelQueriesTableTableManager( - _$DriftChatDatabase db, $ChannelQueriesTable table) - : super(TableManagerState( +class $$ChannelQueriesTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $ChannelQueriesTable, + ChannelQueryEntity, + $$ChannelQueriesTableFilterComposer, + $$ChannelQueriesTableOrderingComposer, + $$ChannelQueriesTableAnnotationComposer, + $$ChannelQueriesTableCreateCompanionBuilder, + $$ChannelQueriesTableUpdateCompanionBuilder, + (ChannelQueryEntity, BaseReferences<_$DriftChatDatabase, $ChannelQueriesTable, ChannelQueryEntity>), + ChannelQueryEntity, + PrefetchHooks Function() + > { + $$ChannelQueriesTableTableManager(_$DriftChatDatabase db, $ChannelQueriesTable table) + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$ChannelQueriesTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$ChannelQueriesTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$ChannelQueriesTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value queryHash = const Value.absent(), - Value channelCid = const Value.absent(), - Value rowid = const Value.absent(), - }) => - ChannelQueriesCompanion( - queryHash: queryHash, - channelCid: channelCid, - rowid: rowid, - ), - createCompanionCallback: ({ - required String queryHash, - required String channelCid, - Value rowid = const Value.absent(), - }) => - ChannelQueriesCompanion.insert( - queryHash: queryHash, - channelCid: channelCid, - rowid: rowid, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: () => $$ChannelQueriesTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$ChannelQueriesTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$ChannelQueriesTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value queryHash = const Value.absent(), + Value channelCid = const Value.absent(), + Value rowid = const Value.absent(), + }) => ChannelQueriesCompanion( + queryHash: queryHash, + channelCid: channelCid, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String queryHash, + required String channelCid, + Value rowid = const Value.absent(), + }) => ChannelQueriesCompanion.insert( + queryHash: queryHash, + channelCid: channelCid, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$ChannelQueriesTableProcessedTableManager = ProcessedTableManager< - _$DriftChatDatabase, - $ChannelQueriesTable, - ChannelQueryEntity, - $$ChannelQueriesTableFilterComposer, - $$ChannelQueriesTableOrderingComposer, - $$ChannelQueriesTableAnnotationComposer, - $$ChannelQueriesTableCreateCompanionBuilder, - $$ChannelQueriesTableUpdateCompanionBuilder, - ( +typedef $$ChannelQueriesTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $ChannelQueriesTable, + ChannelQueryEntity, + $$ChannelQueriesTableFilterComposer, + $$ChannelQueriesTableOrderingComposer, + $$ChannelQueriesTableAnnotationComposer, + $$ChannelQueriesTableCreateCompanionBuilder, + $$ChannelQueriesTableUpdateCompanionBuilder, + (ChannelQueryEntity, BaseReferences<_$DriftChatDatabase, $ChannelQueriesTable, ChannelQueryEntity>), ChannelQueryEntity, - BaseReferences<_$DriftChatDatabase, $ChannelQueriesTable, - ChannelQueryEntity> - ), - ChannelQueryEntity, - PrefetchHooks Function()>; -typedef $$ConnectionEventsTableCreateCompanionBuilder - = ConnectionEventsCompanion Function({ - Value id, - required String type, - Value?> ownUser, - Value totalUnreadCount, - Value unreadChannels, - Value lastEventAt, - Value lastSyncAt, -}); -typedef $$ConnectionEventsTableUpdateCompanionBuilder - = ConnectionEventsCompanion Function({ - Value id, - Value type, - Value?> ownUser, - Value totalUnreadCount, - Value unreadChannels, - Value lastEventAt, - Value lastSyncAt, -}); - -class $$ConnectionEventsTableFilterComposer - extends Composer<_$DriftChatDatabase, $ConnectionEventsTable> { + PrefetchHooks Function() + >; +typedef $$ConnectionEventsTableCreateCompanionBuilder = + ConnectionEventsCompanion Function({ + Value id, + required String type, + Value?> ownUser, + Value totalUnreadCount, + Value unreadChannels, + Value lastEventAt, + Value lastSyncAt, + }); +typedef $$ConnectionEventsTableUpdateCompanionBuilder = + ConnectionEventsCompanion Function({ + Value id, + Value type, + Value?> ownUser, + Value totalUnreadCount, + Value unreadChannels, + Value lastEventAt, + Value lastSyncAt, + }); + +class $$ConnectionEventsTableFilterComposer extends Composer<_$DriftChatDatabase, $ConnectionEventsTable> { $$ConnectionEventsTableFilterComposer({ required super.$db, required super.$table, @@ -13955,35 +14755,27 @@ class $$ConnectionEventsTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnFilters get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnFilters(column)); + ColumnFilters get id => $composableBuilder(column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnFilters(column)); + ColumnFilters get type => $composableBuilder(column: $table.type, builder: (column) => ColumnFilters(column)); - ColumnWithTypeConverterFilters?, Map, - String> - get ownUser => $composableBuilder( - column: $table.ownUser, - builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters?, Map, String> get ownUser => + $composableBuilder(column: $table.ownUser, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnFilters get totalUnreadCount => $composableBuilder( - column: $table.totalUnreadCount, - builder: (column) => ColumnFilters(column)); + ColumnFilters get totalUnreadCount => + $composableBuilder(column: $table.totalUnreadCount, builder: (column) => ColumnFilters(column)); - ColumnFilters get unreadChannels => $composableBuilder( - column: $table.unreadChannels, - builder: (column) => ColumnFilters(column)); + ColumnFilters get unreadChannels => + $composableBuilder(column: $table.unreadChannels, builder: (column) => ColumnFilters(column)); - ColumnFilters get lastEventAt => $composableBuilder( - column: $table.lastEventAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastEventAt => + $composableBuilder(column: $table.lastEventAt, builder: (column) => ColumnFilters(column)); - ColumnFilters get lastSyncAt => $composableBuilder( - column: $table.lastSyncAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastSyncAt => + $composableBuilder(column: $table.lastSyncAt, builder: (column) => ColumnFilters(column)); } -class $$ConnectionEventsTableOrderingComposer - extends Composer<_$DriftChatDatabase, $ConnectionEventsTable> { +class $$ConnectionEventsTableOrderingComposer extends Composer<_$DriftChatDatabase, $ConnectionEventsTable> { $$ConnectionEventsTableOrderingComposer({ required super.$db, required super.$table, @@ -13991,32 +14783,28 @@ class $$ConnectionEventsTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - ColumnOrderings get id => $composableBuilder( - column: $table.id, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get id => $composableBuilder(column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get type => $composableBuilder( - column: $table.type, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get type => + $composableBuilder(column: $table.type, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get ownUser => $composableBuilder( - column: $table.ownUser, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get ownUser => + $composableBuilder(column: $table.ownUser, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get totalUnreadCount => $composableBuilder( - column: $table.totalUnreadCount, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get totalUnreadCount => + $composableBuilder(column: $table.totalUnreadCount, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get unreadChannels => $composableBuilder( - column: $table.unreadChannels, - builder: (column) => ColumnOrderings(column)); + ColumnOrderings get unreadChannels => + $composableBuilder(column: $table.unreadChannels, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get lastEventAt => $composableBuilder( - column: $table.lastEventAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastEventAt => + $composableBuilder(column: $table.lastEventAt, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get lastSyncAt => $composableBuilder( - column: $table.lastSyncAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastSyncAt => + $composableBuilder(column: $table.lastSyncAt, builder: (column) => ColumnOrderings(column)); } -class $$ConnectionEventsTableAnnotationComposer - extends Composer<_$DriftChatDatabase, $ConnectionEventsTable> { +class $$ConnectionEventsTableAnnotationComposer extends Composer<_$DriftChatDatabase, $ConnectionEventsTable> { $$ConnectionEventsTableAnnotationComposer({ required super.$db, required super.$table, @@ -14024,143 +14812,123 @@ class $$ConnectionEventsTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - GeneratedColumn get id => - $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get type => - $composableBuilder(column: $table.type, builder: (column) => column); + GeneratedColumn get type => $composableBuilder(column: $table.type, builder: (column) => column); GeneratedColumnWithTypeConverter?, String> get ownUser => $composableBuilder(column: $table.ownUser, builder: (column) => column); - GeneratedColumn get totalUnreadCount => $composableBuilder( - column: $table.totalUnreadCount, builder: (column) => column); + GeneratedColumn get totalUnreadCount => + $composableBuilder(column: $table.totalUnreadCount, builder: (column) => column); - GeneratedColumn get unreadChannels => $composableBuilder( - column: $table.unreadChannels, builder: (column) => column); + GeneratedColumn get unreadChannels => + $composableBuilder(column: $table.unreadChannels, builder: (column) => column); - GeneratedColumn get lastEventAt => $composableBuilder( - column: $table.lastEventAt, builder: (column) => column); + GeneratedColumn get lastEventAt => + $composableBuilder(column: $table.lastEventAt, builder: (column) => column); - GeneratedColumn get lastSyncAt => $composableBuilder( - column: $table.lastSyncAt, builder: (column) => column); + GeneratedColumn get lastSyncAt => + $composableBuilder(column: $table.lastSyncAt, builder: (column) => column); } -class $$ConnectionEventsTableTableManager extends RootTableManager< - _$DriftChatDatabase, - $ConnectionEventsTable, - ConnectionEventEntity, - $$ConnectionEventsTableFilterComposer, - $$ConnectionEventsTableOrderingComposer, - $$ConnectionEventsTableAnnotationComposer, - $$ConnectionEventsTableCreateCompanionBuilder, - $$ConnectionEventsTableUpdateCompanionBuilder, - ( - ConnectionEventEntity, - BaseReferences<_$DriftChatDatabase, $ConnectionEventsTable, - ConnectionEventEntity> - ), - ConnectionEventEntity, - PrefetchHooks Function()> { - $$ConnectionEventsTableTableManager( - _$DriftChatDatabase db, $ConnectionEventsTable table) - : super(TableManagerState( +class $$ConnectionEventsTableTableManager + extends + RootTableManager< + _$DriftChatDatabase, + $ConnectionEventsTable, + ConnectionEventEntity, + $$ConnectionEventsTableFilterComposer, + $$ConnectionEventsTableOrderingComposer, + $$ConnectionEventsTableAnnotationComposer, + $$ConnectionEventsTableCreateCompanionBuilder, + $$ConnectionEventsTableUpdateCompanionBuilder, + (ConnectionEventEntity, BaseReferences<_$DriftChatDatabase, $ConnectionEventsTable, ConnectionEventEntity>), + ConnectionEventEntity, + PrefetchHooks Function() + > { + $$ConnectionEventsTableTableManager(_$DriftChatDatabase db, $ConnectionEventsTable table) + : super( + TableManagerState( db: db, table: table, - createFilteringComposer: () => - $$ConnectionEventsTableFilterComposer($db: db, $table: table), - createOrderingComposer: () => - $$ConnectionEventsTableOrderingComposer($db: db, $table: table), - createComputedFieldComposer: () => - $$ConnectionEventsTableAnnotationComposer($db: db, $table: table), - updateCompanionCallback: ({ - Value id = const Value.absent(), - Value type = const Value.absent(), - 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(), - }) => - ConnectionEventsCompanion( - id: id, - type: type, - ownUser: ownUser, - totalUnreadCount: totalUnreadCount, - unreadChannels: unreadChannels, - lastEventAt: lastEventAt, - lastSyncAt: lastSyncAt, - ), - createCompanionCallback: ({ - Value id = const Value.absent(), - required 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(), - }) => - ConnectionEventsCompanion.insert( - id: id, - type: type, - ownUser: ownUser, - totalUnreadCount: totalUnreadCount, - unreadChannels: unreadChannels, - lastEventAt: lastEventAt, - lastSyncAt: lastSyncAt, - ), - withReferenceMapper: (p0) => p0 - .map((e) => (e.readTable(table), BaseReferences(db, table, e))) - .toList(), + createFilteringComposer: () => $$ConnectionEventsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => $$ConnectionEventsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => $$ConnectionEventsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value type = const Value.absent(), + 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(), + }) => ConnectionEventsCompanion( + id: id, + type: type, + ownUser: ownUser, + totalUnreadCount: totalUnreadCount, + unreadChannels: unreadChannels, + lastEventAt: lastEventAt, + lastSyncAt: lastSyncAt, + ), + createCompanionCallback: + ({ + Value id = const Value.absent(), + required 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(), + }) => ConnectionEventsCompanion.insert( + id: id, + type: type, + ownUser: ownUser, + totalUnreadCount: totalUnreadCount, + unreadChannels: unreadChannels, + lastEventAt: lastEventAt, + lastSyncAt: lastSyncAt, + ), + withReferenceMapper: (p0) => p0.map((e) => (e.readTable(table), BaseReferences(db, table, e))).toList(), prefetchHooksCallback: null, - )); + ), + ); } -typedef $$ConnectionEventsTableProcessedTableManager = ProcessedTableManager< - _$DriftChatDatabase, - $ConnectionEventsTable, - ConnectionEventEntity, - $$ConnectionEventsTableFilterComposer, - $$ConnectionEventsTableOrderingComposer, - $$ConnectionEventsTableAnnotationComposer, - $$ConnectionEventsTableCreateCompanionBuilder, - $$ConnectionEventsTableUpdateCompanionBuilder, - ( +typedef $$ConnectionEventsTableProcessedTableManager = + ProcessedTableManager< + _$DriftChatDatabase, + $ConnectionEventsTable, + ConnectionEventEntity, + $$ConnectionEventsTableFilterComposer, + $$ConnectionEventsTableOrderingComposer, + $$ConnectionEventsTableAnnotationComposer, + $$ConnectionEventsTableCreateCompanionBuilder, + $$ConnectionEventsTableUpdateCompanionBuilder, + (ConnectionEventEntity, BaseReferences<_$DriftChatDatabase, $ConnectionEventsTable, ConnectionEventEntity>), ConnectionEventEntity, - BaseReferences<_$DriftChatDatabase, $ConnectionEventsTable, - ConnectionEventEntity> - ), - ConnectionEventEntity, - PrefetchHooks Function()>; + PrefetchHooks Function() + >; class $DriftChatDatabaseManager { final _$DriftChatDatabase _db; $DriftChatDatabaseManager(this._db); - $$ChannelsTableTableManager get channels => - $$ChannelsTableTableManager(_db, _db.channels); - $$MessagesTableTableManager get messages => - $$MessagesTableTableManager(_db, _db.messages); - $$DraftMessagesTableTableManager get draftMessages => - $$DraftMessagesTableTableManager(_db, _db.draftMessages); - $$PinnedMessagesTableTableManager get pinnedMessages => - $$PinnedMessagesTableTableManager(_db, _db.pinnedMessages); - $$PollsTableTableManager get polls => - $$PollsTableTableManager(_db, _db.polls); - $$PollVotesTableTableManager get pollVotes => - $$PollVotesTableTableManager(_db, _db.pollVotes); + $$ChannelsTableTableManager get channels => $$ChannelsTableTableManager(_db, _db.channels); + $$MessagesTableTableManager get messages => $$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 => $$PollsTableTableManager(_db, _db.polls); + $$PollVotesTableTableManager get pollVotes => $$PollVotesTableTableManager(_db, _db.pollVotes); $$PinnedMessageReactionsTableTableManager get pinnedMessageReactions => - $$PinnedMessageReactionsTableTableManager( - _db, _db.pinnedMessageReactions); - $$ReactionsTableTableManager get reactions => - $$ReactionsTableTableManager(_db, _db.reactions); - $$UsersTableTableManager get users => - $$UsersTableTableManager(_db, _db.users); - $$MembersTableTableManager get members => - $$MembersTableTableManager(_db, _db.members); - $$ReadsTableTableManager get reads => - $$ReadsTableTableManager(_db, _db.reads); - $$ChannelQueriesTableTableManager get channelQueries => - $$ChannelQueriesTableTableManager(_db, _db.channelQueries); + $$PinnedMessageReactionsTableTableManager(_db, _db.pinnedMessageReactions); + $$ReactionsTableTableManager get reactions => $$ReactionsTableTableManager(_db, _db.reactions); + $$UsersTableTableManager get users => $$UsersTableTableManager(_db, _db.users); + $$MembersTableTableManager get members => $$MembersTableTableManager(_db, _db.members); + $$ReadsTableTableManager get reads => $$ReadsTableTableManager(_db, _db.reads); + $$ChannelQueriesTableTableManager get channelQueries => $$ChannelQueriesTableTableManager(_db, _db.channelQueries); $$ConnectionEventsTableTableManager get connectionEvents => $$ConnectionEventsTableTableManager(_db, _db.connectionEvents); } diff --git a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart index ca31c3bb2d..0af7d3a543 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart @@ -25,13 +25,15 @@ class SharedDB { if (connectionMode == ConnectionMode.background) { return DriftChatDatabase( userId, - DatabaseConnection.delayed(Future(() async { - final isolate = await _createMoorIsolate( - dbName, - logStatements: logStatements, - ); - return isolate.connect(); - })), + DatabaseConnection.delayed( + Future(() async { + final isolate = await _createMoorIsolate( + dbName, + logStatements: logStatements, + ); + return isolate.connect(); + }), + ), ); } @@ -64,10 +66,12 @@ class SharedDB { } static void _startBackground(_IsolateStartRequest request) { - final executor = LazyDatabase(() async => NativeDatabase( - File(request.targetPath), - logStatements: request.logStatements, - )); + final executor = LazyDatabase( + () async => NativeDatabase( + File(request.targetPath), + logStatements: request.logStatements, + ), + ); final moorIsolate = DriftIsolate.inCurrent( () => DatabaseConnection(executor), ); diff --git a/packages/stream_chat_persistence/lib/src/entity/channel_queries.dart b/packages/stream_chat_persistence/lib/src/entity/channel_queries.dart index a9d3dc6cc0..44829c0fa0 100644 --- a/packages/stream_chat_persistence/lib/src/entity/channel_queries.dart +++ b/packages/stream_chat_persistence/lib/src/entity/channel_queries.dart @@ -12,7 +12,7 @@ class ChannelQueries extends Table { @override Set get primaryKey => { - queryHash, - channelCid, - }; + queryHash, + channelCid, + }; } diff --git a/packages/stream_chat_persistence/lib/src/entity/channels.dart b/packages/stream_chat_persistence/lib/src/entity/channels.dart index d7e6cc4112..1e417e3ab0 100644 --- a/packages/stream_chat_persistence/lib/src/entity/channels.dart +++ b/packages/stream_chat_persistence/lib/src/entity/channels.dart @@ -15,8 +15,7 @@ class Channels extends Table { TextColumn get cid => text()(); /// List of user permissions on this channel - TextColumn get ownCapabilities => - text().nullable().map(ListConverter())(); + TextColumn get ownCapabilities => text().nullable().map(ListConverter())(); /// The channel configuration data TextColumn get config => text().map(MapConverter())(); diff --git a/packages/stream_chat_persistence/lib/src/entity/draft_messages.dart b/packages/stream_chat_persistence/lib/src/entity/draft_messages.dart index 0105aec3a1..6a9054eec1 100644 --- a/packages/stream_chat_persistence/lib/src/entity/draft_messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/draft_messages.dart @@ -24,9 +24,7 @@ class DraftMessages extends Table { TextColumn get mentionedUsers => text().map(ListConverter())(); /// The ID of the parent message, if the message is a thread reply. - TextColumn get parentId => text() - .nullable() - .references(Messages, #id, onDelete: KeyAction.cascade)(); + TextColumn get parentId => text().nullable().references(Messages, #id, onDelete: KeyAction.cascade)(); /// The ID of the quoted message, if the message is a quoted reply. TextColumn get quotedMessageId => text().nullable()(); @@ -47,8 +45,7 @@ class DraftMessages extends Table { DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); /// The channel cid of which this message is part of - TextColumn get channelCid => - text().references(Channels, #cid, onDelete: KeyAction.cascade)(); + TextColumn get channelCid => text().references(Channels, #cid, onDelete: KeyAction.cascade)(); /// Message custom extraData TextColumn get extraData => text().nullable().map(MapConverter())(); 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..93b0678ec9 --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/entity/locations.dart @@ -0,0 +1,39 @@ +// 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/members.dart b/packages/stream_chat_persistence/lib/src/entity/members.dart index 086effca7d..801c44c594 100644 --- a/packages/stream_chat_persistence/lib/src/entity/members.dart +++ b/packages/stream_chat_persistence/lib/src/entity/members.dart @@ -1,6 +1,6 @@ // coverage:ignore-file import 'package:drift/drift.dart'; -import 'package:stream_chat_persistence/src/converter/map_converter.dart'; +import 'package:stream_chat_persistence/src/converter/converter.dart'; import 'package:stream_chat_persistence/src/entity/channels.dart'; /// Represents a [Members] table in [MoorChatDatabase]. @@ -10,8 +10,7 @@ class Members extends Table { TextColumn get userId => text()(); /// The channel cid of which this user is part of - TextColumn get channelCid => - text().references(Channels, #cid, onDelete: KeyAction.cascade)(); + TextColumn get channelCid => text().references(Channels, #cid, onDelete: KeyAction.cascade)(); /// The role of the user in the channel TextColumn get channelRole => text().nullable()(); @@ -32,12 +31,10 @@ class Members extends Table { BoolColumn get shadowBanned => boolean().withDefault(const Constant(false))(); /// The date at which the channel was pinned by the member - DateTimeColumn get pinnedAt => - dateTime().nullable().withDefault(const Constant(null))(); + DateTimeColumn get pinnedAt => dateTime().nullable().withDefault(const Constant(null))(); /// The date at which the channel was archived by the member - DateTimeColumn get archivedAt => - dateTime().nullable().withDefault(const Constant(null))(); + DateTimeColumn get archivedAt => dateTime().nullable().withDefault(const Constant(null))(); /// True if the user is a moderator of the channel BoolColumn get isModerator => boolean().withDefault(const Constant(false))(); @@ -51,6 +48,12 @@ class Members extends Table { /// The last date of update DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + /// List of message ids deleted by the member only for himself. + /// + /// These messages are now marked deleted for this member, but are still + /// kept as regular to other channel members. + TextColumn get deletedMessages => text().map(ListConverter())(); + @override Set get primaryKey => {userId, channelCid}; } diff --git a/packages/stream_chat_persistence/lib/src/entity/messages.dart b/packages/stream_chat_persistence/lib/src/entity/messages.dart index 537c6ecdb3..6ede2f9f12 100644 --- a/packages/stream_chat_persistence/lib/src/entity/messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/messages.dart @@ -28,8 +28,7 @@ class Messages extends Table { TextColumn get mentionedUsers => text().map(ListConverter())(); /// A map describing the reaction group for every reaction - TextColumn get reactionGroups => - text().map(ReactionGroupsConverter()).nullable()(); + TextColumn get reactionGroups => text().map(ReactionGroupsConverter()).nullable()(); /// The ID of the parent message, if the message is a thread reply. TextColumn get parentId => text().nullable()(); @@ -99,6 +98,9 @@ class Messages extends Table { /// The DateTime on which the message was deleted on the server. DateTimeColumn get remoteDeletedAt => dateTime().nullable()(); + /// Whether the message was deleted only for the current user. + BoolColumn get deletedForMe => boolean().nullable()(); + /// The DateTime at which the message text was edited DateTimeColumn get messageTextUpdatedAt => dateTime().nullable()(); @@ -121,16 +123,13 @@ class Messages extends Table { TextColumn get pinnedByUserId => text().nullable()(); /// The channel cid of which this message is part of - TextColumn get channelCid => - text().references(Channels, #cid, onDelete: KeyAction.cascade)(); + TextColumn get channelCid => text().references(Channels, #cid, onDelete: KeyAction.cascade)(); /// A Map of [messageText] translations. - TextColumn get i18n => - text().nullable().map(NullableMapConverter())(); + TextColumn get i18n => text().nullable().map(NullableMapConverter())(); /// The list of user ids that should be able to see the message. - TextColumn get restrictedVisibility => - text().nullable().map(ListConverter())(); + TextColumn get restrictedVisibility => text().nullable().map(ListConverter())(); /// Message custom extraData TextColumn get extraData => text().nullable().map(MapConverter())(); 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..97fa438f95 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,5 @@ 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/poll_votes.dart b/packages/stream_chat_persistence/lib/src/entity/poll_votes.dart index 5087b5a0df..f6a069d8d0 100644 --- a/packages/stream_chat_persistence/lib/src/entity/poll_votes.dart +++ b/packages/stream_chat_persistence/lib/src/entity/poll_votes.dart @@ -9,8 +9,7 @@ class PollVotes extends Table { TextColumn get id => text().nullable()(); /// The unique identifier of the poll the vote belongs to. - TextColumn get pollId => - text().nullable().references(Polls, #id, onDelete: KeyAction.cascade)(); + TextColumn get pollId => text().nullable().references(Polls, #id, onDelete: KeyAction.cascade)(); /// The unique identifier of the option selected in the poll. /// diff --git a/packages/stream_chat_persistence/lib/src/entity/polls.dart b/packages/stream_chat_persistence/lib/src/entity/polls.dart index f377301dba..ee4d99e85c 100644 --- a/packages/stream_chat_persistence/lib/src/entity/polls.dart +++ b/packages/stream_chat_persistence/lib/src/entity/polls.dart @@ -22,15 +22,13 @@ class Polls extends Table { /// Represents the visibility of the voting process. /// /// Defaults to 'public'. - TextColumn get votingVisibility => text() - .map(const VotingVisibilityConverter()) - .withDefault(const Constant('public'))(); + TextColumn get votingVisibility => + text().map(const VotingVisibilityConverter()).withDefault(const Constant('public'))(); /// If true, only unique votes are allowed. /// /// Defaults to false. - BoolColumn get enforceUniqueVote => - boolean().withDefault(const Constant(false))(); + BoolColumn get enforceUniqueVote => boolean().withDefault(const Constant(false))(); /// The maximum number of votes allowed per user. IntColumn get maxVotesAllowed => integer().nullable()(); @@ -38,8 +36,7 @@ class Polls extends Table { /// If true, users can suggest their own options. /// /// Defaults to false. - BoolColumn get allowUserSuggestedOptions => - boolean().withDefault(const Constant(false))(); + BoolColumn get allowUserSuggestedOptions => boolean().withDefault(const Constant(false))(); /// If true, users can provide their own answers/comments. /// diff --git a/packages/stream_chat_persistence/lib/src/entity/reactions.dart b/packages/stream_chat_persistence/lib/src/entity/reactions.dart index 39bf42589f..b43044e772 100644 --- a/packages/stream_chat_persistence/lib/src/entity/reactions.dart +++ b/packages/stream_chat_persistence/lib/src/entity/reactions.dart @@ -7,18 +7,23 @@ 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))(); @@ -27,8 +32,8 @@ class Reactions extends Table { @override Set get primaryKey => { - messageId, - type, - userId, - }; + messageId, + type, + userId, + }; } diff --git a/packages/stream_chat_persistence/lib/src/entity/reads.dart b/packages/stream_chat_persistence/lib/src/entity/reads.dart index 2a842d3d69..aadac4c69c 100644 --- a/packages/stream_chat_persistence/lib/src/entity/reads.dart +++ b/packages/stream_chat_persistence/lib/src/entity/reads.dart @@ -12,8 +12,7 @@ class Reads extends Table { TextColumn get userId => text()(); /// The channel cid of which this read belongs - TextColumn get channelCid => - text().references(Channels, #cid, onDelete: KeyAction.cascade)(); + TextColumn get channelCid => text().references(Channels, #cid, onDelete: KeyAction.cascade)(); /// Number of unread messages IntColumn get unreadMessages => integer().withDefault(const Constant(0))(); @@ -29,7 +28,7 @@ class Reads extends Table { @override Set get primaryKey => { - userId, - channelCid, - }; + userId, + channelCid, + }; } diff --git a/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart index 60c0910fba..35b3bc1d84 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/channel_mapper.dart @@ -32,34 +32,33 @@ extension ChannelEntityX on ChannelEntity { List reads = const [], List messages = const [], List pinnedMessages = const [], - }) => - ChannelState( - members: members, - read: reads, - messages: messages, - pinnedMessages: pinnedMessages, - channel: toChannelModel(createdBy: createdBy), - ); + }) => ChannelState( + members: members, + read: reads, + messages: messages, + pinnedMessages: pinnedMessages, + channel: toChannelModel(createdBy: createdBy), + ); } /// Useful mapping functions for [ChannelModel] extension ChannelModelX on ChannelModel { /// Maps a [ChannelModel] into [ChannelEntity] ChannelEntity toEntity() => ChannelEntity( - id: id, - type: type, - cid: cid, - ownCapabilities: ownCapabilities, - config: config.toJson(), - frozen: frozen, - lastMessageAt: lastMessageAt, - createdAt: createdAt, - updatedAt: updatedAt, - deletedAt: deletedAt, - memberCount: memberCount, - messageCount: messageCount, - createdById: createdBy?.id, - filterTags: filterTags, - extraData: extraData, - ); + id: id, + type: type, + cid: cid, + ownCapabilities: ownCapabilities, + config: config.toJson(), + frozen: frozen, + lastMessageAt: lastMessageAt, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + memberCount: memberCount, + messageCount: messageCount, + createdById: createdBy?.id, + filterTags: filterTags, + extraData: extraData, + ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/draft_message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/draft_message_mapper.dart index f4ae5b31ce..9dd08a0d00 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/draft_message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/draft_message_mapper.dart @@ -47,23 +47,23 @@ extension DraftMessageEntityX on DraftMessageEntity { extension DraftMessageX on Draft { /// Maps a [DraftMessage] into [DraftMessageEntity] DraftMessageEntity toEntity() => DraftMessageEntity( - id: message.id, - channelCid: channelCid, - messageText: message.text, - type: message.type, - createdAt: createdAt, - attachments: message.attachments.map((it) { - return jsonEncode(it.toData()); - }).toList(), - parentId: parentId, - showInChannel: message.showInChannel, - mentionedUsers: message.mentionedUsers.map((e) { - return jsonEncode(e.toJson()); - }).toList(), - quotedMessageId: message.quotedMessageId, - silent: message.silent, - command: message.command, - pollId: message.pollId, - extraData: message.extraData, - ); + id: message.id, + channelCid: channelCid, + messageText: message.text, + type: message.type, + createdAt: createdAt, + attachments: message.attachments.map((it) { + return jsonEncode(it.toData()); + }).toList(), + parentId: parentId, + showInChannel: message.showInChannel, + mentionedUsers: message.mentionedUsers.map((e) { + return jsonEncode(e.toJson()); + }).toList(), + quotedMessageId: message.quotedMessageId, + silent: message.silent, + command: message.command, + pollId: message.pollId, + extraData: message.extraData, + ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart index 0c3adb7b07..43a420b060 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/event_mapper.dart @@ -5,10 +5,10 @@ import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; extension ConnectionEventX on ConnectionEventEntity { /// Maps a [ConnectionEventEntity] into [Event] Event toEvent() => Event( - type: type, - createdAt: lastEventAt, - me: ownUser != null ? OwnUser.fromJson(ownUser!) : null, - totalUnreadCount: totalUnreadCount, - unreadChannels: unreadChannels, - ); + type: type, + createdAt: lastEventAt, + me: ownUser != null ? OwnUser.fromJson(ownUser!) : null, + totalUnreadCount: totalUnreadCount, + unreadChannels: unreadChannels, + ); } 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..35985f9132 --- /dev/null +++ b/packages/stream_chat_persistence/lib/src/mapper/location_mapper.dart @@ -0,0 +1,39 @@ +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/member_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart index 6c15e54e5d..26f7df3015 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart @@ -5,40 +5,42 @@ import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; extension MemberEntityX on MemberEntity { /// Maps a [MemberEntity] into [Member] Member toMember({User? user}) => Member( - user: user, - userId: userId, - banned: banned, - shadowBanned: shadowBanned, - updatedAt: updatedAt, - createdAt: createdAt, - channelRole: channelRole, - inviteAcceptedAt: inviteAcceptedAt, - invited: invited, - inviteRejectedAt: inviteRejectedAt, - pinnedAt: pinnedAt, - archivedAt: archivedAt, - isModerator: isModerator, - extraData: extraData ?? {}, - ); + user: user, + userId: userId, + banned: banned, + shadowBanned: shadowBanned, + updatedAt: updatedAt, + createdAt: createdAt, + channelRole: channelRole, + inviteAcceptedAt: inviteAcceptedAt, + invited: invited, + inviteRejectedAt: inviteRejectedAt, + pinnedAt: pinnedAt, + archivedAt: archivedAt, + isModerator: isModerator, + deletedMessages: deletedMessages, + extraData: extraData ?? {}, + ); } /// Useful mapping functions for [Member] extension MemberX on Member { /// Maps a [Member] into [MemberEntity] MemberEntity toEntity({required String cid}) => MemberEntity( - userId: user!.id, - banned: banned, - shadowBanned: shadowBanned, - channelCid: cid, - createdAt: createdAt, - isModerator: isModerator, - inviteRejectedAt: inviteRejectedAt, - invited: invited, - inviteAcceptedAt: inviteAcceptedAt, - pinnedAt: pinnedAt, - archivedAt: archivedAt, - channelRole: channelRole, - updatedAt: updatedAt, - extraData: extraData, - ); + userId: user!.id, + banned: banned, + shadowBanned: shadowBanned, + channelCid: cid, + createdAt: createdAt, + isModerator: isModerator, + inviteRejectedAt: inviteRejectedAt, + invited: invited, + inviteAcceptedAt: inviteAcceptedAt, + pinnedAt: pinnedAt, + archivedAt: archivedAt, + channelRole: channelRole, + updatedAt: updatedAt, + deletedMessages: deletedMessages, + extraData: extraData, + ); } 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 4e24e0d3ba..584ac6f222 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart @@ -14,84 +14,86 @@ extension MessageEntityX on MessageEntity { Message? quotedMessage, Poll? poll, Draft? draft, - }) => - Message( - shadowed: shadowed, - latestReactions: latestReactions, - ownReactions: ownReactions, - attachments: attachments.map((it) { - final json = jsonDecode(it); - return Attachment.fromData(json); - }).toList(), - extraData: extraData ?? {}, - createdAt: remoteCreatedAt, - localCreatedAt: localCreatedAt, - updatedAt: remoteUpdatedAt, - localUpdatedAt: localUpdatedAt, - deletedAt: remoteDeletedAt, - localDeletedAt: localDeletedAt, - messageTextUpdatedAt: messageTextUpdatedAt, - id: id, - type: type, - state: MessageState.fromJson(jsonDecode(state)), - command: command, - parentId: parentId, - quotedMessageId: quotedMessageId, - quotedMessage: quotedMessage, - pollId: pollId, - poll: poll, - reactionGroups: reactionGroups, - replyCount: replyCount, - showInChannel: showInChannel, - text: messageText, - user: user, - channelRole: channelRole, - pinned: pinned, - pinnedAt: pinnedAt, - pinExpires: pinExpires, - pinnedBy: pinnedBy, - mentionedUsers: - mentionedUsers.map((e) => User.fromJson(jsonDecode(e))).toList(), - i18n: i18n, - restrictedVisibility: restrictedVisibility, - draft: draft, - ); + Location? sharedLocation, + }) => Message( + shadowed: shadowed, + latestReactions: latestReactions, + ownReactions: ownReactions, + attachments: attachments.map((it) { + final json = jsonDecode(it); + return Attachment.fromData(json); + }).toList(), + extraData: extraData ?? {}, + createdAt: remoteCreatedAt, + localCreatedAt: localCreatedAt, + updatedAt: remoteUpdatedAt, + localUpdatedAt: localUpdatedAt, + deletedAt: remoteDeletedAt, + localDeletedAt: localDeletedAt, + deletedForMe: deletedForMe, + messageTextUpdatedAt: messageTextUpdatedAt, + id: id, + type: type, + state: MessageState.fromJson(jsonDecode(state)), + command: command, + parentId: parentId, + quotedMessageId: quotedMessageId, + quotedMessage: quotedMessage, + pollId: pollId, + poll: poll, + reactionGroups: reactionGroups, + replyCount: replyCount, + showInChannel: showInChannel, + text: messageText, + user: user, + channelRole: channelRole, + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedBy: pinnedBy, + mentionedUsers: mentionedUsers.map((e) => User.fromJson(jsonDecode(e))).toList(), + i18n: i18n, + restrictedVisibility: restrictedVisibility, + draft: draft, + sharedLocation: sharedLocation, + ); } /// Useful mapping functions for [Message] extension MessageX on Message { /// Maps a [Message] into [MessageEntity] MessageEntity toEntity({required String cid}) => MessageEntity( - id: id, - attachments: attachments.map((it) => jsonEncode(it.toData())).toList(), - channelCid: cid, - type: type, - parentId: parentId, - quotedMessageId: quotedMessageId, - pollId: pollId, - command: command, - remoteCreatedAt: remoteCreatedAt, - localCreatedAt: localCreatedAt, - shadowed: shadowed, - showInChannel: showInChannel, - replyCount: replyCount, - reactionGroups: reactionGroups, - mentionedUsers: mentionedUsers.map(jsonEncode).toList(), - state: jsonEncode(state), - remoteUpdatedAt: remoteUpdatedAt, - localUpdatedAt: localUpdatedAt, - extraData: extraData, - userId: user?.id, - channelRole: channelRole, - remoteDeletedAt: remoteDeletedAt, - localDeletedAt: localDeletedAt, - messageTextUpdatedAt: messageTextUpdatedAt, - messageText: text, - pinned: pinned, - pinnedAt: pinnedAt, - pinExpires: pinExpires, - pinnedByUserId: pinnedBy?.id, - i18n: i18n, - restrictedVisibility: restrictedVisibility, - ); + id: id, + attachments: attachments.map((it) => jsonEncode(it.toData())).toList(), + channelCid: cid, + type: type, + parentId: parentId, + quotedMessageId: quotedMessageId, + pollId: pollId, + command: command, + remoteCreatedAt: remoteCreatedAt, + localCreatedAt: localCreatedAt, + shadowed: shadowed, + showInChannel: showInChannel, + replyCount: replyCount, + reactionGroups: reactionGroups, + mentionedUsers: mentionedUsers.map(jsonEncode).toList(), + state: jsonEncode(state.toJson()), + remoteUpdatedAt: remoteUpdatedAt, + localUpdatedAt: localUpdatedAt, + extraData: extraData, + userId: user?.id, + channelRole: channelRole, + remoteDeletedAt: remoteDeletedAt, + localDeletedAt: localDeletedAt, + deletedForMe: deletedForMe, + messageTextUpdatedAt: messageTextUpdatedAt, + messageText: text, + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedByUserId: pinnedBy?.id, + i18n: i18n, + restrictedVisibility: restrictedVisibility, + ); } 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 a6c70046f2..90a06b5f55 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,85 +14,86 @@ extension PinnedMessageEntityX on PinnedMessageEntity { Message? quotedMessage, Poll? poll, Draft? draft, - }) => - Message( - shadowed: shadowed, - latestReactions: latestReactions, - ownReactions: ownReactions, - attachments: attachments.map((it) { - final json = jsonDecode(it); - return Attachment.fromData(json); - }).toList(), - extraData: extraData ?? {}, - createdAt: remoteCreatedAt, - localCreatedAt: localCreatedAt, - updatedAt: remoteUpdatedAt, - localUpdatedAt: localUpdatedAt, - deletedAt: remoteDeletedAt, - localDeletedAt: localDeletedAt, - messageTextUpdatedAt: messageTextUpdatedAt, - id: id, - type: type, - state: MessageState.fromJson(jsonDecode(state)), - command: command, - parentId: parentId, - quotedMessageId: quotedMessageId, - quotedMessage: quotedMessage, - pollId: pollId, - poll: poll, - reactionGroups: reactionGroups, - replyCount: replyCount, - showInChannel: showInChannel, - text: messageText, - user: user, - channelRole: channelRole, - pinned: pinned, - pinnedAt: pinnedAt, - pinExpires: pinExpires, - pinnedBy: pinnedBy, - mentionedUsers: - mentionedUsers.map((e) => User.fromJson(jsonDecode(e))).toList(), - i18n: i18n, - restrictedVisibility: restrictedVisibility, - draft: draft, - ); + Location? sharedLocation, + }) => Message( + shadowed: shadowed, + latestReactions: latestReactions, + ownReactions: ownReactions, + attachments: attachments.map((it) { + final json = jsonDecode(it); + return Attachment.fromData(json); + }).toList(), + extraData: extraData ?? {}, + createdAt: remoteCreatedAt, + localCreatedAt: localCreatedAt, + updatedAt: remoteUpdatedAt, + localUpdatedAt: localUpdatedAt, + deletedAt: remoteDeletedAt, + localDeletedAt: localDeletedAt, + deletedForMe: deletedForMe, + messageTextUpdatedAt: messageTextUpdatedAt, + id: id, + type: type, + state: MessageState.fromJson(jsonDecode(state)), + command: command, + parentId: parentId, + quotedMessageId: quotedMessageId, + quotedMessage: quotedMessage, + pollId: pollId, + poll: poll, + reactionGroups: reactionGroups, + replyCount: replyCount, + showInChannel: showInChannel, + text: messageText, + user: user, + channelRole: channelRole, + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedBy: pinnedBy, + mentionedUsers: mentionedUsers.map((e) => User.fromJson(jsonDecode(e))).toList(), + i18n: i18n, + restrictedVisibility: restrictedVisibility, + draft: draft, + sharedLocation: sharedLocation, + ); } /// Useful mapping functions for [Message] extension PMessageX on Message { /// Maps a [Message] into [PinnedMessageEntity] - PinnedMessageEntity toPinnedEntity({required String cid}) => - PinnedMessageEntity( - id: id, - attachments: attachments.map((it) => jsonEncode(it.toData())).toList(), - channelCid: cid, - type: type, - parentId: parentId, - quotedMessageId: quotedMessageId, - pollId: pollId, - command: command, - remoteCreatedAt: remoteCreatedAt, - localCreatedAt: localCreatedAt, - shadowed: shadowed, - showInChannel: showInChannel, - replyCount: replyCount, - reactionGroups: reactionGroups, - mentionedUsers: mentionedUsers.map(jsonEncode).toList(), - state: jsonEncode(state), - remoteUpdatedAt: remoteUpdatedAt, - localUpdatedAt: localUpdatedAt, - extraData: extraData, - userId: user?.id, - channelRole: channelRole, - remoteDeletedAt: remoteDeletedAt, - localDeletedAt: localDeletedAt, - messageTextUpdatedAt: messageTextUpdatedAt, - messageText: text, - pinned: pinned, - pinnedAt: pinnedAt, - pinExpires: pinExpires, - pinnedByUserId: pinnedBy?.id, - i18n: i18n, - restrictedVisibility: restrictedVisibility, - ); + PinnedMessageEntity toPinnedEntity({required String cid}) => PinnedMessageEntity( + id: id, + attachments: attachments.map((it) => jsonEncode(it.toData())).toList(), + channelCid: cid, + type: type, + parentId: parentId, + quotedMessageId: quotedMessageId, + pollId: pollId, + command: command, + remoteCreatedAt: remoteCreatedAt, + localCreatedAt: localCreatedAt, + shadowed: shadowed, + showInChannel: showInChannel, + replyCount: replyCount, + reactionGroups: reactionGroups, + mentionedUsers: mentionedUsers.map(jsonEncode).toList(), + state: jsonEncode(state.toJson()), + remoteUpdatedAt: remoteUpdatedAt, + localUpdatedAt: localUpdatedAt, + extraData: extraData, + userId: user?.id, + channelRole: channelRole, + remoteDeletedAt: remoteDeletedAt, + localDeletedAt: localDeletedAt, + deletedForMe: deletedForMe, + messageTextUpdatedAt: messageTextUpdatedAt, + messageText: text, + pinned: pinned, + pinnedAt: pinnedAt, + pinExpires: pinExpires, + pinnedByUserId: pinnedBy?.id, + i18n: i18n, + restrictedVisibility: restrictedVisibility, + ); } 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..1802280fd5 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,25 +5,29 @@ 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, - ); + type: type, + userId: userId, + user: user, + messageId: messageId, + score: score, + emojiCode: emojiCode, + createdAt: createdAt, + updatedAt: updatedAt, + extraData: extraData ?? {}, + ); } /// Useful mapping functions for [Reaction] extension PReactionX on Reaction { /// Maps a [Reaction] into [ReactionEntity] PinnedMessageReactionEntity toPinnedEntity() => PinnedMessageReactionEntity( - extraData: extraData, - type: type, - createdAt: createdAt, - userId: userId!, - messageId: messageId!, - score: score, - ); + type: type, + 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/poll_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/poll_mapper.dart index 451b5e853c..2b9a388ce9 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/poll_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/poll_mapper.dart @@ -45,22 +45,22 @@ extension PollEntityX on PollEntity { extension PollX on Poll { /// Maps a [Poll] into [PollEntity] PollEntity toEntity() => PollEntity( - id: id, - name: name, - description: description, - options: options.map(jsonEncode).toList(), - votingVisibility: votingVisibility, - enforceUniqueVote: enforceUniqueVote, - maxVotesAllowed: maxVotesAllowed, - allowAnswers: allowAnswers, - answersCount: answersCount, - allowUserSuggestedOptions: allowUserSuggestedOptions, - isClosed: isClosed, - createdAt: createdAt, - updatedAt: updatedAt, - voteCountsByOption: voteCountsByOption, - voteCount: voteCount, - createdById: createdById, - extraData: extraData, - ); + id: id, + name: name, + description: description, + options: options.map(jsonEncode).toList(), + votingVisibility: votingVisibility, + enforceUniqueVote: enforceUniqueVote, + maxVotesAllowed: maxVotesAllowed, + allowAnswers: allowAnswers, + answersCount: answersCount, + allowUserSuggestedOptions: allowUserSuggestedOptions, + isClosed: isClosed, + createdAt: createdAt, + updatedAt: updatedAt, + voteCountsByOption: voteCountsByOption, + voteCount: voteCount, + createdById: createdById, + extraData: extraData, + ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/poll_vote_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/poll_vote_mapper.dart index 25fa23ab35..a9cac39709 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/poll_vote_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/poll_vote_mapper.dart @@ -6,29 +6,28 @@ extension PollVoteEntityX on PollVoteEntity { /// Maps a [PollVoteEntity] into [PollVote] PollVote toPollVote({ User? user, - }) => - PollVote( - id: id, - pollId: pollId, - optionId: optionId, - answerText: answerText, - createdAt: createdAt, - updatedAt: updatedAt, - userId: userId, - user: user, - ); + }) => PollVote( + id: id, + pollId: pollId, + optionId: optionId, + answerText: answerText, + createdAt: createdAt, + updatedAt: updatedAt, + userId: userId, + user: user, + ); } /// Useful mapping functions for [PollVote] extension PollVoteX on PollVote { /// Maps a [PollVote] into [PollVoteEntity] PollVoteEntity toEntity() => PollVoteEntity( - id: id, - pollId: pollId, - optionId: optionId, - answerText: answerText, - createdAt: createdAt, - updatedAt: updatedAt, - userId: userId, - ); + id: id, + pollId: pollId, + optionId: optionId, + answerText: answerText, + createdAt: createdAt, + updatedAt: updatedAt, + userId: userId, + ); } 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..cc62e59db7 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/reaction_mapper.dart @@ -5,25 +5,29 @@ 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, - ); + type: type, + userId: userId, + user: user, + messageId: messageId, + score: score, + emojiCode: emojiCode, + createdAt: createdAt, + updatedAt: updatedAt, + extraData: extraData ?? {}, + ); } /// Useful mapping functions for [Reaction] extension ReactionX on Reaction { /// Maps a [Reaction] into [ReactionEntity] ReactionEntity toEntity() => ReactionEntity( - extraData: extraData, - type: type, - createdAt: createdAt, - userId: userId!, - messageId: messageId!, - score: score, - ); + type: type, + 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/read_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/read_mapper.dart index bdcaefc70e..d26e8bf80a 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/read_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/read_mapper.dart @@ -5,25 +5,25 @@ import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; extension ReadEntityX on ReadEntity { /// Maps a [ReadEntity] into [Read] Read toRead({required User user}) => Read( - user: user, - lastRead: lastRead, - unreadMessages: unreadMessages, - lastReadMessageId: lastReadMessageId, - lastDeliveredAt: lastDeliveredAt, - lastDeliveredMessageId: lastDeliveredMessageId, - ); + user: user, + lastRead: lastRead, + unreadMessages: unreadMessages, + lastReadMessageId: lastReadMessageId, + lastDeliveredAt: lastDeliveredAt, + lastDeliveredMessageId: lastDeliveredMessageId, + ); } /// Useful mapping functions for [Read] extension ReadX on Read { /// Maps a [Read] into [ReadEntity] ReadEntity toEntity({required String cid}) => ReadEntity( - lastRead: lastRead, - userId: user.id, - channelCid: cid, - unreadMessages: unreadMessages, - lastReadMessageId: lastReadMessageId, - lastDeliveredAt: lastDeliveredAt, - lastDeliveredMessageId: lastDeliveredMessageId, - ); + lastRead: lastRead, + userId: user.id, + channelCid: cid, + unreadMessages: unreadMessages, + lastReadMessageId: lastReadMessageId, + lastDeliveredAt: lastDeliveredAt, + lastDeliveredMessageId: lastDeliveredMessageId, + ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart index 4e894e81c6..678f4cc3a9 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/user_mapper.dart @@ -5,34 +5,34 @@ import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; extension UserEntityX on UserEntity { /// Maps a [UserEntity] into [User] User toUser() => User( - id: id, - updatedAt: updatedAt, - language: language, - role: role, - online: online, - lastActive: lastActive, - extraData: extraData, - banned: banned, - createdAt: createdAt, - teamsRole: teamsRole, - avgResponseTime: avgResponseTime, - ); + id: id, + updatedAt: updatedAt, + language: language, + role: role, + online: online, + lastActive: lastActive, + extraData: extraData, + banned: banned, + createdAt: createdAt, + teamsRole: teamsRole, + avgResponseTime: avgResponseTime, + ); } /// Useful mapping functions for [User] extension UserX on User { /// Maps a [User] into [UserEntity] UserEntity toEntity() => UserEntity( - id: id, - role: role, - language: language, - createdAt: createdAt, - updatedAt: updatedAt, - lastActive: lastActive, - online: online, - banned: banned, - teamsRole: teamsRole, - avgResponseTime: avgResponseTime, - extraData: extraData, - ); + id: id, + role: role, + language: language, + createdAt: createdAt, + updatedAt: updatedAt, + lastActive: lastActive, + online: online, + banned: banned, + teamsRole: teamsRole, + avgResponseTime: avgResponseTime, + 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 588510381e..3db937a51a 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 @@ -33,9 +33,9 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { /// Otherwise, falls back to the local storage based implementation. bool webUseExperimentalIndexedDb = false, LogHandlerFunction? logHandlerFunction, - }) : _connectionMode = connectionMode, - _webUseIndexedDbIfSupported = webUseExperimentalIndexedDb, - _logger = Logger.detached('💽')..level = logLevel { + }) : _connectionMode = connectionMode, + _webUseIndexedDbIfSupported = webUseExperimentalIndexedDb, + _logger = Logger.detached('💽')..level = logLevel { _logger.onRecord.listen(logHandlerFunction ?? _defaultLogHandler); } @@ -72,12 +72,11 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { Future _defaultDatabaseProvider( String userId, ConnectionMode mode, - ) => - SharedDB.constructDatabase( - userId, - connectionMode: mode, - webUseIndexedDbIfSupported: _webUseIndexedDbIfSupported, - ); + ) => SharedDB.constructDatabase( + userId, + connectionMode: mode, + webUseIndexedDbIfSupported: _webUseIndexedDbIfSupported, + ); @override bool get isConnected => db != null; @@ -97,8 +96,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { ); } _logger.info('connect'); - db = databaseProvider?.call(userId, _connectionMode) ?? - await _defaultDatabaseProvider(userId, _connectionMode); + db = databaseProvider?.call(userId, _connectionMode) ?? await _defaultDatabaseProvider(userId, _connectionMode); } @override @@ -211,6 +209,32 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { ); } + @override + Future deleteMessagesFromUser({ + String? cid, + required String userId, + bool hardDelete = false, + DateTime? deletedAt, + }) async { + assert(_debugIsConnected, ''); + _logger.info('deleteMessagesFromUser'); + + // Delete from both messages and pinned_messages tables + await Future.wait( + [ + db!.messageDao.deleteMessagesByUser, + db!.pinnedMessageDao.deleteMessagesByUser, + ].map( + (f) => f.call( + cid: cid, + userId: userId, + hardDelete: hardDelete, + deletedAt: deletedAt, + ), + ), + ); + } + @override Future getDraftMessageByCid( String cid, { @@ -224,6 +248,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, ''); @@ -265,6 +303,7 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { Future> getChannelStates({ Filter? filter, SortOrder? channelStateSort, + int? messageLimit, PaginationParams? paginationParams, }) async { assert(_debugIsConnected, ''); @@ -272,8 +311,18 @@ class StreamChatPersistenceClient extends ChatPersistenceClient { final channels = await db!.channelQueryDao.getChannels(filter: filter); + final messagePagination = PaginationParams( + // Default limit is set to 25 in backend. + limit: messageLimit ?? 25, + ); + final channelStates = await Future.wait( - channels.map((e) => getChannelStateByCid(e.cid)), + channels.map( + (e) => getChannelStateByCid( + e.cid, + messagePagination: messagePagination, + ), + ), ); // Sort the channel states @@ -394,6 +443,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 +500,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 45099d28e5..b38fb54dae 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.23.0 +version: 10.0.0-beta.13 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -18,11 +18,11 @@ issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues # 2. Add it to the melos.yaml file for future updates. environment: - sdk: ^3.6.2 - flutter: ">=3.27.4" + sdk: ^3.10.0 + flutter: ">=3.38.1" dependencies: - drift: ^2.22.1 + drift: ^2.28.0 flutter: sdk: flutter logging: ^1.2.0 @@ -30,11 +30,11 @@ dependencies: path: ^1.8.3 path_provider: ^2.1.3 sqlite3_flutter_libs: ^0.5.26 - stream_chat: ^9.23.0 + stream_chat: ^10.0.0-beta.13 dev_dependencies: build_runner: ^2.4.9 - drift_dev: ^2.22.1 + drift_dev: ^2.28.0 flutter_test: sdk: flutter mocktail: ^1.0.0 \ No newline at end of file diff --git a/packages/stream_chat_persistence/test/mock_chat_database.dart b/packages/stream_chat_persistence/test/mock_chat_database.dart index 6f1f61af0d..fe4facfd75 100644 --- a/packages/stream_chat_persistence/test/mock_chat_database.dart +++ b/packages/stream_chat_persistence/test/mock_chat_database.dart @@ -19,8 +19,7 @@ class MockChatDatabase extends Mock implements DriftChatDatabase { MessageDao? _messageDao; @override - PinnedMessageDao get pinnedMessageDao => - _pinnedMessageDao ??= MockPinnedMessageDao(); + PinnedMessageDao get pinnedMessageDao => _pinnedMessageDao ??= MockPinnedMessageDao(); PinnedMessageDao? _pinnedMessageDao; @override @@ -32,8 +31,7 @@ class MockChatDatabase extends Mock implements DriftChatDatabase { ReactionDao? _reactionDao; @override - PinnedMessageReactionDao get pinnedMessageReactionDao => - _pinnedMessageReactionDao ??= MockPinnedMessageReactionDao(); + PinnedMessageReactionDao get pinnedMessageReactionDao => _pinnedMessageReactionDao ??= MockPinnedMessageReactionDao(); PinnedMessageReactionDao? _pinnedMessageReactionDao; @override @@ -41,13 +39,11 @@ class MockChatDatabase extends Mock implements DriftChatDatabase { ReadDao? _readDao; @override - ChannelQueryDao get channelQueryDao => - _channelQueryDao ??= MockChannelQueryDao(); + ChannelQueryDao get channelQueryDao => _channelQueryDao ??= MockChannelQueryDao(); ChannelQueryDao? _channelQueryDao; @override - ConnectionEventDao get connectionEventDao => - _connectionEventDao ??= MockConnectionEventDao(); + ConnectionEventDao get connectionEventDao => _connectionEventDao ??= MockConnectionEventDao(); ConnectionEventDao? _connectionEventDao; @override @@ -59,10 +55,13 @@ class MockChatDatabase extends Mock implements DriftChatDatabase { PollVoteDao? _pollVoteDao; @override - DraftMessageDao get draftMessageDao => - _draftMessageDao ??= MockDraftMessageDao(); + DraftMessageDao get draftMessageDao => _draftMessageDao ??= MockDraftMessageDao(); DraftMessageDao? _draftMessageDao; + @override + LocationDao get locationDao => _locationDao ??= MockLocationDao(); + LocationDao? _locationDao; + @override Future flush() => Future.value(); @@ -82,8 +81,7 @@ class MockMemberDao extends Mock implements MemberDao {} class MockReactionDao extends Mock implements ReactionDao {} -class MockPinnedMessageReactionDao extends Mock - implements PinnedMessageReactionDao {} +class MockPinnedMessageReactionDao extends Mock implements PinnedMessageReactionDao {} class MockReadDao extends Mock implements ReadDao {} @@ -96,3 +94,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/channel_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart index da974fa31c..44dde6b1f6 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart @@ -99,13 +99,11 @@ void main() { expect(updatedReads.first.user, dummyUser); // Saving a dummy reaction - final dummyReaction = - Reaction(type: 'type', messageId: messageId, userId: userId); + final dummyReaction = Reaction(type: 'type', messageId: messageId, userId: userId); await database.reactionDao.updateReactions([dummyReaction]); // Should match the dummy reaction - final updatedReactions = - await database.reactionDao.getReactionsByUserId(messageId, userId); + final updatedReactions = await database.reactionDao.getReactionsByUserId(messageId, userId); expect(updatedReactions.length, 1); expect(updatedReactions.first.messageId, messageId); @@ -129,8 +127,7 @@ void main() { expect(reads, isEmpty); // Fetched readtions for passed message id and user id should be empty - final reactions = - await database.reactionDao.getReactionsByUserId(messageId, userId); + final reactions = await database.reactionDao.getReactionsByUserId(messageId, userId); expect(reactions, isEmpty); }); diff --git a/packages/stream_chat_persistence/test/src/dao/draft_message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/draft_message_dao_test.dart index 15c8e48b2f..0a020da477 100644 --- a/packages/stream_chat_persistence/test/src/dao/draft_message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/draft_message_dao_test.dart @@ -62,8 +62,7 @@ void main() { (index) { // When count is 1, use the exact cid provided // Otherwise, create unique cids for each draft to avoid conflicts - final draftChannelCid = - count == 1 ? cid : (withParentMessage ? cid : '$cid$index'); + final draftChannelCid = count == 1 ? cid : (withParentMessage ? cid : '$cid$index'); final draftMessage = DraftMessage( id: 'testDraftId$cid$index', @@ -236,8 +235,7 @@ void main() { await draftMessageDao.updateDraftMessages([firstDraft]); // Verify first draft exists - final firstFetchedDraft = - await draftMessageDao.getDraftMessageByCid(cid); + final firstFetchedDraft = await draftMessageDao.getDraftMessageByCid(cid); expect(firstFetchedDraft, isNotNull); expect(firstFetchedDraft!.message.text, 'First channel draft'); @@ -254,16 +252,13 @@ void main() { await draftMessageDao.updateDraftMessages([secondDraft]); // Verify only the second draft exists - final secondFetchedDraft = - await draftMessageDao.getDraftMessageByCid(cid); + final secondFetchedDraft = await draftMessageDao.getDraftMessageByCid(cid); expect(secondFetchedDraft, isNotNull); expect(secondFetchedDraft!.message.text, 'Second channel draft'); // Verify the first draft no longer exists - final firstDraftAfterUpdate = - await draftMessageDao.getDraftMessageByCid(firstDraft.channelCid); - expect( - firstDraftAfterUpdate!.message.text, isNot('First channel draft')); + final firstDraftAfterUpdate = await draftMessageDao.getDraftMessageByCid(firstDraft.channelCid); + expect(firstDraftAfterUpdate!.message.text, isNot('First channel draft')); // Verify there's only one draft message for this channel final channelDraft = await draftMessageDao.getDraftMessageByCid(cid); @@ -303,8 +298,7 @@ void main() { await draftMessageDao.updateDraftMessages([firstDraft]); // Verify first thread draft exists - final firstFetchedDraft = await draftMessageDao - .getDraftMessageByCid(cid, parentId: firstDraft.parentId); + final firstFetchedDraft = await draftMessageDao.getDraftMessageByCid(cid, parentId: firstDraft.parentId); expect(firstFetchedDraft, isNotNull); expect(firstFetchedDraft!.message.text, 'First thread draft'); @@ -323,20 +317,16 @@ void main() { await draftMessageDao.updateDraftMessages([secondDraft]); // Verify only the second draft exists - final secondFetchedDraft = await draftMessageDao - .getDraftMessageByCid(cid, parentId: secondDraft.parentId); + final secondFetchedDraft = await draftMessageDao.getDraftMessageByCid(cid, parentId: secondDraft.parentId); expect(secondFetchedDraft, isNotNull); expect(secondFetchedDraft!.message.text, 'Second thread draft'); // Verify the first draft no longer exists - final firstDraftAfterUpdate = await draftMessageDao - .getDraftMessageByCid(cid, parentId: firstDraft.parentId); - expect( - firstDraftAfterUpdate!.message.text, isNot('First thread draft')); + final firstDraftAfterUpdate = await draftMessageDao.getDraftMessageByCid(cid, parentId: firstDraft.parentId); + expect(firstDraftAfterUpdate!.message.text, isNot('First thread draft')); // Verify there's only one draft message for this thread - final threadDraft = await draftMessageDao.getDraftMessageByCid(cid, - parentId: parentMessage.id); + final threadDraft = await draftMessageDao.getDraftMessageByCid(cid, parentId: parentMessage.id); expect(threadDraft, isNotNull); expect(threadDraft!.message.text, 'Second thread draft'); }, @@ -387,16 +377,14 @@ void main() { await _prepareTestData(cid, count: 1); // Verify draft exists - final draftBeforeChannelDelete = - await draftMessageDao.getDraftMessageByCid(cid); + final draftBeforeChannelDelete = await draftMessageDao.getDraftMessageByCid(cid); expect(draftBeforeChannelDelete, isNotNull); // Delete the channel await database.channelDao.deleteChannelByCids([cid]); // Verify draft has been deleted (cascade) - final draftAfterChannelDelete = - await draftMessageDao.getDraftMessageByCid(cid); + final draftAfterChannelDelete = await draftMessageDao.getDraftMessageByCid(cid); expect(draftAfterChannelDelete, isNull); }, ); @@ -454,14 +442,12 @@ void main() { ); // Verify drafts exist before channel deletion - final channelDraftBeforeDelete = - await draftMessageDao.getDraftMessageByCid(cid); + final channelDraftBeforeDelete = await draftMessageDao.getDraftMessageByCid(cid); expect(channelDraftBeforeDelete, isNotNull); expect(channelDraftBeforeDelete!.parentId, isNull); for (var i = 0; i < threadDrafts.length; i++) { - final threadDraft = await draftMessageDao.getDraftMessageByCid(cid, - parentId: threadDrafts[i].parentId); + final threadDraft = await draftMessageDao.getDraftMessageByCid(cid, parentId: threadDrafts[i].parentId); expect(threadDraft, isNotNull); expect(threadDraft!.parentId, messages[i].id); } @@ -470,13 +456,11 @@ void main() { await database.channelDao.deleteChannelByCids([cid]); // Verify all drafts have been deleted (cascade) - final channelDraftAfterDelete = - await draftMessageDao.getDraftMessageByCid(cid); + final channelDraftAfterDelete = await draftMessageDao.getDraftMessageByCid(cid); expect(channelDraftAfterDelete, isNull); for (final threadDraft in threadDrafts) { - final draft = await draftMessageDao.getDraftMessageByCid(cid, - parentId: threadDraft.parentId); + final draft = await draftMessageDao.getDraftMessageByCid(cid, parentId: threadDraft.parentId); expect(draft, isNull); } }, @@ -486,21 +470,18 @@ void main() { 'should delete draft messages when referenced parent message is deleted', () async { const cid = 'test:parentRefCascade'; - final testDrafts = - await _prepareTestData(cid, withParentMessage: true, count: 1); + final testDrafts = await _prepareTestData(cid, withParentMessage: true, count: 1); final parentId = testDrafts.first.parentId!; // Verify draft with parent exists - final draftBeforeMessageDelete = - await draftMessageDao.getDraftMessageByCid(cid, parentId: parentId); + final draftBeforeMessageDelete = await draftMessageDao.getDraftMessageByCid(cid, parentId: parentId); expect(draftBeforeMessageDelete, isNotNull); // Delete the parent message await database.messageDao.deleteMessageByIds([parentId]); // Verify draft has been deleted (cascade) - final draftAfterMessageDelete = - await draftMessageDao.getDraftMessageByCid(cid, parentId: parentId); + final draftAfterMessageDelete = await draftMessageDao.getDraftMessageByCid(cid, parentId: parentId); expect(draftAfterMessageDelete, isNull); }, ); 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..a521d2836a --- /dev/null +++ b/packages/stream_chat_persistence/test/src/dao/location_dao_test.dart @@ -0,0 +1,301 @@ +// 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/member_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart index 936097fe8f..88f034a793 100644 --- a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart @@ -129,15 +129,11 @@ void main() { final newFetchedMembers = await memberDao.getMembersByCid(cid); expect(newFetchedMembers.length, fetchedMembers.length + 1); expect( - newFetchedMembers - .firstWhere((it) => it.user!.id == copyMember.user!.id) - .banned, + newFetchedMembers.firstWhere((it) => it.user!.id == copyMember.user!.id).banned, true, ); expect( - newFetchedMembers - .where((it) => it.user!.id == newMember.user!.id) - .isNotEmpty, + newFetchedMembers.where((it) => it.user!.id == newMember.user!.id).isNotEmpty, true, ); }); diff --git a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart index 29cda8c938..80472a1a78 100644 --- a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values + import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; @@ -83,8 +85,7 @@ void main() { type: 'testType', user: users[index], channelRole: 'channel_member', - parentId: - mapAllThreadToFirstMessage ? messages[0].id : messages[index].id, + parentId: mapAllThreadToFirstMessage ? messages[0].id : messages[index].id, createdAt: DateTime.now(), shadowed: math.Random().nextBool(), replyCount: index, @@ -101,11 +102,7 @@ void main() { }, ), ); - final allMessages = [ - ...messages, - if (quoted) ...quotedMessages, - if (threads) ...threadMessages - ]; + final allMessages = [...messages, if (quoted) ...quotedMessages, if (threads) ...threadMessages]; final reaction = Reaction( type: 'type', messageId: allMessages.first.id, @@ -145,8 +142,7 @@ void main() { expect(newMessages.length, messages.length - 2); // Reaction for the first message should be deleted too - final newReactions = - await database.reactionDao.getReactions(firstMessageId); + final newReactions = await database.reactionDao.getReactions(firstMessageId); expect(newReactions, isEmpty); }); @@ -169,8 +165,7 @@ void main() { // Fetched reactions list should have one reaction for given message id final cid1firstMessageId = cid1Messages.first.id; - final cid1Reactions = - await database.reactionDao.getReactions(cid1firstMessageId); + final cid1Reactions = await database.reactionDao.getReactions(cid1firstMessageId); expect(cid1Reactions.length, 1); // Deleting all the messages of cid1 @@ -183,8 +178,7 @@ void main() { expect(cid2FetchedMessages, isNotEmpty); // Reaction for the first message should be deleted too - final cid1FetchedReactions = - await database.reactionDao.getReactions(cid1firstMessageId); + final cid1FetchedReactions = await database.reactionDao.getReactions(cid1firstMessageId); expect(cid1FetchedReactions, isEmpty); }, ); @@ -204,12 +198,10 @@ void main() { // Fetched reactions list should have one reaction for given message id final cid1FirstMessageId = cid1Messages.first.id; - final cid1Reactions = - await database.reactionDao.getReactions(cid1FirstMessageId); + final cid1Reactions = await database.reactionDao.getReactions(cid1FirstMessageId); expect(cid1Reactions.length, 1); final cid2FirstMessageId = cid2Messages.first.id; - final cid2Reactions = - await database.reactionDao.getReactions(cid2FirstMessageId); + final cid2Reactions = await database.reactionDao.getReactions(cid2FirstMessageId); expect(cid2Reactions.length, 1); // Deleting all the messages of cid1 @@ -222,11 +214,9 @@ void main() { expect(cid2FetchedMessages, isEmpty); // Reaction for the first message should be deleted too - final cid1FetchedReactions = - await database.reactionDao.getReactions(cid1FirstMessageId); + final cid1FetchedReactions = await database.reactionDao.getReactions(cid1FirstMessageId); expect(cid1FetchedReactions, isEmpty); - final cid2FetchedReactions = - await database.reactionDao.getReactions(cid2FirstMessageId); + final cid2FetchedReactions = await database.reactionDao.getReactions(cid2FirstMessageId); expect(cid2FetchedReactions, isEmpty); }, ); @@ -282,8 +272,7 @@ void main() { expect(insertedMessages, isNotEmpty); // Should fetch all the thread messages of parentId - final threadMessages = - await messageDao.getThreadMessagesByParentId(parentId); + final threadMessages = await messageDao.getThreadMessagesByParentId(parentId); expect(threadMessages.length, 1); expect(threadMessages.first.parentId, parentId); }); @@ -435,6 +424,185 @@ void main() { ); }); + group('deleteMessagesByUser', () { + const cid1 = 'test:Cid1'; + const cid2 = 'test:Cid2'; + const userId = 'testUserId0'; + + test('hard deletes user messages in specific channel', () async { + // Preparing test data for two channels + await _prepareTestData(cid1); + await _prepareTestData(cid2); + + // Verify messages exist in both channels + final cid1Messages = await messageDao.getMessagesByCid(cid1); + final cid2Messages = await messageDao.getMessagesByCid(cid2); + expect(cid1Messages, isNotEmpty); + expect(cid2Messages, isNotEmpty); + + // Count messages from the specific user in cid1 + final cid1UserMessages = cid1Messages.where((m) => m.user?.id == userId).length; + expect(cid1UserMessages, greaterThan(0)); + + // Hard delete messages from user in cid1 only + await messageDao.deleteMessagesByUser( + cid: cid1, + userId: userId, + hardDelete: true, + ); + + // Verify user's messages are deleted from cid1 + final cid1MessagesAfter = await messageDao.getMessagesByCid(cid1); + final cid1UserMessagesAfter = cid1MessagesAfter.where((m) => m.user?.id == userId).length; + expect(cid1UserMessagesAfter, 0); + + // Verify other users' messages in cid1 are not affected + expect(cid1MessagesAfter.length, cid1Messages.length - cid1UserMessages); + + // Verify messages in cid2 are not affected + final cid2MessagesAfter = await messageDao.getMessagesByCid(cid2); + expect(cid2MessagesAfter.length, cid2Messages.length); + }); + + test('soft deletes user messages in specific channel', () async { + // Preparing test data + await _prepareTestData(cid1); + + final cid1Messages = await messageDao.getMessagesByCid(cid1); + final cid1UserMessages = cid1Messages.where((m) => m.user?.id == userId).toList(); + expect(cid1UserMessages, isNotEmpty); + + // Verify messages are not deleted initially + for (final message in cid1UserMessages) { + expect(message.type, isNot('deleted')); + expect(message.deletedAt, isNull); + } + + // Soft delete messages from user + final deletedAt = DateTime.now(); + await messageDao.deleteMessagesByUser( + cid: cid1, + userId: userId, + hardDelete: false, + deletedAt: deletedAt, + ); + + // Verify messages are marked as deleted + final cid1MessagesAfter = await messageDao.getMessagesByCid(cid1); + final cid1UserMessagesAfter = cid1MessagesAfter.where((m) => m.user?.id == userId).toList(); + + // Messages should still exist in DB + expect(cid1UserMessagesAfter.length, cid1UserMessages.length); + + // But they should be marked as deleted + for (final message in cid1UserMessagesAfter) { + expect(message.type, 'deleted'); + expect(message.deletedAt, isNotNull); + } + + // Other users' messages should not be affected + final otherUserMessages = cid1MessagesAfter.where((m) => m.user?.id != userId).toList(); + for (final message in otherUserMessages) { + expect(message.type, isNot('deleted')); + } + }); + + test('hard deletes user messages across all channels when cid is null', () async { + // Preparing test data for multiple channels + await _prepareTestData(cid1); + await _prepareTestData(cid2); + + final cid1Messages = await messageDao.getMessagesByCid(cid1); + final cid2Messages = await messageDao.getMessagesByCid(cid2); + + final cid1UserMessages = cid1Messages.where((m) => m.user?.id == userId).length; + final cid2UserMessages = cid2Messages.where((m) => m.user?.id == userId).length; + + expect(cid1UserMessages, greaterThan(0)); + expect(cid2UserMessages, greaterThan(0)); + + // Hard delete all messages from user across all channels + await messageDao.deleteMessagesByUser( + userId: userId, + hardDelete: true, + ); + + // Verify user's messages are deleted from both channels + final cid1MessagesAfter = await messageDao.getMessagesByCid(cid1); + final cid2MessagesAfter = await messageDao.getMessagesByCid(cid2); + + expect(cid1MessagesAfter.where((m) => m.user?.id == userId).length, 0); + expect(cid2MessagesAfter.where((m) => m.user?.id == userId).length, 0); + + // Verify other messages are preserved + expect( + cid1MessagesAfter.length, + cid1Messages.length - cid1UserMessages, + ); + expect( + cid2MessagesAfter.length, + cid2Messages.length - cid2UserMessages, + ); + }); + + test('soft deletes user messages across all channels when cid is null', () async { + // Preparing test data for multiple channels + await _prepareTestData(cid1); + await _prepareTestData(cid2); + + final cid1Messages = await messageDao.getMessagesByCid(cid1); + final cid2Messages = await messageDao.getMessagesByCid(cid2); + + final cid1UserMessages = cid1Messages.where((m) => m.user?.id == userId).length; + final cid2UserMessages = cid2Messages.where((m) => m.user?.id == userId).length; + + // Soft delete all messages from user across all channels + await messageDao.deleteMessagesByUser( + userId: userId, + hardDelete: false, + ); + + // Verify user's messages are marked as deleted in both channels + final cid1MessagesAfter = await messageDao.getMessagesByCid(cid1); + final cid2MessagesAfter = await messageDao.getMessagesByCid(cid2); + + final cid1UserMessagesAfter = cid1MessagesAfter.where((m) => m.user?.id == userId).toList(); + final cid2UserMessagesAfter = cid2MessagesAfter.where((m) => m.user?.id == userId).toList(); + + // Messages should still exist + expect(cid1UserMessagesAfter.length, cid1UserMessages); + expect(cid2UserMessagesAfter.length, cid2UserMessages); + + // All user messages should be marked as deleted + for (final message in [...cid1UserMessagesAfter, ...cid2UserMessagesAfter]) { + expect(message.type, 'deleted'); + expect(message.deletedAt, isNotNull); + } + }); + + test('handles thread messages correctly', () async { + // Preparing test data with threads + await _prepareTestData(cid1, threads: true); + + final cid1ThreadMessages = await messageDao.getThreadMessages(cid1); + + final userThreadMessages = cid1ThreadMessages.where((m) => m.user?.id == userId).length; + expect(userThreadMessages, greaterThan(0)); + + // Hard delete all messages from user + await messageDao.deleteMessagesByUser( + cid: cid1, + userId: userId, + hardDelete: true, + ); + + // Verify thread messages from user are also deleted + final cid1ThreadMessagesAfter = await messageDao.getThreadMessages(cid1); + final userThreadMessagesAfter = cid1ThreadMessagesAfter.where((m) => m.user?.id == userId).length; + expect(userThreadMessagesAfter, 0); + }); + }); + tearDown(() async { await database.disconnect(); }); diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart index 0bd5166e52..ca7567b1de 100644 --- a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values + import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; @@ -73,8 +75,7 @@ void main() { type: 'testType', user: users[index], channelRole: 'channel_member', - parentId: - mapAllThreadToFirstMessage ? messages[0].id : messages[index].id, + parentId: mapAllThreadToFirstMessage ? messages[0].id : messages[index].id, createdAt: DateTime.now(), shadowed: math.Random().nextBool(), replyCount: index, @@ -86,11 +87,7 @@ void main() { pinnedBy: User(id: 'testUserId$index'), ), ); - final allMessages = [ - ...messages, - if (quoted) ...quotedMessages, - if (threads) ...threadMessages - ]; + final allMessages = [...messages, if (quoted) ...quotedMessages, if (threads) ...threadMessages]; final reaction = Reaction( type: 'type', messageId: allMessages.first.id, @@ -116,8 +113,7 @@ void main() { final firstMessageId = messages.first.id; // Fetched reactions list should have one reaction for given message id - final reactions = - await database.pinnedMessageReactionDao.getReactions(firstMessageId); + final reactions = await database.pinnedMessageReactionDao.getReactions(firstMessageId); expect(reactions.length, 1); // Deleting 2 messages from DB @@ -131,8 +127,7 @@ void main() { expect(newMessages.length, messages.length - 2); // Reaction for the first message should be deleted too - final newReactions = - await database.pinnedMessageReactionDao.getReactions(firstMessageId); + final newReactions = await database.pinnedMessageReactionDao.getReactions(firstMessageId); expect(newReactions, isEmpty); }); @@ -155,24 +150,20 @@ void main() { // Fetched reactions list should have one reaction for given message id final cid1firstMessageId = cid1Messages.first.id; - final cid1Reactions = await database.pinnedMessageReactionDao - .getReactions(cid1firstMessageId); + final cid1Reactions = await database.pinnedMessageReactionDao.getReactions(cid1firstMessageId); expect(cid1Reactions.length, 1); // Deleting all the messages of cid1 await pinnedMessageDao.deleteMessageByCids([cid1]); // Fetched messages length of only cid1 should be empty - final cid1FetchedMessages = - await pinnedMessageDao.getMessagesByCid(cid1); - final cid2FetchedMessages = - await pinnedMessageDao.getMessagesByCid(cid2); + final cid1FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid2); expect(cid1FetchedMessages, isEmpty); expect(cid2FetchedMessages, isNotEmpty); // Reaction for the first message should be deleted too - final cid1FetchedReactions = await database.pinnedMessageReactionDao - .getReactions(cid1firstMessageId); + final cid1FetchedReactions = await database.pinnedMessageReactionDao.getReactions(cid1firstMessageId); expect(cid1FetchedReactions, isEmpty); }, ); @@ -192,31 +183,25 @@ void main() { // Fetched reactions list should have one reaction for given message id final cid1FirstMessageId = cid1Messages.first.id; - final cid1Reactions = await database.pinnedMessageReactionDao - .getReactions(cid1FirstMessageId); + final cid1Reactions = await database.pinnedMessageReactionDao.getReactions(cid1FirstMessageId); expect(cid1Reactions.length, 1); final cid2FirstMessageId = cid2Messages.first.id; - final cid2Reactions = await database.pinnedMessageReactionDao - .getReactions(cid2FirstMessageId); + final cid2Reactions = await database.pinnedMessageReactionDao.getReactions(cid2FirstMessageId); expect(cid2Reactions.length, 1); // Deleting all the messages of cid1 await pinnedMessageDao.deleteMessageByCids([cid1, cid2]); // Fetched messages length of both cid1 and cid2 should be empty - final cid1FetchedMessages = - await pinnedMessageDao.getMessagesByCid(cid1); - final cid2FetchedMessages = - await pinnedMessageDao.getMessagesByCid(cid2); + final cid1FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2FetchedMessages = await pinnedMessageDao.getMessagesByCid(cid2); expect(cid1FetchedMessages, isEmpty); expect(cid2FetchedMessages, isEmpty); // Reaction for the first message should be deleted too - final cid1FetchedReactions = await database.pinnedMessageReactionDao - .getReactions(cid1FirstMessageId); + final cid1FetchedReactions = await database.pinnedMessageReactionDao.getReactions(cid1FirstMessageId); expect(cid1FetchedReactions, isEmpty); - final cid2FetchedReactions = await database.pinnedMessageReactionDao - .getReactions(cid2FirstMessageId); + final cid2FetchedReactions = await database.pinnedMessageReactionDao.getReactions(cid2FirstMessageId); expect(cid2FetchedReactions, isEmpty); }, ); @@ -264,8 +249,7 @@ void main() { const parentId = 'testMessageId${cid}0'; // Messages should be empty initially - final messages = - await pinnedMessageDao.getThreadMessagesByParentId(parentId); + final messages = await pinnedMessageDao.getThreadMessagesByParentId(parentId); expect(messages, isEmpty); // Preparing test data @@ -273,8 +257,7 @@ void main() { expect(insertedMessages, isNotEmpty); // Should fetch all the thread messages of parentId - final threadMessages = - await pinnedMessageDao.getThreadMessagesByParentId(parentId); + final threadMessages = await pinnedMessageDao.getThreadMessagesByParentId(parentId); expect(threadMessages.length, 1); expect(threadMessages.first.parentId, parentId); }); @@ -426,6 +409,169 @@ void main() { ); }); + group('deleteMessagesByUser', () { + const cid1 = 'test:Cid1'; + const cid2 = 'test:Cid2'; + const userId = 'testUserId0'; + + test('hard deletes user pinned messages in specific channel', () async { + // Preparing test data for two channels + await _prepareTestData(cid1); + await _prepareTestData(cid2); + + // Verify messages exist in both channels + final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid1Messages, isNotEmpty); + expect(cid2Messages, isNotEmpty); + + // Count messages from the specific user in cid1 + final cid1UserMessages = cid1Messages.where((m) => m.user?.id == userId).length; + expect(cid1UserMessages, greaterThan(0)); + + // Hard delete messages from user in cid1 only + await pinnedMessageDao.deleteMessagesByUser( + cid: cid1, + userId: userId, + hardDelete: true, + ); + + // Verify user's messages are deleted from cid1 + final cid1MessagesAfter = await pinnedMessageDao.getMessagesByCid(cid1); + final cid1UserMessagesAfter = cid1MessagesAfter.where((m) => m.user?.id == userId).length; + expect(cid1UserMessagesAfter, 0); + + // Verify other users' messages in cid1 are not affected + expect(cid1MessagesAfter.length, cid1Messages.length - cid1UserMessages); + + // Verify messages in cid2 are not affected + final cid2MessagesAfter = await pinnedMessageDao.getMessagesByCid(cid2); + expect(cid2MessagesAfter.length, cid2Messages.length); + }); + + test('soft deletes user pinned messages in specific channel', () async { + // Preparing test data + await _prepareTestData(cid1); + + final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid1UserMessages = cid1Messages.where((m) => m.user?.id == userId).toList(); + expect(cid1UserMessages, isNotEmpty); + + // Verify messages are not deleted initially + for (final message in cid1UserMessages) { + expect(message.type, isNot('deleted')); + expect(message.deletedAt, isNull); + } + + // Soft delete messages from user + final deletedAt = DateTime.now(); + await pinnedMessageDao.deleteMessagesByUser( + cid: cid1, + userId: userId, + hardDelete: false, + deletedAt: deletedAt, + ); + + // Verify messages are marked as deleted + final cid1MessagesAfter = await pinnedMessageDao.getMessagesByCid(cid1); + final cid1UserMessagesAfter = cid1MessagesAfter.where((m) => m.user?.id == userId).toList(); + + // Messages should still exist in DB + expect(cid1UserMessagesAfter.length, cid1UserMessages.length); + + // But they should be marked as deleted + for (final message in cid1UserMessagesAfter) { + expect(message.type, 'deleted'); + expect(message.deletedAt, isNotNull); + } + + // Other users' messages should not be affected + final otherUserMessages = cid1MessagesAfter.where((m) => m.user?.id != userId).toList(); + for (final message in otherUserMessages) { + expect(message.type, isNot('deleted')); + } + }); + + test('hard deletes user pinned messages across all channels when cid null', () async { + // Preparing test data for multiple channels + await _prepareTestData(cid1); + await _prepareTestData(cid2); + + final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); + + final cid1UserMessages = cid1Messages.where((m) => m.user?.id == userId).length; + final cid2UserMessages = cid2Messages.where((m) => m.user?.id == userId).length; + + expect(cid1UserMessages, greaterThan(0)); + expect(cid2UserMessages, greaterThan(0)); + + // Hard delete all messages from user across all channels + await pinnedMessageDao.deleteMessagesByUser( + userId: userId, + hardDelete: true, + ); + + // Verify user's messages are deleted from both channels + final cid1MessagesAfter = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2MessagesAfter = await pinnedMessageDao.getMessagesByCid(cid2); + + expect( + cid1MessagesAfter.where((m) => m.user?.id == userId).length, + 0, + ); + expect( + cid2MessagesAfter.where((m) => m.user?.id == userId).length, + 0, + ); + + // Verify other messages are preserved + expect( + cid1MessagesAfter.length, + cid1Messages.length - cid1UserMessages, + ); + expect( + cid2MessagesAfter.length, + cid2Messages.length - cid2UserMessages, + ); + }); + + test('soft deletes user pinned messages across all channels when cid null', () async { + // Preparing test data for multiple channels + await _prepareTestData(cid1); + await _prepareTestData(cid2); + + final cid1Messages = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2Messages = await pinnedMessageDao.getMessagesByCid(cid2); + + final cid1UserMessages = cid1Messages.where((m) => m.user?.id == userId).length; + final cid2UserMessages = cid2Messages.where((m) => m.user?.id == userId).length; + + // Soft delete all messages from user across all channels + await pinnedMessageDao.deleteMessagesByUser( + userId: userId, + hardDelete: false, + ); + + // Verify user's messages are marked as deleted in both channels + final cid1MessagesAfter = await pinnedMessageDao.getMessagesByCid(cid1); + final cid2MessagesAfter = await pinnedMessageDao.getMessagesByCid(cid2); + + final cid1UserMessagesAfter = cid1MessagesAfter.where((m) => m.user?.id == userId).toList(); + final cid2UserMessagesAfter = cid2MessagesAfter.where((m) => m.user?.id == userId).toList(); + + // Messages should still exist + expect(cid1UserMessagesAfter.length, cid1UserMessages); + expect(cid2UserMessagesAfter.length, cid2UserMessages); + + // All user messages should be marked as deleted + for (final message in [...cid1UserMessagesAfter, ...cid2UserMessagesAfter]) { + expect(message.type, 'deleted'); + expect(message.deletedAt, isNotNull); + } + }); + }); + 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..4b76fe095c 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); @@ -72,10 +80,17 @@ void main() { // Fetched reaction length should match inserted reactions length. // Every reaction messageId should match the provided messageId. - final fetchedReactions = - await pinnedMessageReactionDao.getReactions(messageId); + final fetchedReactions = 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 { @@ -83,23 +98,28 @@ void main() { const userId = 'testUserId'; // Should be empty initially - final reactions = - await pinnedMessageReactionDao.getReactionsByUserId(messageId, userId); + final reactions = await pinnedMessageReactionDao.getReactionsByUserId(messageId, userId); expect(reactions, isEmpty); // Adding sample reactions - final insertedReactions = - await _prepareReactionData(messageId, userId: userId); + final insertedReactions = await _prepareReactionData(messageId, userId: userId); expect(insertedReactions, isNotEmpty); // Fetched reaction length should match inserted reactions length. // Every reaction messageId should match the provided messageId. // Every reaction userId should match the provided userId. - final fetchedReactions = - await pinnedMessageReactionDao.getReactionsByUserId(messageId, userId); + final fetchedReactions = await pinnedMessageReactionDao.getReactionsByUserId(messageId, userId); 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 +129,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 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); + 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))), ); }); @@ -153,10 +181,8 @@ void main() { // Fetched reaction list length should match // the inserted reactions list length - final reactions1 = - await pinnedMessageReactionDao.getReactions(messageId1); - final reactions2 = - await pinnedMessageReactionDao.getReactions(messageId2); + final reactions1 = await pinnedMessageReactionDao.getReactions(messageId1); + final reactions2 = await pinnedMessageReactionDao.getReactions(messageId2); expect(reactions1.length, insertedReactions1.length); expect(reactions2.length, insertedReactions2.length); @@ -164,36 +190,30 @@ void main() { await pinnedMessageReactionDao.deleteReactionsByMessageIds([messageId1]); // Fetched reactions length of only messageId1 should be empty - final fetchedReactions1 = - await pinnedMessageReactionDao.getReactions(messageId1); - final fetchedReactions2 = - await pinnedMessageReactionDao.getReactions(messageId2); + final fetchedReactions1 = await pinnedMessageReactionDao.getReactions(messageId1); + final fetchedReactions2 = await pinnedMessageReactionDao.getReactions(messageId2); 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); // Fetched reaction list length should match // the inserted reactions list length - final reactions1 = - await pinnedMessageReactionDao.getReactions(messageId1); - final reactions2 = - await pinnedMessageReactionDao.getReactions(messageId2); + final reactions1 = await pinnedMessageReactionDao.getReactions(messageId1); + final reactions2 = await pinnedMessageReactionDao.getReactions(messageId2); expect(reactions1.length, insertedReactions1.length); expect(reactions2.length, insertedReactions2.length); // Deleting all the reactions of messageId1 and messageId2 - await pinnedMessageReactionDao - .deleteReactionsByMessageIds([messageId1, messageId2]); + await pinnedMessageReactionDao.deleteReactionsByMessageIds([messageId1, messageId2]); // Fetched reactions length of both messages should be empty - final fetchedReactions1 = - await pinnedMessageReactionDao.getReactions(messageId1); - final fetchedReactions2 = - await pinnedMessageReactionDao.getReactions(messageId2); + final fetchedReactions1 = await pinnedMessageReactionDao.getReactions(messageId1); + final fetchedReactions2 = await pinnedMessageReactionDao.getReactions(messageId2); expect(fetchedReactions1, isEmpty); expect(fetchedReactions2, isEmpty); }); diff --git a/packages/stream_chat_persistence/test/src/dao/poll_vote_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/poll_vote_dao_test.dart index 093aac84bf..2c13ee1cdb 100644 --- a/packages/stream_chat_persistence/test/src/dao/poll_vote_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/poll_vote_dao_test.dart @@ -44,9 +44,7 @@ void main() { (key, value) => MapEntry(key, value.length), ); - final users = latestVotesByOption.values - .expand((it) => it.map((it) => it.user!)) - .toList(); + final users = latestVotesByOption.values.expand((it) => it.map((it) => it.user!)).toList(); final poll = Poll( id: pollId, @@ -114,11 +112,13 @@ void main() { final fetchedPollVotes = await pollVoteDao.getPollVotes(pollId); expect(fetchedPollVotes.length, pollVotes.length + 1); expect( - fetchedPollVotes.any((it) => - it.id == newPollVote.id && - it.pollId == newPollVote.pollId && - it.optionId == newPollVote.optionId && - it.answerText == newPollVote.answerText), + fetchedPollVotes.any( + (it) => + it.id == newPollVote.id && + it.pollId == newPollVote.pollId && + it.optionId == newPollVote.optionId && + it.answerText == newPollVote.answerText, + ), true, ); }); 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..2252641bc1 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 { @@ -86,18 +102,24 @@ void main() { expect(reactions, isEmpty); // Adding sample reactions - final insertedReactions = - await _prepareReactionData(messageId, userId: userId); + final insertedReactions = await _prepareReactionData(messageId, userId: userId); expect(insertedReactions, isNotEmpty); // Fetched reaction length should match inserted reactions length. // Every reaction messageId should match the provided messageId. // Every reaction userId should match the provided userId. - final fetchedReactions = - await reactionDao.getReactionsByUserId(messageId, userId); + final fetchedReactions = await reactionDao.getReactionsByUserId(messageId, userId); 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 +129,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 +195,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/dao/read_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart index 142daf4155..473e2de82f 100644 --- a/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart @@ -56,10 +56,8 @@ void main() { expect(fetchedRead.user.id, insertedRead.user.id); expect(fetchedRead.lastRead, isSameDateAs(insertedRead.lastRead)); expect(fetchedRead.unreadMessages, insertedRead.unreadMessages); - expect(fetchedRead.lastDeliveredAt, - isSameDateAs(insertedRead.lastDeliveredAt)); - expect(fetchedRead.lastDeliveredMessageId, - insertedRead.lastDeliveredMessageId); + expect(fetchedRead.lastDeliveredAt, isSameDateAs(insertedRead.lastDeliveredAt)); + expect(fetchedRead.lastDeliveredMessageId, insertedRead.lastDeliveredMessageId); } }); @@ -89,16 +87,12 @@ void main() { final fetchedReads = await readDao.getReadsByCid(cid); expect(fetchedReads.length, insertedReads.length + 1); expect( - fetchedReads - .firstWhere((it) => it.user.id == copyRead.user.id) - .unreadMessages, + fetchedReads.firstWhere((it) => it.user.id == copyRead.user.id).unreadMessages, 33, ); expect( fetchedReads - .where((it) => - it.user.id == newRead.user.id && - it.unreadMessages == newRead.unreadMessages) + .where((it) => it.user.id == newRead.user.id && it.unreadMessages == newRead.unreadMessages) .isNotEmpty, true, ); diff --git a/packages/stream_chat_persistence/test/src/db/drift_chat_database_test.dart b/packages/stream_chat_persistence/test/src/db/drift_chat_database_test.dart index 996c152fb1..dd14acf8c1 100644 --- a/packages/stream_chat_persistence/test/src/db/drift_chat_database_test.dart +++ b/packages/stream_chat_persistence/test/src/db/drift_chat_database_test.dart @@ -4,8 +4,7 @@ import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_persistence/src/db/drift_chat_database.dart'; -DatabaseConnection _backgroundConnection() => - DatabaseConnection(NativeDatabase.memory()); +DatabaseConnection _backgroundConnection() => DatabaseConnection(NativeDatabase.memory()); void main() { test( 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..ba728659d2 --- /dev/null +++ b/packages/stream_chat_persistence/test/src/mapper/location_mapper_test.dart @@ -0,0 +1,140 @@ +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.timestamp().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.timestamp(); + final updatedAt = DateTime.timestamp(); + final endAt = DateTime.timestamp().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.timestamp(); + final updatedAt = DateTime.timestamp(); + final endAt = DateTime.timestamp().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.timestamp().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/member_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart index 0c2bea8fca..a577a6d078 100644 --- a/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart @@ -24,6 +24,7 @@ void main() { pinnedAt: DateTime.now(), archivedAt: DateTime.now(), isModerator: math.Random().nextBool(), + deletedMessages: ['msg1', 'msg2', 'msg3'], extraData: {'test_extra_data': 'testData'}, ); final member = entity.toMember(user: user); @@ -40,6 +41,7 @@ void main() { expect(member.pinnedAt, isSameDateAs(entity.pinnedAt)); expect(member.archivedAt, isSameDateAs(entity.archivedAt)); expect(member.isModerator, entity.isModerator); + expect(member.deletedMessages, entity.deletedMessages); expect(member.extraData, entity.extraData); }); @@ -59,6 +61,7 @@ void main() { pinnedAt: DateTime.now(), archivedAt: DateTime.now(), isModerator: math.Random().nextBool(), + deletedMessages: const ['msg1', 'msg2', 'msg3'], extraData: const {'test_extra_data': 'testData'}, ); final entity = member.toEntity(cid: cid); @@ -76,6 +79,7 @@ void main() { expect(entity.pinnedAt, isSameDateAs(member.pinnedAt)); expect(entity.archivedAt, isSameDateAs(member.archivedAt)); expect(entity.isModerator, member.isModerator); + expect(entity.deletedMessages, member.deletedMessages); expect(entity.extraData, member.extraData); }); } diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart index 0c26454f94..be7cc74e9b 100644 --- a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -81,6 +81,7 @@ void main() { channelRole: 'channel_member', localDeletedAt: DateTime.now(), remoteDeletedAt: DateTime.now().add(const Duration(seconds: 1)), + deletedForMe: false, messageText: 'Hello', pinned: true, pinExpires: DateTime.now().toUtc(), @@ -114,8 +115,7 @@ void main() { expect(message.shadowed, entity.shadowed); expect(message.showInChannel, entity.showInChannel); for (var i = 0; i < message.mentionedUsers.length; i++) { - final entityMentionedUser = - User.fromJson(jsonDecode(entity.mentionedUsers[i])); + final entityMentionedUser = User.fromJson(jsonDecode(entity.mentionedUsers[i])); expect(message.mentionedUsers[i].id, entityMentionedUser.id); } expect(message.replyCount, entity.replyCount); @@ -131,6 +131,7 @@ void main() { expect(message.user!.id, entity.userId); expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt)); expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt)); + expect(message.deletedForMe, entity.deletedForMe); expect(message.text, entity.messageText); expect(message.channelRole, entity.channelRole); expect(message.pinned, entity.pinned); @@ -221,6 +222,7 @@ void main() { channelRole: 'channel_member', localDeletedAt: DateTime.now(), deletedAt: DateTime.now().add(const Duration(seconds: 1)), + deletedForMe: true, text: 'Hello', pinned: true, pinExpires: DateTime.now(), @@ -246,8 +248,7 @@ void main() { expect(entity.shadowed, message.shadowed); expect(entity.showInChannel, message.showInChannel); expect(entity.replyCount, message.replyCount); - expect( - entity.mentionedUsers, message.mentionedUsers.map(jsonEncode).toList()); + expect(entity.mentionedUsers, message.mentionedUsers.map(jsonEncode).toList()); expect(entity.state, jsonEncode(message.state)); expect(entity.localUpdatedAt, isSameDateAs(message.localUpdatedAt)); expect(entity.remoteUpdatedAt, isSameDateAs(message.remoteUpdatedAt)); @@ -259,6 +260,7 @@ void main() { expect(entity.userId, message.user!.id); expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt)); expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt)); + expect(entity.deletedForMe, message.deletedForMe); expect(entity.messageText, message.text); expect(entity.channelRole, message.channelRole); expect(entity.pinned, message.pinned); diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart index e0bb30feba..170026665c 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart @@ -81,6 +81,7 @@ void main() { channelRole: 'channel_member', localDeletedAt: DateTime.now(), remoteDeletedAt: DateTime.now().add(const Duration(seconds: 1)), + deletedForMe: false, messageText: 'Hello', pinned: true, pinExpires: DateTime.now().toUtc(), @@ -114,8 +115,7 @@ void main() { expect(message.shadowed, entity.shadowed); expect(message.showInChannel, entity.showInChannel); for (var i = 0; i < message.mentionedUsers.length; i++) { - final entityMentionedUser = - User.fromJson(jsonDecode(entity.mentionedUsers[i])); + final entityMentionedUser = User.fromJson(jsonDecode(entity.mentionedUsers[i])); expect(message.mentionedUsers[i].id, entityMentionedUser.id); } expect(message.replyCount, entity.replyCount); @@ -131,6 +131,7 @@ void main() { expect(message.user!.id, entity.userId); expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt)); expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt)); + expect(message.deletedForMe, entity.deletedForMe); expect(message.text, entity.messageText); expect(message.channelRole, entity.channelRole); expect(message.pinned, entity.pinned); @@ -221,6 +222,7 @@ void main() { channelRole: 'channel_member', localDeletedAt: DateTime.now(), deletedAt: DateTime.now().add(const Duration(seconds: 1)), + deletedForMe: true, text: 'Hello', pinned: true, pinExpires: DateTime.now(), @@ -246,8 +248,7 @@ void main() { expect(entity.shadowed, message.shadowed); expect(entity.showInChannel, message.showInChannel); expect(entity.replyCount, message.replyCount); - expect( - entity.mentionedUsers, message.mentionedUsers.map(jsonEncode).toList()); + expect(entity.mentionedUsers, message.mentionedUsers.map(jsonEncode).toList()); expect(entity.state, jsonEncode(message.state)); expect(entity.localUpdatedAt, isSameDateAs(message.localUpdatedAt)); expect(entity.remoteUpdatedAt, isSameDateAs(message.remoteUpdatedAt)); @@ -259,6 +260,7 @@ void main() { expect(entity.userId, message.user!.id); expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt)); expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt)); + expect(entity.deletedForMe, message.deletedForMe); expect(entity.messageText, message.text); expect(entity.channelRole, message.channelRole); expect(entity.pinned, message.pinned); 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/src/utils/date_matcher.dart b/packages/stream_chat_persistence/test/src/utils/date_matcher.dart index d19d25f2a6..1d49e4a719 100644 --- a/packages/stream_chat_persistence/test/src/utils/date_matcher.dart +++ b/packages/stream_chat_persistence/test/src/utils/date_matcher.dart @@ -1,7 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; -Matcher isSameDateAs(DateTime? targetDate) => - _IsSameDateAs(targetDate: targetDate); +Matcher isSameDateAs(DateTime? targetDate) => _IsSameDateAs(targetDate: targetDate); class _IsSameDateAs extends Matcher { const _IsSameDateAs({required this.targetDate}); @@ -19,6 +18,5 @@ class _IsSameDateAs extends Matcher { } @override - Description describe(Description description) => - description.add('is same date as $targetDate'); + Description describe(Description description) => description.add('is same date as $targetDate'); } 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 018327e927..3fe63bef88 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 @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_redundant_argument_values + import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; @@ -113,20 +115,16 @@ void main() { const parentId = 'testParentId'; final replies = List.generate(3, (index) => Message(id: 'testId$index')); - when(() => mockDatabase.messageDao.getThreadMessagesByParentId(parentId)) - .thenAnswer((_) async => replies); + when(() => mockDatabase.messageDao.getThreadMessagesByParentId(parentId)).thenAnswer((_) async => replies); final fetchedReplies = await client.getReplies(parentId); expect(fetchedReplies.length, replies.length); - verify(() => - mockDatabase.messageDao.getThreadMessagesByParentId(parentId)) - .called(1); + verify(() => mockDatabase.messageDao.getThreadMessagesByParentId(parentId)).called(1); }); test('getConnectionInfo', () async { final event = Event(); - when(() => mockDatabase.connectionEventDao.connectionEvent) - .thenAnswer((_) async => event); + when(() => mockDatabase.connectionEventDao.connectionEvent).thenAnswer((_) async => event); final fetchedEvent = await client.getConnectionInfo(); expect(fetchedEvent, isNotNull); @@ -136,8 +134,7 @@ void main() { test('getLastSyncAt', () async { final lastSync = DateTime.now(); - when(() => mockDatabase.connectionEventDao.lastSyncAt) - .thenAnswer((_) async => lastSync); + when(() => mockDatabase.connectionEventDao.lastSyncAt).thenAnswer((_) async => lastSync); final fetchedLastSync = await client.getLastSyncAt(); expect(fetchedLastSync, isSameDateAs(lastSync)); @@ -146,28 +143,23 @@ void main() { test('updateConnectionInfo', () async { final event = Event(); - when(() => mockDatabase.connectionEventDao.updateConnectionEvent(event)) - .thenAnswer((_) async => 1); + when(() => mockDatabase.connectionEventDao.updateConnectionEvent(event)).thenAnswer((_) async => 1); await client.updateConnectionInfo(event); - verify(() => mockDatabase.connectionEventDao.updateConnectionEvent(event)) - .called(1); + verify(() => mockDatabase.connectionEventDao.updateConnectionEvent(event)).called(1); }); test('updateLastSyncAt', () async { final lastSync = DateTime.now(); - when(() => mockDatabase.connectionEventDao.updateLastSyncAt(lastSync)) - .thenAnswer((_) async => 1); + when(() => mockDatabase.connectionEventDao.updateLastSyncAt(lastSync)).thenAnswer((_) async => 1); await client.updateLastSyncAt(lastSync); - verify(() => mockDatabase.connectionEventDao.updateLastSyncAt(lastSync)) - .called(1); + verify(() => mockDatabase.connectionEventDao.updateLastSyncAt(lastSync)).called(1); }); test('getChannelCids', () async { final channelCids = List.generate(3, (index) => 'testCid$index'); - when(() => mockDatabase.channelDao.cids) - .thenAnswer((_) async => channelCids); + when(() => mockDatabase.channelDao.cids).thenAnswer((_) async => channelCids); final fetchedChannelCids = await client.getChannelCids(); expect(fetchedChannelCids.length, channelCids.length); @@ -177,8 +169,7 @@ void main() { test('getChannelByCid', () async { const cid = 'testType:testId'; final channelModel = ChannelModel(cid: cid); - when(() => mockDatabase.channelDao.getChannelByCid(cid)) - .thenAnswer((_) async => channelModel); + when(() => mockDatabase.channelDao.getChannelByCid(cid)).thenAnswer((_) async => channelModel); final fetchedChannelModel = await client.getChannelByCid(cid); expect(fetchedChannelModel, isNotNull); @@ -189,8 +180,7 @@ void main() { test('getMembersByCid', () async { const cid = 'testCid'; final members = List.generate(3, (index) => Member()); - when(() => mockDatabase.memberDao.getMembersByCid(cid)) - .thenAnswer((_) async => members); + when(() => mockDatabase.memberDao.getMembersByCid(cid)).thenAnswer((_) async => members); final fetchedMembers = await client.getMembersByCid(cid); expect(fetchedMembers.length, members.length); @@ -207,8 +197,7 @@ void main() { lastReadMessageId: 'lastMessageId$index', ), ); - when(() => mockDatabase.readDao.getReadsByCid(cid)) - .thenAnswer((_) async => reads); + when(() => mockDatabase.readDao.getReadsByCid(cid)).thenAnswer((_) async => reads); final fetchedReads = await client.getReadsByCid(cid); expect(fetchedReads.length, reads.length); @@ -218,8 +207,7 @@ void main() { test('getMessagesByCid', () async { const cid = 'testCid'; final messages = List.generate(3, (index) => Message()); - when(() => mockDatabase.messageDao.getMessagesByCid(cid)) - .thenAnswer((_) async => messages); + when(() => mockDatabase.messageDao.getMessagesByCid(cid)).thenAnswer((_) async => messages); final fetchedMessages = await client.getMessagesByCid(cid); expect(fetchedMessages.length, messages.length); @@ -229,13 +217,11 @@ void main() { test('getPinnedMessagesByCid', () async { const cid = 'testCid'; final messages = List.generate(3, (index) => Message()); - when(() => mockDatabase.pinnedMessageDao.getMessagesByCid(cid)) - .thenAnswer((_) async => messages); + when(() => mockDatabase.pinnedMessageDao.getMessagesByCid(cid)).thenAnswer((_) async => messages); final fetchedMessages = await client.getPinnedMessagesByCid(cid); expect(fetchedMessages.length, messages.length); - verify(() => mockDatabase.pinnedMessageDao.getMessagesByCid(cid)) - .called(1); + verify(() => mockDatabase.pinnedMessageDao.getMessagesByCid(cid)).called(1); }); test('getChannelStateByCid', () async { @@ -260,21 +246,14 @@ void main() { ), ); - when(() => mockDatabase.draftMessageDao.getDraftMessageByCid(cid)) - .thenAnswer((_) async => draft); - - when(() => mockDatabase.memberDao.getMembersByCid(cid)) - .thenAnswer((_) async => members); - when(() => mockDatabase.readDao.getReadsByCid(cid)) - .thenAnswer((_) async => reads); - when(() => mockDatabase.channelDao.getChannelByCid(cid)) - .thenAnswer((_) async => channel); - when(() => mockDatabase.messageDao.getMessagesByCid(cid)) - .thenAnswer((_) async => messages); - when(() => mockDatabase.pinnedMessageDao.getMessagesByCid(cid)) - .thenAnswer((_) async => messages); - when(() => mockDatabase.draftMessageDao.getDraftMessageByCid(cid)) - .thenAnswer((_) async => draft); + when(() => mockDatabase.draftMessageDao.getDraftMessageByCid(cid)).thenAnswer((_) async => draft); + + when(() => mockDatabase.memberDao.getMembersByCid(cid)).thenAnswer((_) async => members); + when(() => mockDatabase.readDao.getReadsByCid(cid)).thenAnswer((_) async => reads); + when(() => mockDatabase.channelDao.getChannelByCid(cid)).thenAnswer((_) async => channel); + when(() => mockDatabase.messageDao.getMessagesByCid(cid)).thenAnswer((_) async => messages); + when(() => mockDatabase.pinnedMessageDao.getMessagesByCid(cid)).thenAnswer((_) async => messages); + when(() => mockDatabase.draftMessageDao.getDraftMessageByCid(cid)).thenAnswer((_) async => draft); final fetchedChannelState = await client.getChannelStateByCid(cid); expect(fetchedChannelState.messages?.length, messages.length); @@ -288,10 +267,8 @@ void main() { verify(() => mockDatabase.readDao.getReadsByCid(cid)).called(1); verify(() => mockDatabase.channelDao.getChannelByCid(cid)).called(1); verify(() => mockDatabase.messageDao.getMessagesByCid(cid)).called(1); - verify(() => mockDatabase.pinnedMessageDao.getMessagesByCid(cid)) - .called(1); - verify(() => mockDatabase.draftMessageDao.getDraftMessageByCid(cid)) - .called(1); + verify(() => mockDatabase.pinnedMessageDao.getMessagesByCid(cid)).called(1); + verify(() => mockDatabase.draftMessageDao.getDraftMessageByCid(cid)).called(1); }); group('getChannelState', () { @@ -321,18 +298,15 @@ void main() { ) .toList(growable: false); - when(() => mockDatabase.channelQueryDao.getChannels()) - .thenAnswer((_) async => channels); - when(() => mockDatabase.memberDao.getMembersByCid(cid)) - .thenAnswer((_) async => members); - when(() => mockDatabase.readDao.getReadsByCid(cid)) - .thenAnswer((_) async => reads); - when(() => mockDatabase.channelDao.getChannelByCid(cid)) - .thenAnswer((_) async => channel); - when(() => mockDatabase.messageDao.getMessagesByCid(cid)) - .thenAnswer((_) async => messages); - when(() => mockDatabase.pinnedMessageDao.getMessagesByCid(cid)) - .thenAnswer((_) async => messages); + when(() => mockDatabase.channelQueryDao.getChannels()).thenAnswer((_) async => channels); + when(() => mockDatabase.memberDao.getMembersByCid(cid)).thenAnswer((_) async => members); + when(() => mockDatabase.readDao.getReadsByCid(cid)).thenAnswer((_) async => reads); + when(() => mockDatabase.channelDao.getChannelByCid(cid)).thenAnswer((_) async => channel); + when( + () => mockDatabase.messageDao.getMessagesByCid(cid, messagePagination: any(named: 'messagePagination')), + ).thenAnswer((_) async => messages); + when(() => mockDatabase.pinnedMessageDao.getMessagesByCid(cid)).thenAnswer((_) async => messages); + when(() => mockDatabase.draftMessageDao.getDraftMessageByCid(cid)).thenAnswer((_) async => null); final fetchedChannelStates = await client.getChannelStates(); expect(fetchedChannelStates.length, channelStates.length); @@ -342,8 +316,7 @@ void main() { final fetched = fetchedChannelStates[i]; expect(fetched.members?.length, original.members?.length); expect(fetched.messages?.length, original.messages?.length); - expect( - fetched.pinnedMessages?.length, original.pinnedMessages?.length); + expect(fetched.pinnedMessages?.length, original.pinnedMessages?.length); expect(fetched.read?.length, original.read?.length); expect(fetched.channel!.cid, original.channel!.cid); } @@ -352,90 +325,74 @@ void main() { verify(() => mockDatabase.memberDao.getMembersByCid(cid)).called(3); verify(() => mockDatabase.readDao.getReadsByCid(cid)).called(3); verify(() => mockDatabase.channelDao.getChannelByCid(cid)).called(3); - verify(() => mockDatabase.messageDao.getMessagesByCid(cid)).called(3); - verify(() => mockDatabase.pinnedMessageDao.getMessagesByCid(cid)) - .called(3); + verify( + () => mockDatabase.messageDao.getMessagesByCid(cid, messagePagination: any(named: 'messagePagination')), + ).called(3); + verify(() => mockDatabase.pinnedMessageDao.getMessagesByCid(cid)).called(3); + verify(() => mockDatabase.draftMessageDao.getDraftMessageByCid(cid)).called(3); }); }); test('updateChannelQueries', () async { final filter = Filter.in_('members', const ['testUserId']); const cids = []; - when(() => - mockDatabase.channelQueryDao.updateChannelQueries(filter, cids)) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.channelQueryDao.updateChannelQueries(filter, cids)).thenAnswer((_) => Future.value()); await client.updateChannelQueries(filter, cids); - verify(() => - mockDatabase.channelQueryDao.updateChannelQueries(filter, cids)) - .called(1); + verify(() => mockDatabase.channelQueryDao.updateChannelQueries(filter, cids)).called(1); }); test('deleteMessageById', () async { const messageId = 'testMessageId'; - when(() => mockDatabase.messageDao.deleteMessageByIds([messageId])) - .thenAnswer((_) async => 1); + when(() => mockDatabase.messageDao.deleteMessageByIds([messageId])).thenAnswer((_) async => 1); await client.deleteMessageById(messageId); - verify(() => mockDatabase.messageDao.deleteMessageByIds([messageId])) - .called(1); + verify(() => mockDatabase.messageDao.deleteMessageByIds([messageId])).called(1); }); test('deletePinnedMessageById', () async { const messageId = 'testMessageId'; - when(() => mockDatabase.pinnedMessageDao.deleteMessageByIds([messageId])) - .thenAnswer((_) async => 1); + when(() => mockDatabase.pinnedMessageDao.deleteMessageByIds([messageId])).thenAnswer((_) async => 1); await client.deletePinnedMessageById(messageId); - verify(() => - mockDatabase.pinnedMessageDao.deleteMessageByIds([messageId])) - .called(1); + verify(() => mockDatabase.pinnedMessageDao.deleteMessageByIds([messageId])).called(1); }); test('deleteMessageByIds', () async { const messageIds = []; - when(() => mockDatabase.messageDao.deleteMessageByIds(messageIds)) - .thenAnswer((_) async => 1); + when(() => mockDatabase.messageDao.deleteMessageByIds(messageIds)).thenAnswer((_) async => 1); await client.deleteMessageByIds(messageIds); - verify(() => mockDatabase.messageDao.deleteMessageByIds(messageIds)) - .called(1); + verify(() => mockDatabase.messageDao.deleteMessageByIds(messageIds)).called(1); }); test('deletePinnedMessageByIds', () async { const messageIds = []; - when(() => mockDatabase.pinnedMessageDao.deleteMessageByIds(messageIds)) - .thenAnswer((_) async => 1); + when(() => mockDatabase.pinnedMessageDao.deleteMessageByIds(messageIds)).thenAnswer((_) async => 1); await client.deletePinnedMessageByIds(messageIds); - verify(() => mockDatabase.pinnedMessageDao.deleteMessageByIds(messageIds)) - .called(1); + verify(() => mockDatabase.pinnedMessageDao.deleteMessageByIds(messageIds)).called(1); }); test('deleteMessageByCid', () async { const cid = 'testCid'; - when(() => mockDatabase.messageDao.deleteMessageByCids([cid])) - .thenAnswer((_) async => 1); + when(() => mockDatabase.messageDao.deleteMessageByCids([cid])).thenAnswer((_) async => 1); await client.deleteMessageByCid(cid); - verify(() => mockDatabase.messageDao.deleteMessageByCids([cid])) - .called(1); + verify(() => mockDatabase.messageDao.deleteMessageByCids([cid])).called(1); }); test('deletePinnedMessageByCid', () async { const cid = 'testCid'; - when(() => mockDatabase.pinnedMessageDao.deleteMessageByCids([cid])) - .thenAnswer((_) async => 1); + when(() => mockDatabase.pinnedMessageDao.deleteMessageByCids([cid])).thenAnswer((_) async => 1); await client.deletePinnedMessageByCid(cid); - verify(() => mockDatabase.pinnedMessageDao.deleteMessageByCids([cid])) - .called(1); + verify(() => mockDatabase.pinnedMessageDao.deleteMessageByCids([cid])).called(1); }); test('deleteMessageByCids', () async { const cids = []; - when(() => mockDatabase.messageDao.deleteMessageByCids(cids)) - .thenAnswer((_) async => 1); + when(() => mockDatabase.messageDao.deleteMessageByCids(cids)).thenAnswer((_) async => 1); await client.deleteMessageByCids(cids); verify(() => mockDatabase.messageDao.deleteMessageByCids(cids)).called(1); @@ -443,18 +400,15 @@ void main() { test('deletePinnedMessageByCids', () async { const cids = []; - when(() => mockDatabase.pinnedMessageDao.deleteMessageByCids(cids)) - .thenAnswer((_) async => 1); + when(() => mockDatabase.pinnedMessageDao.deleteMessageByCids(cids)).thenAnswer((_) async => 1); await client.deletePinnedMessageByCids(cids); - verify(() => mockDatabase.pinnedMessageDao.deleteMessageByCids(cids)) - .called(1); + verify(() => mockDatabase.pinnedMessageDao.deleteMessageByCids(cids)).called(1); }); test('deleteChannels', () async { const cids = []; - when(() => mockDatabase.channelDao.deleteChannelByCids(cids)) - .thenAnswer((_) async => 1); + when(() => mockDatabase.channelDao.deleteChannelByCids(cids)).thenAnswer((_) async => 1); await client.deleteChannels(cids); verify(() => mockDatabase.channelDao.deleteChannelByCids(cids)).called(1); @@ -464,12 +418,10 @@ void main() { const cid = 'testCid'; final messages = List.generate(3, (index) => Message()); - when(() => mockDatabase.messageDao.bulkUpdateMessages({cid: messages})) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.messageDao.bulkUpdateMessages({cid: messages})).thenAnswer((_) => Future.value()); await client.updateMessages(cid, messages); - verify(() => mockDatabase.messageDao.bulkUpdateMessages({cid: messages})) - .called(1); + verify(() => mockDatabase.messageDao.bulkUpdateMessages({cid: messages})).called(1); }); test('updatePinnedMessages', () async { @@ -487,8 +439,7 @@ void main() { test('getChannelThreads', () async { const cid = 'testCid'; - final messages = - List.generate(3, (index) => Message(parentId: 'testParentId$index')); + final messages = List.generate(3, (index) => Message(parentId: 'testParentId$index')); final threads = messages.fold>>( {}, (prev, curr) => prev @@ -498,8 +449,7 @@ void main() { ifAbsent: () => [], ), ); - when(() => mockDatabase.messageDao.getThreadMessages(cid)) - .thenAnswer((realInvocation) async => messages); + when(() => mockDatabase.messageDao.getThreadMessages(cid)).thenAnswer((realInvocation) async => messages); final fetchedThreads = await client.getChannelThreads(cid); expect(fetchedThreads.length, threads.length); @@ -515,8 +465,7 @@ void main() { test('updateChannels', () async { const cid = 'testType:testId'; final channels = List.generate(3, (index) => ChannelModel(cid: cid)); - when(() => mockDatabase.channelDao.updateChannels(channels)) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.channelDao.updateChannels(channels)).thenAnswer((_) => Future.value()); await client.updateChannels(channels); verify(() => mockDatabase.channelDao.updateChannels(channels)).called(1); @@ -525,10 +474,8 @@ void main() { test('updatePolls', () async { const name = 'testPollName'; final options = List.generate(3, (index) => PollOption(text: '$index')); - final polls = - List.generate(3, (index) => Poll(name: name, options: options)); - when(() => mockDatabase.pollDao.updatePolls(polls)) - .thenAnswer((_) => Future.value()); + final polls = List.generate(3, (index) => Poll(name: name, options: options)); + when(() => mockDatabase.pollDao.updatePolls(polls)).thenAnswer((_) => Future.value()); await client.updatePolls(polls); verify(() => mockDatabase.pollDao.updatePolls(polls)).called(1); @@ -536,43 +483,35 @@ void main() { test('deletePollsByIds', () async { final pollIds = ['testPollId']; - when(() => mockDatabase.pollDao.deletePollsByIds(pollIds)) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.pollDao.deletePollsByIds(pollIds)).thenAnswer((_) => Future.value()); await client.deletePollsByIds(pollIds); verify(() => mockDatabase.pollDao.deletePollsByIds(pollIds)).called(1); }); test('updatePollVotes', () async { - final pollVotes = List.generate( - 3, (index) => PollVote(id: '$index', optionId: 'testOptionId$index')); - when(() => mockDatabase.pollVoteDao.updatePollVotes(pollVotes)) - .thenAnswer((_) => Future.value()); + final pollVotes = List.generate(3, (index) => PollVote(id: '$index', optionId: 'testOptionId$index')); + when(() => mockDatabase.pollVoteDao.updatePollVotes(pollVotes)).thenAnswer((_) => Future.value()); await client.updatePollVotes(pollVotes); - verify(() => mockDatabase.pollVoteDao.updatePollVotes(pollVotes)) - .called(1); + verify(() => mockDatabase.pollVoteDao.updatePollVotes(pollVotes)).called(1); }); test('deletePollVotesByPollIds', () async { final pollIds = ['testPollId']; - when(() => mockDatabase.pollVoteDao.deletePollVotesByPollIds(pollIds)) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.pollVoteDao.deletePollVotesByPollIds(pollIds)).thenAnswer((_) => Future.value()); await client.deletePollVotesByPollIds(pollIds); - verify(() => mockDatabase.pollVoteDao.deletePollVotesByPollIds(pollIds)) - .called(1); + verify(() => mockDatabase.pollVoteDao.deletePollVotesByPollIds(pollIds)).called(1); }); test('updateMembers', () async { const cid = 'testCid'; final members = List.generate(3, (index) => Member()); - when(() => mockDatabase.memberDao.bulkUpdateMembers({cid: members})) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.memberDao.bulkUpdateMembers({cid: members})).thenAnswer((_) => Future.value()); await client.updateMembers(cid, members); - verify(() => mockDatabase.memberDao.bulkUpdateMembers({cid: members})) - .called(1); + verify(() => mockDatabase.memberDao.bulkUpdateMembers({cid: members})).called(1); }); test('updateReads', () async { @@ -585,18 +524,15 @@ void main() { lastReadMessageId: 'lastMessageId$index', ), ); - when(() => mockDatabase.readDao.bulkUpdateReads({cid: reads})) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.readDao.bulkUpdateReads({cid: reads})).thenAnswer((_) => Future.value()); await client.updateReads(cid, reads); - verify(() => mockDatabase.readDao.bulkUpdateReads({cid: reads})) - .called(1); + verify(() => mockDatabase.readDao.bulkUpdateReads({cid: reads})).called(1); }); test('updateUsers', () async { final users = List.generate(3, (index) => User(id: 'testUserId$index')); - when(() => mockDatabase.userDao.updateUsers(users)) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.userDao.updateUsers(users)).thenAnswer((_) => Future.value()); await client.updateUsers(users); verify(() => mockDatabase.userDao.updateUsers(users)).called(1); @@ -607,12 +543,10 @@ void main() { 3, (index) => Reaction(type: 'testType$index'), ); - when(() => mockDatabase.reactionDao.updateReactions(reactions)) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.reactionDao.updateReactions(reactions)).thenAnswer((_) => Future.value()); await client.updateReactions(reactions); - verify(() => mockDatabase.reactionDao.updateReactions(reactions)) - .called(1); + verify(() => mockDatabase.reactionDao.updateReactions(reactions)).called(1); }); test('updatePinnedMessageReactions', () async { @@ -620,43 +554,33 @@ void main() { 3, (index) => Reaction(type: 'testType$index'), ); - when(() => - mockDatabase.pinnedMessageReactionDao.updateReactions(reactions)) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.pinnedMessageReactionDao.updateReactions(reactions)).thenAnswer((_) => Future.value()); await client.updatePinnedMessageReactions(reactions); - verify(() => - mockDatabase.pinnedMessageReactionDao.updateReactions(reactions)) - .called(1); + verify(() => mockDatabase.pinnedMessageReactionDao.updateReactions(reactions)).called(1); }); test('deleteReactionsByMessageId', () async { final messageIds = []; - when(() => - mockDatabase.reactionDao.deleteReactionsByMessageIds(messageIds)) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.reactionDao.deleteReactionsByMessageIds(messageIds)).thenAnswer((_) => Future.value()); await client.deleteReactionsByMessageId(messageIds); - verify(() => - mockDatabase.reactionDao.deleteReactionsByMessageIds(messageIds)) - .called(1); + verify(() => mockDatabase.reactionDao.deleteReactionsByMessageIds(messageIds)).called(1); }); test('deletePinnedMessageReactionsByMessageId', () async { final messageIds = []; - when(() => mockDatabase.pinnedMessageReactionDao - .deleteReactionsByMessageIds(messageIds)) - .thenAnswer((_) => Future.value()); + when( + () => mockDatabase.pinnedMessageReactionDao.deleteReactionsByMessageIds(messageIds), + ).thenAnswer((_) => Future.value()); await client.deletePinnedMessageReactionsByMessageId(messageIds); - verify(() => mockDatabase.pinnedMessageReactionDao - .deleteReactionsByMessageIds(messageIds)).called(1); + verify(() => mockDatabase.pinnedMessageReactionDao.deleteReactionsByMessageIds(messageIds)).called(1); }); test('deleteMembersByCids', () async { final cids = []; - when(() => mockDatabase.memberDao.deleteMemberByCids(cids)) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.memberDao.deleteMemberByCids(cids)).thenAnswer((_) => Future.value()); await client.deleteMembersByCids(cids); verify(() => mockDatabase.memberDao.deleteMemberByCids(cids)).called(1); @@ -664,12 +588,10 @@ void main() { test('deleteDraftMessagesByCids', () async { final cids = []; - when(() => mockDatabase.draftMessageDao.deleteDraftMessagesByCids(cids)) - .thenAnswer((_) => Future.value()); + when(() => mockDatabase.draftMessageDao.deleteDraftMessagesByCids(cids)).thenAnswer((_) => Future.value()); await client.deleteDraftMessagesByCids(cids); - verify(() => mockDatabase.draftMessageDao.deleteDraftMessagesByCids(cids)) - .called(1); + verify(() => mockDatabase.draftMessageDao.deleteDraftMessagesByCids(cids)).called(1); }); test('getDraftMessageByCid', () async { @@ -685,16 +607,14 @@ void main() { ), ); - when(() => mockDatabase.draftMessageDao.getDraftMessageByCid(cid)) - .thenAnswer((_) async => draft); + when(() => mockDatabase.draftMessageDao.getDraftMessageByCid(cid)).thenAnswer((_) async => draft); final fetchedDraft = await client.getDraftMessageByCid(cid); expect(fetchedDraft, isNotNull); expect(fetchedDraft!.channelCid, cid); expect(fetchedDraft.message.id, draft.message.id); expect(fetchedDraft.message.text, draft.message.text); - verify(() => mockDatabase.draftMessageDao.getDraftMessageByCid(cid)) - .called(1); + verify(() => mockDatabase.draftMessageDao.getDraftMessageByCid(cid)).called(1); }); test('updateDraftMessages', () async { @@ -710,24 +630,242 @@ void main() { ), ); - when(() => mockDatabase.draftMessageDao.updateDraftMessages(drafts)) - .thenAnswer((_) async {}); + when(() => mockDatabase.draftMessageDao.updateDraftMessages(drafts)).thenAnswer((_) async {}); await client.updateDraftMessages(drafts); - verify(() => mockDatabase.draftMessageDao.updateDraftMessages(drafts)) - .called(1); + verify(() => mockDatabase.draftMessageDao.updateDraftMessages(drafts)).called(1); }); test('deleteDraftMessageByCid', () async { const cid = 'testCid'; const parentId = 'testParentId'; - when(() => mockDatabase.draftMessageDao.deleteDraftMessageByCid(cid, - parentId: parentId)).thenAnswer((_) async {}); + when( + () => mockDatabase.draftMessageDao.deleteDraftMessageByCid(cid, parentId: parentId), + ).thenAnswer((_) async {}); await client.deleteDraftMessageByCid(cid, parentId: parentId); - verify(() => mockDatabase.draftMessageDao - .deleteDraftMessageByCid(cid, parentId: parentId)).called(1); + verify(() => mockDatabase.draftMessageDao.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); + }); + + group('deleteMessagesFromUser', () { + const userId = 'testUserId'; + const cid = 'testCid'; + + test('calls deleteMessagesByUser on both DAOs with hard delete', () async { + when( + () => mockDatabase.messageDao.deleteMessagesByUser( + cid: cid, + userId: userId, + hardDelete: true, + deletedAt: any(named: 'deletedAt'), + ), + ).thenAnswer((_) async => 1); + + when( + () => mockDatabase.pinnedMessageDao.deleteMessagesByUser( + cid: cid, + userId: userId, + hardDelete: true, + deletedAt: any(named: 'deletedAt'), + ), + ).thenAnswer((_) async => 1); + + await client.deleteMessagesFromUser( + cid: cid, + userId: userId, + hardDelete: true, + ); + + verify( + () => mockDatabase.messageDao.deleteMessagesByUser( + cid: cid, + userId: userId, + hardDelete: true, + deletedAt: any(named: 'deletedAt'), + ), + ).called(1); + + verify( + () => mockDatabase.pinnedMessageDao.deleteMessagesByUser( + cid: cid, + userId: userId, + hardDelete: true, + deletedAt: any(named: 'deletedAt'), + ), + ).called(1); + }); + + test('calls deleteMessagesByUser on both DAOs with soft delete', () async { + final deletedAt = DateTime.now(); + + when( + () => mockDatabase.messageDao.deleteMessagesByUser( + cid: cid, + userId: userId, + hardDelete: false, + deletedAt: deletedAt, + ), + ).thenAnswer((_) async => 1); + + when( + () => mockDatabase.pinnedMessageDao.deleteMessagesByUser( + cid: cid, + userId: userId, + hardDelete: false, + deletedAt: deletedAt, + ), + ).thenAnswer((_) async => 1); + + await client.deleteMessagesFromUser( + cid: cid, + userId: userId, + hardDelete: false, + deletedAt: deletedAt, + ); + + verify( + () => mockDatabase.messageDao.deleteMessagesByUser( + cid: cid, + userId: userId, + hardDelete: false, + deletedAt: deletedAt, + ), + ).called(1); + + verify( + () => mockDatabase.pinnedMessageDao.deleteMessagesByUser( + cid: cid, + userId: userId, + hardDelete: false, + deletedAt: deletedAt, + ), + ).called(1); + }); + + test('calls deleteMessagesByUser without cid when cid is null', () async { + when( + () => mockDatabase.messageDao.deleteMessagesByUser( + userId: userId, + hardDelete: true, + deletedAt: any(named: 'deletedAt'), + ), + ).thenAnswer((_) async => 1); + + when( + () => mockDatabase.pinnedMessageDao.deleteMessagesByUser( + userId: userId, + hardDelete: true, + deletedAt: any(named: 'deletedAt'), + ), + ).thenAnswer((_) async => 1); + + await client.deleteMessagesFromUser( + userId: userId, + hardDelete: true, + ); + + verify( + () => mockDatabase.messageDao.deleteMessagesByUser( + userId: userId, + hardDelete: true, + deletedAt: any(named: 'deletedAt'), + ), + ).called(1); + + verify( + () => mockDatabase.pinnedMessageDao.deleteMessagesByUser( + userId: userId, + hardDelete: true, + deletedAt: any(named: 'deletedAt'), + ), + ).called(1); + }); }); tearDown(() async { diff --git a/pubspec.lock b/pubspec.lock index 9eed33197b..b4283bfbc5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" url: "https://pub.dev" source: hosted - version: "82.0.0" + version: "93.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "13c1e6c6fd460522ea840abec3f677cc226f5fec7872c04ad7b425517ccf54f7" + sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b url: "https://pub.dev" source: hosted - version: "7.4.4" + version: "10.0.1" ansi_styles: dependency: transitive description: @@ -61,10 +61,10 @@ packages: dependency: transitive description: name: built_value - sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" url: "https://pub.dev" source: hosted - version: "8.9.5" + version: "8.12.3" charcode: dependency: transitive description: @@ -77,18 +77,18 @@ packages: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" cli_launcher: dependency: transitive description: name: cli_launcher - sha256: "5e7e0282b79e8642edd6510ee468ae2976d847a0a29b3916e85f5fa1bfe24005" + sha256: "17d2744fb9a254c49ec8eda582536abe714ea0131533e24389843a4256f82eac" url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "0.3.2+1" cli_util: dependency: transitive description: @@ -97,22 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.2" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" code_builder: dependency: "direct dev" description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.11.1" collection: dependency: transitive description: @@ -125,10 +117,10 @@ packages: dependency: transitive description: name: conventional_commit - sha256: fad254feb6fb8eace2be18855176b0a4b97e0d50e416ff0fe590d5ba83735d34 + sha256: c40b1b449ce2a63fa2ce852f35e3890b1e182f5951819934c0e4a66254bc0dc3 url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.1+1" convert: dependency: transitive description: @@ -141,18 +133,18 @@ packages: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" dart_style: dependency: "direct dev" description: name: dart_style - sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + sha256: "15a7db352c8fc6a4d2bc475ba901c25b39fe7157541da4c16eacce6f8be83e49" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.5" file: dependency: transitive description: @@ -189,10 +181,10 @@ packages: dependency: transitive description: name: http - sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.6.0" http_parser: dependency: transitive description: @@ -201,14 +193,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" - intl: - dependency: transitive - description: - name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf - url: "https://pub.dev" - source: hosted - version: "0.19.0" io: dependency: transitive description: @@ -221,42 +205,42 @@ packages: dependency: transitive description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.10.0" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" melos: dependency: "direct dev" description: name: melos - sha256: "3f3ab3f902843d1e5a1b1a4dd39a4aca8ba1056f2d32fd8995210fa2843f646f" + sha256: "4280dc46bd5b741887cce1e67e5c1a6aaf3c22310035cf5bd33dceeeda62ed22" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" meta: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "9f29b9bcc8ee287b1a31e0d01be0eae99a930dbffdaecf04b3f3d82a969f296f" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.18.1" mustache_template: dependency: transitive description: name: mustache_template - sha256: a46e26f91445bfb0b60519be280555b06792460b27b19e2b19ad5b9740df5d1c + sha256: "4326d0002ff58c74b9486990ccbdab08157fca3c996fe9e197aff9d61badf307" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.3" package_config: dependency: transitive description: @@ -285,18 +269,18 @@ packages: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.2" process: dependency: transitive description: name: process - sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 url: "https://pub.dev" source: hosted - version: "5.0.3" + version: "5.0.5" prompts: dependency: transitive description: @@ -317,10 +301,10 @@ packages: dependency: transitive description: name: pub_updater - sha256: "54e8dc865349059ebe7f163d6acce7c89eb958b8047e6d6e80ce93b13d7c9e60" + sha256: "739a0161d73a6974c0675b864fb0cf5147305f7b077b7f03a58fa7a9ab3e7e7d" url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.5.0" pubspec_parse: dependency: transitive description: @@ -381,10 +365,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.9" typed_data: dependency: transitive description: @@ -397,10 +381,10 @@ packages: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.1" web: dependency: transitive description: @@ -421,9 +405,9 @@ packages: dependency: transitive description: name: yaml_edit - sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5 + sha256: ec709065bb2c911b336853b67f3732dd13e0336bd065cc2f1061d7610ddf45e3 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" sdks: - dart: ">=3.6.2 <4.0.0" + dart: ">=3.10.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index a65d06f2b8..2ac3430d00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_workspace environment: - sdk: ^3.6.2 + sdk: ^3.10.0 dev_dependencies: code_builder: ^4.10.1 diff --git a/sample_app/android/app/src/main/AndroidManifest.xml b/sample_app/android/app/src/main/AndroidManifest.xml index 8ff8f0abb5..28a3985094 100644 --- a/sample_app/android/app/src/main/AndroidManifest.xml +++ b/sample_app/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,18 @@ + + + + + + + + + + + + ???? CFBundleVersion 1.0 - MinimumOSVersion - 12.0 diff --git a/sample_app/ios/Runner.xcodeproj/project.pbxproj b/sample_app/ios/Runner.xcodeproj/project.pbxproj index 17806dba6c..5543119cb8 100644 --- a/sample_app/ios/Runner.xcodeproj/project.pbxproj +++ b/sample_app/ios/Runner.xcodeproj/project.pbxproj @@ -286,6 +286,7 @@ "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", "${BUILT_PRODUCTS_DIR}/gal/gal.framework", + "${BUILT_PRODUCTS_DIR}/geolocator_apple/geolocator_apple.framework", "${BUILT_PRODUCTS_DIR}/get_thumbnail_video/get_thumbnail_video.framework", "${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework", "${BUILT_PRODUCTS_DIR}/just_audio/just_audio.framework", @@ -329,6 +330,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/gal.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/geolocator_apple.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/get_thumbnail_video.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/just_audio.framework", diff --git a/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c53e2b314e..9c12df59c6 100644 --- a/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/sample_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> ???? 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/ios/fastlane/Fastfile b/sample_app/ios/fastlane/Fastfile index 20f466bc78..2cdb0391c6 100644 --- a/sample_app/ios/fastlane/Fastfile +++ b/sample_app/ios/fastlane/Fastfile @@ -116,6 +116,29 @@ platform :ios do ) end + # Usage: bundle exec fastlane ios distribute_to_testflight_internal + lane :distribute_to_testflight_internal do + match_me + + current_build_number = latest_testflight_build_number( + api_key: appstore_api_key, + app_identifier: app_identifier + ) + + build_number = (current_build_number || 0).to_i + 1 + build_ipa(export_method: "app-store", build_number: build_number) + + upload_to_testflight( + api_key: appstore_api_key, + distribute_external: false, + notify_external_testers: false, + ipa: "#{root_path}/build/ios/ipa/ChatSample.ipa", + groups: ['Internal Testers'], + changelog: 'Lots of amazing new features to test out!', + skip_waiting_for_build_processing: true, + ) + end + private_lane :appstore_api_key do @appstore_api_key ||= app_store_connect_api_key( key_id: 'MT3PRT8TB7', diff --git a/sample_app/lib/app.dart b/sample_app/lib/app.dart index 7fc8ba93b8..cbe9c9507d 100644 --- a/sample_app/lib/app.dart +++ b/sample_app/lib/app.dart @@ -7,8 +7,7 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart' hide Priority; -import 'package:flutter_local_notifications/flutter_local_notifications.dart' - hide Message; +import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Message; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -21,6 +20,9 @@ import 'package:sample_app/state/init_data.dart'; import 'package:sample_app/utils/app_config.dart'; import 'package:sample_app/utils/local_notification_observer.dart'; import 'package:sample_app/utils/localizations.dart'; +import 'package:sample_app/widgets/custom_message_actions.dart'; +import 'package:sample_app/widgets/location/location_attachment.dart'; +import 'package:sample_app/widgets/location/location_detail_dialog.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_localizations/stream_chat_localizations.dart'; @@ -43,8 +45,7 @@ const notificationChannelDescription = 'Notifications for Stream messages'; const bool kIsIOS = bool.fromEnvironment('dart.io.is_ios'); // Initialize FlutterLocalNotificationsPlugin for background messages -final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); +final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); /// Constructs callback for background notification handling. /// @@ -179,8 +180,7 @@ Future _showAndroidNotification({ await flutterLocalNotificationsPlugin.initialize( initializationSettings, onDidReceiveNotificationResponse: (NotificationResponse response) async { - debugPrint( - '[onBackgroundMessage] #firebase; notification clicked: ${response.payload}'); + debugPrint('[onBackgroundMessage] #firebase; notification clicked: ${response.payload}'); // The payload contains the channel information (channelType:channelId) // This will be handled when the app is opened }, @@ -199,10 +199,10 @@ Future _showAndroidNotification({ ); debugPrint( - '[onBackgroundMessage] #firebase; android notification shown successfully: ID=$notificationId, Title="$title"'); + '[onBackgroundMessage] #firebase; android notification shown successfully: ID=$notificationId, Title="$title"', + ); } catch (e) { - debugPrint( - '[onBackgroundMessage] #firebase; failed to show notification: $e'); + debugPrint('[onBackgroundMessage] #firebase; failed to show notification: $e'); } } @@ -220,21 +220,17 @@ Future _createNotificationChannel() async { ); // Create the channel - final androidPlugin = - flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>(); + final androidPlugin = flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation(); if (androidPlugin != null) { await androidPlugin.createNotificationChannel(channel); - debugPrint( - '[onBackgroundMessage] #firebase; notification channel created'); + debugPrint('[onBackgroundMessage] #firebase; notification channel created'); } else { - debugPrint( - '[onBackgroundMessage] #firebase; failed to resolve Android plugin'); + debugPrint('[onBackgroundMessage] #firebase; failed to resolve Android plugin'); } } catch (e) { - debugPrint( - '[onBackgroundMessage] #firebase; notification channel failed: $e'); + debugPrint('[onBackgroundMessage] #firebase; notification channel failed: $e'); } } @@ -315,10 +311,7 @@ class _StreamChatSampleAppState extends State Future _initFirebaseMessaging(StreamChatClient client) async { userIdSubscription?.cancel(); - userIdSubscription = client.state.currentUserStream - .map((it) => it?.id) - .distinct() - .listen((userId) async { + userIdSubscription = client.state.currentUserStream.map((it) => it?.id).distinct().listen((userId) async { // User logged in if (userId != null) { // Requests notification permission. @@ -326,23 +319,24 @@ class _StreamChatSampleAppState extends State // Sets callback for background messages. FirebaseMessaging.onBackgroundMessage(_onFirebaseBackgroundMessage); // Sets callback for the notification click event. - firebaseSubscriptions.add(FirebaseMessaging.onMessageOpenedApp - .listen(_onFirebaseMessageOpenedApp(client))); + firebaseSubscriptions.add(FirebaseMessaging.onMessageOpenedApp.listen(_onFirebaseMessageOpenedApp(client))); // Sets callback for foreground messages - firebaseSubscriptions.add(FirebaseMessaging.onMessage - .listen(_onFirebaseForegroundMessage(client))); + firebaseSubscriptions.add(FirebaseMessaging.onMessage.listen(_onFirebaseForegroundMessage(client))); // Sets callback for the token refresh event. - firebaseSubscriptions.add(FirebaseMessaging.instance.onTokenRefresh - .listen(_onFirebaseTokenRefresh(client))); - - final token = await FirebaseMessaging.instance.getToken(); - debugPrint('[onTokenInit] #firebase; token: $token'); - if (token != null) { - // replace with your push provider, e.g., 'PushProvider.xiaomi' - const pushProvider = PushProvider.firebase; - - // add Token to Stream - await client.addDevice(token, pushProvider); + firebaseSubscriptions.add(FirebaseMessaging.instance.onTokenRefresh.listen(_onFirebaseTokenRefresh(client))); + + try { + final token = await FirebaseMessaging.instance.getToken(); + debugPrint('[onTokenInit] #firebase; token: $token'); + if (token != null) { + // replace with your push provider, e.g., 'PushProvider.xiaomi' + const pushProvider = PushProvider.firebase; + + // add Token to Stream + await client.addDevice(token, pushProvider); + } + } catch (e) { + debugPrint('[onTokenInit] #firebase; failed to get token: $e'); } } // User logged out @@ -386,8 +380,7 @@ class _StreamChatSampleAppState extends State /// Constructs callback for foreground notification handling. OnRemoteMessage _onFirebaseForegroundMessage(StreamChatClient client) { return (message) async { - debugPrint( - '[onForegroundMessage] #firebase; message: ${message.toMap()}'); + debugPrint('[onForegroundMessage] #firebase; message: ${message.toMap()}'); }; } @@ -441,25 +434,25 @@ 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() { if (localNotificationObserver != null) { localNotificationObserver!.dispose(); } - localNotificationObserver = LocalNotificationObserver( - _initNotifier.initData!.client, _navigatorKey); + localNotificationObserver = LocalNotificationObserver(_initNotifier.initData!.client, _navigatorKey); - return GoRouter( + return router ??= GoRouter( refreshListenable: _initNotifier, initialLocation: Routes.CHANNEL_LIST_PAGE.path, navigatorKey: _navigatorKey, observers: [localNotificationObserver!], redirect: (context, state) { - final loggedIn = - _initNotifier.initData?.client.state.currentUser != null; - final loggingIn = state.matchedLocation == Routes.CHOOSE_USER.path || - state.matchedLocation == Routes.ADVANCED_OPTIONS.path; + final loggedIn = _initNotifier.initData?.client.state.currentUser != null; + final loggingIn = + state.matchedLocation == Routes.CHOOSE_USER.path || state.matchedLocation == Routes.ADVANCED_OPTIONS.path; if (!loggedIn) { return loggingIn ? null : Routes.CHOOSE_USER.path; @@ -493,32 +486,60 @@ class _StreamChatSampleAppState extends State 'theme', defaultValue: 0, ), - builder: (context, snapshot) => MaterialApp.router( - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - themeMode: const { - -1: ThemeMode.dark, - 0: ThemeMode.system, - 1: ThemeMode.light, - }[snapshot], - supportedLocales: const [ - Locale('en'), - Locale('it'), - ], - localizationsDelegates: const [ - AppLocalizationsDelegate(), - GlobalStreamChatLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ], - builder: (context, child) => StreamChat( - client: _initNotifier.initData!.client, - streamChatConfigData: StreamChatConfigurationData( - draftMessagesEnabled: true, + builder: (context, snapshot) => PreferenceBuilder( + preference: _initNotifier.initData!.preferences.getBool( + 'forceRtl', + defaultValue: false, + ), + builder: (context, forceRtl) => MaterialApp.router( + theme: ThemeData( + brightness: .light, + extensions: [StreamTheme.light()], + ), + darkTheme: ThemeData( + brightness: .dark, + extensions: [StreamTheme.dark()], + ), + themeMode: const { + -1: ThemeMode.dark, + 0: ThemeMode.system, + 1: ThemeMode.light, + }[snapshot], + supportedLocales: const [ + Locale('en'), + Locale('it'), + ], + localizationsDelegates: const [ + AppLocalizationsDelegate(), + GlobalStreamChatLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + builder: (context, child) => Directionality( + textDirection: forceRtl ? TextDirection.rtl : TextDirection.ltr, + child: StreamChat( + client: _initNotifier.initData!.client, + componentBuilders: StreamComponentBuilders( + extensions: streamChatComponentBuilders( + messageWidget: customMessageWidgetBuilder, + ), + ), + streamChatConfigData: StreamChatConfigurationData( + draftMessagesEnabled: true, + enforceUniqueReactions: false, + attachmentBuilders: [ + LocationAttachmentBuilder( + onAttachmentTap: (context, location) { + showLocationDetailDialog(context: context, location: location); + }, + ), + ], + ), + child: child, + ), ), - child: child, + routerConfig: _setupRouter(), ), - routerConfig: _setupRouter(), ), ); }, diff --git a/sample_app/lib/firebase_options.dart b/sample_app/lib/firebase_options.dart index 683caab1ce..9fd43a439e 100644 --- a/sample_app/lib/firebase_options.dart +++ b/sample_app/lib/firebase_options.dart @@ -1,8 +1,7 @@ // File generated by FlutterFire CLI. // ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; +import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; /// Default [FirebaseOptions] for use with your Firebase apps. /// @@ -70,10 +69,8 @@ class DefaultFirebaseOptions { projectId: 'stream-chat-internal', databaseURL: 'https://stream-chat-internal.firebaseio.com', storageBucket: 'stream-chat-internal.appspot.com', - androidClientId: - '674907137625-0aa50j6b2i35ef9c52lsbk1v16otl492.apps.googleusercontent.com', - iosClientId: - '674907137625-flarfn9cefu4lermgpbc4b8rm8l15ian.apps.googleusercontent.com', + androidClientId: '674907137625-0aa50j6b2i35ef9c52lsbk1v16otl492.apps.googleusercontent.com', + iosClientId: '674907137625-flarfn9cefu4lermgpbc4b8rm8l15ian.apps.googleusercontent.com', iosBundleId: 'io.getstream.flutter', ); @@ -84,10 +81,8 @@ class DefaultFirebaseOptions { projectId: 'stream-chat-internal', databaseURL: 'https://stream-chat-internal.firebaseio.com', storageBucket: 'stream-chat-internal.appspot.com', - androidClientId: - '674907137625-0aa50j6b2i35ef9c52lsbk1v16otl492.apps.googleusercontent.com', - iosClientId: - '674907137625-p3msks3snq0h22l7ekpqcf0frr0vt8mg.apps.googleusercontent.com', + androidClientId: '674907137625-0aa50j6b2i35ef9c52lsbk1v16otl492.apps.googleusercontent.com', + iosClientId: '674907137625-p3msks3snq0h22l7ekpqcf0frr0vt8mg.apps.googleusercontent.com', iosBundleId: 'io.getstream.streamChatV1', ); } diff --git a/sample_app/lib/pages/advanced_options_page.dart b/sample_app/lib/pages/advanced_options_page.dart index 81ab732c02..5bc5d45227 100644 --- a/sample_app/lib/pages/advanced_options_page.dart +++ b/sample_app/lib/pages/advanced_options_page.dart @@ -132,11 +132,12 @@ class _AdvancedOptionsPageState extends State { centerTitle: true, title: Text( AppLocalizations.of(context).advancedOptions, - style: StreamChatTheme.of(context).textTheme.headlineBold.copyWith( - color: StreamChatTheme.of(context).colorTheme.textHighEmphasis), + style: StreamChatTheme.of( + context, + ).textTheme.headlineBold.copyWith(color: StreamChatTheme.of(context).colorTheme.textHighEmphasis), ), leading: IconButton( - icon: const StreamSvgIcon(icon: StreamSvgIcons.left), + icon: Icon(context.streamIcons.chevronLeft20), color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, onPressed: () { Navigator.pop(context); @@ -164,9 +165,7 @@ class _AdvancedOptionsPageState extends State { validator: (value) { if (value!.isEmpty) { setState(() { - _apiKeyError = AppLocalizations.of(context) - .apiKeyError - .toUpperCase(); + _apiKeyError = AppLocalizations.of(context).apiKeyError.toUpperCase(); }); return _apiKeyError; } @@ -174,9 +173,7 @@ class _AdvancedOptionsPageState extends State { }, style: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, ), decoration: InputDecoration( errorStyle: const TextStyle(height: 0, fontSize: 0), @@ -185,9 +182,7 @@ class _AdvancedOptionsPageState extends State { fontWeight: FontWeight.bold, color: _apiKeyError != null ? StreamChatTheme.of(context).colorTheme.accentError - : StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + : StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), border: UnderlineInputBorder( borderRadius: BorderRadius.circular(8), @@ -214,9 +209,7 @@ class _AdvancedOptionsPageState extends State { validator: (value) { if (value!.isEmpty) { setState(() { - _userIdError = AppLocalizations.of(context) - .userIdError - .toUpperCase(); + _userIdError = AppLocalizations.of(context).userIdError.toUpperCase(); }); return _userIdError; } @@ -224,9 +217,7 @@ class _AdvancedOptionsPageState extends State { }, style: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, ), textInputAction: TextInputAction.next, decoration: InputDecoration( @@ -236,9 +227,7 @@ class _AdvancedOptionsPageState extends State { fontSize: 14, color: _userIdError != null ? StreamChatTheme.of(context).colorTheme.accentError - : StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + : StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), border: UnderlineInputBorder( borderRadius: BorderRadius.circular(8), @@ -264,9 +253,7 @@ class _AdvancedOptionsPageState extends State { validator: (value) { if (value!.isEmpty) { setState(() { - _userTokenError = AppLocalizations.of(context) - .userTokenError - .toUpperCase(); + _userTokenError = AppLocalizations.of(context).userTokenError.toUpperCase(); }); return _userTokenError; } @@ -274,9 +261,7 @@ class _AdvancedOptionsPageState extends State { }, style: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, ), textInputAction: TextInputAction.next, decoration: InputDecoration( @@ -286,9 +271,7 @@ class _AdvancedOptionsPageState extends State { fontSize: 14, color: _userTokenError != null ? StreamChatTheme.of(context).colorTheme.accentError - : StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + : StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), border: UnderlineInputBorder( borderRadius: BorderRadius.circular(8), @@ -309,9 +292,7 @@ class _AdvancedOptionsPageState extends State { labelStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), border: UnderlineInputBorder( borderRadius: BorderRadius.circular(8), @@ -326,14 +307,12 @@ class _AdvancedOptionsPageState extends State { ElevatedButton( style: ButtonStyle( backgroundColor: WidgetStateProperty.all( - Theme.of(context).brightness == Brightness.light - ? StreamChatTheme.of(context) - .colorTheme - .accentPrimary - : Colors.white), + Theme.of(context).brightness == Brightness.light + ? StreamChatTheme.of(context).colorTheme.accentPrimary + : Colors.white, + ), elevation: WidgetStateProperty.all(0), - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(vertical: 16)), + padding: WidgetStateProperty.all(const EdgeInsets.symmetric(vertical: 16)), shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(26), @@ -346,9 +325,7 @@ class _AdvancedOptionsPageState extends State { style: TextStyle( fontSize: 16, color: Theme.of(context).brightness != Brightness.light - ? StreamChatTheme.of(context) - .colorTheme - .accentPrimary + ? StreamChatTheme.of(context).colorTheme.accentPrimary : Colors.white, ), ), diff --git a/sample_app/lib/pages/channel_file_display_screen.dart b/sample_app/lib/pages/channel_file_display_screen.dart index 727dd6bb87..6781abdc17 100644 --- a/sample_app/lib/pages/channel_file_display_screen.dart +++ b/sample_app/lib/pages/channel_file_display_screen.dart @@ -13,8 +13,7 @@ class ChannelFileDisplayScreen extends StatefulWidget { final StreamMessageThemeData messageTheme; @override - State createState() => - _ChannelFileDisplayScreenState(); + State createState() => _ChannelFileDisplayScreenState(); } class _ChannelFileDisplayScreenState extends State { @@ -31,10 +30,7 @@ class _ChannelFileDisplayScreenState extends State { const ['file'], ), sort: [ - const SortOption( - 'created_at', - direction: SortOption.ASC, - ), + const SortOption.asc('created_at'), ], limit: 20, ); @@ -58,88 +54,82 @@ class _ChannelFileDisplayScreenState extends State { ), body: ValueListenableBuilder( valueListenable: controller, - builder: ( - BuildContext context, - PagedValue value, - Widget? child, - ) { - return value.when( - (items, nextPageKey, error) { - if (items.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StreamSvgIcon( - icon: StreamSvgIcons.files, - size: 136, - color: StreamChatTheme.of(context).colorTheme.disabled, - ), - const SizedBox(height: 16), - Text( - AppLocalizations.of(context).noFiles, - style: TextStyle( - fontSize: 14, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, - ), - ), - const SizedBox(height: 8), - Text( - AppLocalizations.of(context).filesAppearHere, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), - ), + builder: + ( + BuildContext context, + PagedValue value, + Widget? child, + ) { + return value.when( + (items, nextPageKey, error) { + if (items.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + context.streamIcons.file32, + size: 136, + color: StreamChatTheme.of(context).colorTheme.disabled, + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of(context).noFiles, + style: TextStyle( + fontSize: 14, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, + ), + ), + const SizedBox(height: 8), + Text( + AppLocalizations.of(context).filesAppearHere, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), + ), + ), + ], ), - ], - ), - ); - } - final media = {}; - - for (final item in items) { - item.message.attachments - .where((e) => e.type == 'file') - .forEach((e) { - media[e] = item.message; - }); - } + ); + } + final media = {}; - return LazyLoadScrollView( - onEndOfPage: () async { - if (nextPageKey != null) { - controller.loadMore(nextPageKey); + for (final item in items) { + item.message.attachments.where((e) => e.type == 'file').forEach((e) { + media[e] = item.message; + }); } + + return LazyLoadScrollView( + onEndOfPage: () async { + if (nextPageKey != null) { + controller.loadMore(nextPageKey); + } + }, + child: ListView.builder( + itemBuilder: (context, position) { + return Padding( + padding: const EdgeInsets.all(1), + child: Padding( + padding: const EdgeInsets.all(8), + child: StreamFileAttachment( + message: media.values.toList()[position], + file: media.keys.toList()[position], + ), + ), + ); + }, + itemCount: media.length, + ), + ); }, - child: ListView.builder( - itemBuilder: (context, position) { - return Padding( - padding: const EdgeInsets.all(1), - child: Padding( - padding: const EdgeInsets.all(8), - child: StreamFileAttachment( - message: media.values.toList()[position], - file: media.keys.toList()[position], - ), - ), - ); - }, - itemCount: media.length, + loading: () => const Center( + child: CircularProgressIndicator(), ), + error: (_) => const Offstage(), ); }, - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (_) => const Offstage(), - ); - }, ), ); } diff --git a/sample_app/lib/pages/channel_list_page.dart b/sample_app/lib/pages/channel_list_page.dart index b756e231f6..6cfd81c18a 100644 --- a/sample_app/lib/pages/channel_list_page.dart +++ b/sample_app/lib/pages/channel_list_page.dart @@ -7,12 +7,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; import 'package:sample_app/pages/draft_list_page.dart'; import 'package:sample_app/pages/reminders_page.dart'; 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/state/init_data.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'; @@ -32,18 +35,18 @@ class _ChannelListPageState extends State { bool _isSelected(int index) => _currentIndex == index; List get _navBarItems { + final icons = context.streamIcons; + return [ BottomNavigationBarItem( icon: Stack( clipBehavior: Clip.none, children: [ - StreamSvgIcon( - icon: StreamSvgIcons.message, - color: _isSelected(0) - ? StreamChatTheme.of(context).colorTheme.textHighEmphasis - : Colors.grey, + Icon( + _isSelected(0) ? icons.messageBubbleFill20 : icons.messageBubble20, + color: _isSelected(0) ? StreamChatTheme.of(context).colorTheme.textHighEmphasis : Colors.grey, ), - PositionedDirectional( + const PositionedDirectional( top: -4, start: 12, child: StreamUnreadIndicator(), @@ -53,11 +56,9 @@ class _ChannelListPageState extends State { label: AppLocalizations.of(context).chats, ), BottomNavigationBarItem( - icon: StreamSvgIcon( - icon: StreamSvgIcons.mentions, - color: _isSelected(1) - ? StreamChatTheme.of(context).colorTheme.textHighEmphasis - : Colors.grey, + icon: Icon( + _isSelected(1) ? icons.mention32 : icons.mention20, + color: _isSelected(1) ? StreamChatTheme.of(context).colorTheme.textHighEmphasis : Colors.grey, ), label: AppLocalizations.of(context).mentions, ), @@ -66,10 +67,8 @@ class _ChannelListPageState extends State { clipBehavior: Clip.none, children: [ Icon( - Icons.message_outlined, - color: _isSelected(2) - ? StreamChatTheme.of(context).colorTheme.textHighEmphasis - : Colors.grey, + _isSelected(2) ? icons.threadFill20 : icons.thread20, + color: _isSelected(2) ? StreamChatTheme.of(context).colorTheme.textHighEmphasis : Colors.grey, ), PositionedDirectional( top: -4, @@ -82,25 +81,25 @@ class _ChannelListPageState extends State { ), BottomNavigationBarItem( icon: Icon( - Icons.edit_note_rounded, - color: _isSelected(3) - ? StreamChatTheme.of(context).colorTheme.textHighEmphasis - : Colors.grey, + _isSelected(3) ? icons.edit32 : icons.edit20, + color: _isSelected(3) ? StreamChatTheme.of(context).colorTheme.textHighEmphasis : Colors.grey, ), label: 'Drafts', ), BottomNavigationBarItem( icon: Icon( - Icons.bookmark_border_rounded, - color: _isSelected(4) - ? StreamChatTheme.of(context).colorTheme.textHighEmphasis - : Colors.grey, + icons.save20, + color: _isSelected(4) ? StreamChatTheme.of(context).colorTheme.textHighEmphasis : Colors.grey, ), label: 'Reminders', ), ]; } + late final _locationService = SharedLocationService( + client: StreamChat.of(context).client, + ); + @override Widget build(BuildContext context) { final user = StreamChat.of(context).currentUser; @@ -110,11 +109,18 @@ class _ChannelListPageState extends State { return Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, appBar: StreamChannelListHeader( + titleBuilder: _currentIndex == 0 + ? null + : (context, status, client) { + return Text( + _navBarItems[_currentIndex].label!, + style: context.streamTextTheme.headingSm, + ); + }, onNewChatButtonTap: () { GoRouter.of(context).pushNamed(Routes.NEW_CHAT.name); }, - preNavigationCallback: () => - FocusScope.of(context).requestFocus(FocusNode()), + preNavigationCallback: () => FocusScope.of(context).requestFocus(FocusNode()), ), drawer: LeftDrawer( user: user, @@ -125,11 +131,9 @@ class _ChannelListPageState extends State { currentIndex: _currentIndex, items: _navBarItems, selectedLabelStyle: StreamChatTheme.of(context).textTheme.footnoteBold, - unselectedLabelStyle: - StreamChatTheme.of(context).textTheme.footnoteBold, + unselectedLabelStyle: StreamChatTheme.of(context).textTheme.footnoteBold, type: BottomNavigationBarType.fixed, - selectedItemColor: - StreamChatTheme.of(context).colorTheme.textHighEmphasis, + selectedItemColor: StreamChatTheme.of(context).colorTheme.textHighEmphasis, unselectedItemColor: Colors.grey, onTap: (index) { setState(() => _currentIndex = index); @@ -152,12 +156,9 @@ class _ChannelListPageState extends State { @override void initState() { + super.initState(); if (!kIsWeb) { - badgeListener = StreamChat.of(context) - .client - .state - .totalUnreadCountStream - .listen((count) { + badgeListener = StreamChat.of(context).client.state.totalUnreadCountStream.listen((count) { if (count > 0) { FlutterAppBadger.updateBadgeCount(count); } else { @@ -165,12 +166,14 @@ class _ChannelListPageState extends State { } }); } - super.initState(); + + _locationService.initialize(); } @override void dispose() { badgeListener?.cancel(); + _locationService.dispose(); super.dispose(); } } @@ -203,10 +206,9 @@ class LeftDrawer extends StatelessWidget { child: Row( children: [ StreamUserAvatar( + size: .lg, user: user, - showOnlineStatus: false, - constraints: - BoxConstraints.tight(const Size.fromRadius(20)), + showOnlineIndicator: false, ), Padding( padding: const EdgeInsets.only(left: 16), @@ -222,12 +224,9 @@ class LeftDrawer extends StatelessWidget { ), ), ListTile( - leading: StreamSvgIcon( - icon: StreamSvgIcons.penWrite, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(.5), + leading: Icon( + context.streamIcons.edit20, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(.5), ), onTap: () { Navigator.of(context).pop(); @@ -241,12 +240,9 @@ class LeftDrawer extends StatelessWidget { ), ), ListTile( - leading: StreamSvgIcon( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(.5), - icon: StreamSvgIcons.contacts, + leading: Icon( + context.streamIcons.users20, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(.5), ), onTap: () { Navigator.of(context).pop(); @@ -259,6 +255,27 @@ class LeftDrawer extends StatelessWidget { ), ), ), + PreferenceBuilder( + preference: context.read().initData!.preferences.getBool( + 'forceRtl', + defaultValue: false, + ), + builder: (context, forceRtl) => SwitchListTile( + secondary: Icon( + Icons.format_textdirection_r_to_l, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(.5), + ), + title: const Text( + 'Force RTL', + style: TextStyle(fontSize: 14.5), + ), + value: forceRtl, + onChanged: (value) async { + final sp = await StreamingSharedPreferences.instance; + sp.setBool('forceRtl', value); + }, + ), + ), Expanded( child: Container( alignment: Alignment.bottomCenter, @@ -276,12 +293,9 @@ class LeftDrawer extends StatelessWidget { router.goNamed(Routes.CHOOSE_USER.name); }, - leading: StreamSvgIcon( - icon: StreamSvgIcons.user, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(.5), + leading: Icon( + context.streamIcons.user20, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(.5), ), title: Text( AppLocalizations.of(context).signOut, @@ -292,9 +306,7 @@ class LeftDrawer extends StatelessWidget { trailing: IconButton( iconSize: 24, icon: const StreamSvgIcon(icon: StreamSvgIcons.moon), - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, onPressed: () async { final theme = Theme.of(context); final sp = await StreamingSharedPreferences.instance; diff --git a/sample_app/lib/pages/channel_media_display_screen.dart b/sample_app/lib/pages/channel_media_display_screen.dart index d7b7a472d1..752e7441a9 100644 --- a/sample_app/lib/pages/channel_media_display_screen.dart +++ b/sample_app/lib/pages/channel_media_display_screen.dart @@ -15,8 +15,7 @@ class ChannelMediaDisplayScreen extends StatefulWidget { final StreamMessageThemeData messageTheme; @override - State createState() => - _ChannelMediaDisplayScreenState(); + State createState() => _ChannelMediaDisplayScreenState(); } class _ChannelMediaDisplayScreenState extends State { @@ -33,10 +32,7 @@ class _ChannelMediaDisplayScreenState extends State { const ['image', 'video'], ), sort: [ - const SortOption( - 'created_at', - direction: SortOption.ASC, - ), + const SortOption.asc('created_at'), ], limit: 20, ); @@ -60,8 +56,7 @@ class _ChannelMediaDisplayScreenState extends State { ), body: ValueListenableBuilder( valueListenable: controller, - builder: (BuildContext context, - PagedValue value, Widget? child) { + builder: (BuildContext context, PagedValue value, Widget? child) { return value.when( (items, nextPageKey, error) { if (items.isEmpty) { @@ -69,8 +64,8 @@ class _ChannelMediaDisplayScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - StreamSvgIcon( - icon: StreamSvgIcons.pictures, + Icon( + context.streamIcons.image32, size: 136, color: StreamChatTheme.of(context).colorTheme.disabled, ), @@ -79,22 +74,16 @@ class _ChannelMediaDisplayScreenState extends State { AppLocalizations.of(context).noMedia, style: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, ), ), const SizedBox(height: 8), Text( - AppLocalizations.of(context) - .photosOrVideosWillAppearHere, + AppLocalizations.of(context).photosOrVideosWillAppearHere, textAlign: TextAlign.center, style: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), ), ), ], @@ -105,25 +94,23 @@ class _ChannelMediaDisplayScreenState extends State { for (final item in value.asSuccess.items) { item.message.attachments - .where((e) => - (e.type == 'image' || e.type == 'video') && - e.ogScrapeUrl == null) + .where((e) => (e.type == 'image' || e.type == 'video') && e.ogScrapeUrl == null) .forEach((e) { - VideoPlayerController? controller; - if (e.type == 'video') { - final cachedController = controllerCache[e.assetUrl]; + VideoPlayerController? controller; + if (e.type == 'video') { + final cachedController = controllerCache[e.assetUrl]; - if (cachedController == null) { - final url = Uri.parse(e.assetUrl!); - controller = VideoPlayerController.networkUrl(url); - controller.initialize(); - controllerCache[e.assetUrl] = controller; - } else { - controller = cachedController; - } - } - media.add(_AssetPackage(e, item.message, controller)); - }); + if (cachedController == null) { + final url = Uri.parse(e.assetUrl!); + controller = VideoPlayerController.networkUrl(url); + controller.initialize(); + controllerCache[e.assetUrl] = controller; + } else { + controller = cachedController; + } + } + media.add(_AssetPackage(e, item.message, controller)); + }); } return LazyLoadScrollView( @@ -133,8 +120,7 @@ class _ChannelMediaDisplayScreenState extends State { } }, child: GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3), itemBuilder: (context, position) { final channel = StreamChannel.of(context).channel; return Padding( @@ -164,10 +150,8 @@ class _ChannelMediaDisplayScreenState extends State { } router.pushNamed( Routes.CHANNEL_PAGE.name, - pathParameters: - Routes.CHANNEL_PAGE.params(channel), - queryParameters: - Routes.CHANNEL_PAGE.queryParams(m), + pathParameters: Routes.CHANNEL_PAGE.params(channel), + queryParameters: Routes.CHANNEL_PAGE.queryParams(m), ); }, ), diff --git a/sample_app/lib/pages/channel_page.dart b/sample_app/lib/pages/channel_page.dart index d1e8e213c8..afa9dc2349 100644 --- a/sample_app/lib/pages/channel_page.dart +++ b/sample_app/lib/pages/channel_page.dart @@ -1,12 +1,12 @@ -// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use, avoid_redundant_argument_values import 'package:collection/collection.dart'; 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/message_info_sheet.dart'; -import 'package:sample_app/widgets/reminder_dialog.dart'; +import 'package:sample_app/widgets/location/location_picker_dialog.dart'; +import 'package:sample_app/widgets/location/location_picker_option.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class ChannelPage extends StatefulWidget { @@ -26,8 +26,7 @@ class ChannelPage extends StatefulWidget { class _ChannelPageState extends State { FocusNode? _focusNode; - final StreamMessageInputController _messageInputController = - StreamMessageInputController(); + final _messageInputController = StreamMessageInputController(); @override void initState() { @@ -48,12 +47,22 @@ class _ChannelPageState extends State { }); } + void _editMessage(Message message) { + _messageInputController.editMessage(message); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + _focusNode!.requestFocus(); + }); + } + @override Widget build(BuildContext context) { final theme = StreamChatTheme.of(context); 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( @@ -92,9 +101,10 @@ class _ChannelPageState extends State { initialScrollIndex: widget.initialScrollIndex, initialAlignment: widget.initialAlignment, highlightInitialMessage: widget.highlightInitialMessage, - //onMessageSwiped: _reply, + onEditMessageTap: _editMessage, + onReplyTap: _reply, + swipeToReply: true, messageFilter: defaultFilter, - messageBuilder: customMessageBuilder, threadBuilder: (_, parentMessage) { return ThreadPage(parent: parentMessage!); }, @@ -125,176 +135,67 @@ class _ChannelPageState extends State { messageInputController: _messageInputController, onQuotedMessageCleared: _messageInputController.clearQuotedMessage, enableVoiceRecording: true, + allowedAttachmentPickerTypes: [ + ...AttachmentPickerType.values, + if (config?.sharedLocations == true && channel.canShareLocation) const LocationPickerType(), + ], + onAttachmentPickerResult: (result) { + return _onCustomAttachmentPickerResult(channel, result); + }, + attachmentPickerOptionsBuilder: (context, defaultOptions) => [ + ...defaultOptions, + TabbedAttachmentPickerOption( + key: 'location-picker', + 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; + + controller.notifyCustomResult( + LocationPicked(location: locationResult), + ); + }, + ), + ), + ], ), ], ), ); } - Widget customMessageBuilder( - BuildContext context, - MessageDetails details, - List messages, - StreamMessageWidget defaultMessageWidget, + bool _onCustomAttachmentPickerResult( + Channel channel, + StreamAttachmentPickerResult result, ) { - final theme = StreamChatTheme.of(context); - final textTheme = theme.textTheme; - final colorTheme = theme.colorTheme; + if (result is LocationPicked) { + _onShareLocationPicked(channel, result.location).ignore(); + return true; // Notify that the result was handled. + } - final message = details.message; - final reminder = message.reminder; - final channelConfig = StreamChannel.of(context).channel.config; - - final customOptions = [ - if (channelConfig?.userMessageReminders == true) ...[ - if (reminder != null) ...[ - StreamMessageAction( - leading: StreamSvgIcon( - icon: StreamSvgIcons.time, - 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(); - }, - ), - StreamMessageAction( - leading: StreamSvgIcon( - icon: StreamSvgIcons.checkAll, - 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(); - }, - ), - ] else ...[ - StreamMessageAction( - leading: StreamSvgIcon( - icon: StreamSvgIcons.time, - 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(); - }, - ), - StreamMessageAction( - leading: Icon( - Icons.bookmark_border, - 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(); - }, - ), - ], - ], - if (channelConfig?.deliveryEvents == true) - StreamMessageAction( - leading: Icon( - Icons.info_outline_rounded, - color: colorTheme.textLowEmphasis, - ), - title: const Text('Message Info'), - onTap: (message) { - Navigator.of(context).pop(); - MessageInfoSheet.show(context: context, message: message); - }, - ), - ]; + return false; // Notify that the result was not handled. + } - return Container( - color: reminder != null ? colorTheme.accentPrimary.withOpacity(.1) : null, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (reminder != null) - Align( - alignment: switch (defaultMessageWidget.reverse) { - true => AlignmentDirectional.centerEnd, - false => AlignmentDirectional.centerStart, - }, - child: Padding( - padding: const EdgeInsetsDirectional.fromSTEB(16, 4, 16, 8), - child: Row( - spacing: 4, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - size: 16, - Icons.bookmark_rounded, - color: colorTheme.accentPrimary, - ), - Text( - 'Saved for later', - style: textTheme.footnote.copyWith( - color: colorTheme.accentPrimary, - ), - ), - ], - ), - ), - ), - defaultMessageWidget.copyWith( - onReplyTap: _reply, - customActions: customOptions, - onShowMessage: (message, channel) => GoRouter.of(context).goNamed( - Routes.CHANNEL_PAGE.name, - pathParameters: Routes.CHANNEL_PAGE.params(channel), - queryParameters: Routes.CHANNEL_PAGE.queryParams(message), - ), - bottomRowBuilderWithDefaultWidget: (_, __, defaultWidget) { - return defaultWidget.copyWith( - deletedBottomRowBuilder: (context, message) { - return const StreamVisibleFootnote(); - }, - ); - }, - ), - // If the message has a reminder, add some space below it. - if (reminder != null) const SizedBox(height: 4), - ], - ), - ); + 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); } bool defaultFilter(Message m) { diff --git a/sample_app/lib/pages/chat_info_screen.dart b/sample_app/lib/pages/chat_info_screen.dart index 6caacbdfc4..726e5feac5 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(), ], ), ); @@ -69,19 +68,14 @@ class _ChatInfoScreenState extends State { Padding( padding: const EdgeInsets.all(16), child: StreamUserAvatar( + size: .xl, user: widget.user!, - constraints: const BoxConstraints.tightFor( - width: 72, - height: 72, - ), - borderRadius: BorderRadius.circular(36), - showOnlineStatus: false, + showOnlineIndicator: false, ), ), Text( widget.user!.name, - style: const TextStyle( - fontSize: 16, fontWeight: FontWeight.bold), + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 7), _buildConnectedTitleState(), @@ -94,11 +88,9 @@ class _ChatInfoScreenState extends State { child: Text( widget.user!.name, style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), - fontSize: 16), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), + fontSize: 16, + ), ), ), onTap: () {}, @@ -123,63 +115,59 @@ class _ChatInfoScreenState extends State { return Column( children: [ StreamBuilder( - stream: StreamChannel.of(context).channel.isMutedStream, - builder: (context, snapshot) { - mutedBool.value = snapshot.data; - - return StreamOptionListTile( - tileColor: StreamChatTheme.of(context).colorTheme.appBg, - title: AppLocalizations.of(context).muteUser, - titleTextStyle: StreamChatTheme.of(context).textTheme.body, - leading: Padding( - padding: const EdgeInsets.symmetric(horizontal: 22), - child: StreamSvgIcon( - icon: StreamSvgIcons.mute, - size: 24, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), - ), + stream: StreamChannel.of(context).channel.isMutedStream, + builder: (context, snapshot) { + mutedBool.value = snapshot.data; + + return StreamOptionListTile( + tileColor: StreamChatTheme.of(context).colorTheme.appBg, + title: AppLocalizations.of(context).muteUser, + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 22), + child: Icon( + context.streamIcons.mute20, + size: 24, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), ), - trailing: snapshot.data == null - ? const CircularProgressIndicator() - : ValueListenableBuilder( - valueListenable: mutedBool, - builder: (context, value, _) { - return CupertinoSwitch( - value: value!, - onChanged: (val) { - mutedBool.value = val; - - if (snapshot.data!) { - channel.channel.unmute(); - } else { - channel.channel.mute(); - } - }, - ); - }), - onTap: () {}, - ); - }), + ), + trailing: snapshot.data == null + ? const CircularProgressIndicator() + : ValueListenableBuilder( + valueListenable: mutedBool, + builder: (context, value, _) { + return CupertinoSwitch( + value: value!, + onChanged: (val) { + mutedBool.value = val; + + if (snapshot.data!) { + channel.channel.unmute(); + } else { + channel.channel.mute(); + } + }, + ); + }, + ), + onTap: () {}, + ); + }, + ), StreamOptionListTile( title: AppLocalizations.of(context).pinnedMessages, tileColor: StreamChatTheme.of(context).colorTheme.appBg, titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 22), - child: StreamSvgIcon( - icon: StreamSvgIcons.pin, + child: Icon( + context.streamIcons.pin20, size: 24, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), ), ), - trailing: StreamSvgIcon( - icon: StreamSvgIcons.right, + trailing: Icon( + context.streamIcons.chevronRight20, color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), onTap: () { @@ -202,17 +190,14 @@ class _ChatInfoScreenState extends State { titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamSvgIcon( - icon: StreamSvgIcons.pictures, + child: Icon( + context.streamIcons.image32, size: 36, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), ), ), - trailing: StreamSvgIcon( - icon: StreamSvgIcons.right, + trailing: Icon( + context.streamIcons.chevronRight20, color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), onTap: () { @@ -237,17 +222,14 @@ class _ChatInfoScreenState extends State { titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 18), - child: StreamSvgIcon( - icon: StreamSvgIcons.files, + child: Icon( + context.streamIcons.file32, size: 32, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), ), ), - trailing: StreamSvgIcon( - icon: StreamSvgIcons.right, + trailing: Icon( + context.streamIcons.chevronRight20, color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), onTap: () { @@ -272,25 +254,23 @@ class _ChatInfoScreenState extends State { titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 22), - child: StreamSvgIcon( - icon: StreamSvgIcons.group, + child: Icon( + context.streamIcons.users20, size: 24, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), ), ), - trailing: StreamSvgIcon( - icon: StreamSvgIcons.right, + trailing: Icon( + context.streamIcons.chevronRight20, color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), onTap: () { Navigator.push( - context, - MaterialPageRoute( - builder: (context) => _SharedGroupsScreen( - StreamChat.of(context).currentUser, widget.user))); + context, + MaterialPageRoute( + builder: (context) => _SharedGroupsScreen(StreamChat.of(context).currentUser, widget.user), + ), + ); }, ), ], @@ -302,12 +282,12 @@ class _ChatInfoScreenState extends State { title: 'Delete Conversation', tileColor: StreamChatTheme.of(context).colorTheme.appBg, titleTextStyle: StreamChatTheme.of(context).textTheme.body.copyWith( - color: StreamChatTheme.of(context).colorTheme.accentError, - ), + color: StreamChatTheme.of(context).colorTheme.accentError, + ), leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 22), - child: StreamSvgIcon( - icon: StreamSvgIcons.delete, + child: Icon( + context.streamIcons.delete20, size: 24, color: StreamChatTheme.of(context).colorTheme.accentError, ), @@ -325,8 +305,8 @@ class _ChatInfoScreenState extends State { okText: AppLocalizations.of(context).delete.toUpperCase(), question: AppLocalizations.of(context).deleteConversationAreYouSure, cancelText: AppLocalizations.of(context).cancel.toUpperCase(), - icon: StreamSvgIcon( - icon: StreamSvgIcons.delete, + icon: Icon( + context.streamIcons.delete20, color: StreamChatTheme.of(context).colorTheme.accentError, ), ); @@ -348,20 +328,12 @@ class _ChatInfoScreenState extends State { if (otherMember.online) { alternativeWidget = Text( AppLocalizations.of(context).online, - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5)), + style: TextStyle(color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5)), ); } else { alternativeWidget = Text( '${AppLocalizations.of(context).lastSeen} ${Jiffy.parseFromDateTime(otherMember.lastActive!).fromNow()}', - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5)), + style: TextStyle(color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5)), ); } } @@ -416,9 +388,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { centerTitle: true, title: Text( AppLocalizations.of(context).sharedGroups, - style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, - fontSize: 16), + style: TextStyle(color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, fontSize: 16), ), leading: const StreamBackButton(), backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, @@ -442,8 +412,8 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - StreamSvgIcon( - icon: StreamSvgIcons.message, + Icon( + context.streamIcons.messageBubble32, size: 136, color: StreamChatTheme.of(context).colorTheme.disabled, ), @@ -452,9 +422,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { AppLocalizations.of(context).noSharedGroups, style: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, ), ), const SizedBox(height: 8), @@ -463,10 +431,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { textAlign: TextAlign.center, style: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), ), ), ], @@ -475,11 +440,11 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { } final channels = snapshot.data! - .where((c) => - c.state!.members.any((m) => - m.userId != widget.mainUser!.id && - m.userId != widget.otherUser!.id) || - !c.isDistinct) + .where( + (c) => + c.state!.members.any((m) => m.userId != widget.mainUser!.id && m.userId != widget.otherUser!.id) || + !c.isDistinct, + ) .toList(); return ListView.builder( @@ -508,8 +473,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { builder: (context, constraints) { String? title; if (extraData['name'] == null) { - final otherMembers = members.where((member) => - member.userId != StreamChat.of(context).currentUser!.id); + final otherMembers = members.where((member) => member.userId != StreamChat.of(context).currentUser!.id); if (otherMembers.isNotEmpty) { final maxWidth = constraints.maxWidth; final maxChars = maxWidth / textStyle.fontSize!; @@ -523,8 +487,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { } } - final exceedingMembers = - otherMembers.length - currentMembers.length; + final exceedingMembers = otherMembers.length - currentMembers.length; title = '${currentMembers.map((e) => e.user!.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; } else { @@ -542,36 +505,31 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { Padding( padding: const EdgeInsets.all(8), child: StreamChannelAvatar( + size: .lg, channel: channel, - constraints: - const BoxConstraints(maxWidth: 40, maxHeight: 40), ), ), Expanded( - child: Text( - title, - style: textStyle, - )), + child: Text( + title, + style: textStyle, + ), + ), Padding( padding: const EdgeInsets.all(8), child: Text( '${channel.memberCount} ${AppLocalizations.of(context).members.toLowerCase()}', style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5)), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), + ), ), - ) + ), ], ), ), Container( height: 1, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(.08), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(.08), ), ], ); diff --git a/sample_app/lib/pages/choose_user_page.dart b/sample_app/lib/pages/choose_user_page.dart index b2aa66391a..d38f20f173 100644 --- a/sample_app/lib/pages/choose_user_page.dart +++ b/sample_app/lib/pages/choose_user_page.dart @@ -76,16 +76,12 @@ class ChooseUserPage extends StatelessWidget { showDialog( barrierDismissible: false, context: context, - barrierColor: StreamChatTheme.of(context) - .colorTheme - .overlay, + barrierColor: StreamChatTheme.of(context).colorTheme.overlay, builder: (context) => Center( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - color: StreamChatTheme.of(context) - .colorTheme - .barsBg, + color: StreamChatTheme.of(context).colorTheme.barsBg, ), height: 100, width: 100, @@ -96,8 +92,7 @@ class ChooseUserPage extends StatelessWidget { ), ); - final client = - context.read().initData!.client; + final client = context.read().initData!.client; final router = GoRouter.of(context); @@ -127,46 +122,32 @@ class ChooseUserPage extends StatelessWidget { router.replaceNamed(Routes.CHANNEL_LIST_PAGE.name); }, leading: StreamUserAvatar( + size: .lg, user: user, - constraints: BoxConstraints.tight( - const Size.fromRadius(20), - ), ), title: Text( user.name, - style: - StreamChatTheme.of(context).textTheme.bodyBold, + style: StreamChatTheme.of(context).textTheme.bodyBold, ), subtitle: Text( AppLocalizations.of(context).streamTestAccount, - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, - ), + style: StreamChatTheme.of(context).textTheme.footnote.copyWith( + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, + ), ), - trailing: StreamSvgIcon( - icon: StreamSvgIcons.arrowRight, - color: StreamChatTheme.of(context) - .colorTheme - .accentPrimary, + trailing: Icon( + context.streamIcons.arrowRight20, + color: StreamChatTheme.of(context).colorTheme.accentPrimary, ), ); }), ListTile( - onTap: () => GoRouter.of(context) - .pushNamed(Routes.ADVANCED_OPTIONS.name), + onTap: () => GoRouter.of(context).pushNamed(Routes.ADVANCED_OPTIONS.name), leading: CircleAvatar( - backgroundColor: - StreamChatTheme.of(context).colorTheme.borders, - child: StreamSvgIcon( - icon: StreamSvgIcons.settings, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, + backgroundColor: StreamChatTheme.of(context).colorTheme.borders, + child: Icon( + Icons.settings, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, ), ), title: Text( @@ -175,14 +156,9 @@ class ChooseUserPage extends StatelessWidget { ), subtitle: Text( AppLocalizations.of(context).customSettings, - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, - ), + style: StreamChatTheme.of(context).textTheme.footnote.copyWith( + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, + ), ), trailing: SvgPicture.asset( 'assets/icon_arrow_right.svg', diff --git a/sample_app/lib/pages/draft_list_page.dart b/sample_app/lib/pages/draft_list_page.dart index ffdda5fec7..77c51df1d7 100644 --- a/sample_app/lib/pages/draft_list_page.dart +++ b/sample_app/lib/pages/draft_list_page.dart @@ -39,9 +39,9 @@ class _DraftListPageState extends State { children: [ CustomSlidableAction( backgroundColor: Colors.red, - child: const StreamSvgIcon( + child: Icon( + context.streamIcons.delete20, size: 24, - icon: StreamSvgIcons.delete, color: Colors.white, ), onPressed: (context) { @@ -71,8 +71,8 @@ class _DraftListPageState extends State { initialMessageId: draft.parentId, child: switch (draft.parentMessage) { final parent? => ThreadPage( - parent: parent.copyWith(draft: draft), - ), + parent: parent.copyWith(draft: draft), + ), _ => const ChannelPage(), }, ); diff --git a/sample_app/lib/pages/group_chat_details_screen.dart b/sample_app/lib/pages/group_chat_details_screen.dart index 5df7ae5075..bf536f8df3 100644 --- a/sample_app/lib/pages/group_chat_details_screen.dart +++ b/sample_app/lib/pages/group_chat_details_screen.dart @@ -19,8 +19,7 @@ class GroupChatDetailsScreen extends StatefulWidget { } class _GroupChatDetailsScreenState extends State { - late final TextEditingController _groupNameController = - TextEditingController()..addListener(_groupNameListener); + late final TextEditingController _groupNameController = TextEditingController()..addListener(_groupNameListener); bool _isGroupNameEmpty = true; @@ -74,9 +73,7 @@ class _GroupChatDetailsScreenState extends State { AppLocalizations.of(context).name.toUpperCase(), style: TextStyle( fontSize: 12, - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), ), const SizedBox(width: 16), @@ -91,13 +88,10 @@ class _GroupChatDetailsScreenState extends State { errorBorder: InputBorder.none, disabledBorder: InputBorder.none, contentPadding: EdgeInsets.zero, - hintText: - AppLocalizations.of(context).chooseAGroupChatName, + hintText: AppLocalizations.of(context).chooseAGroupChatName, hintStyle: TextStyle( fontSize: 14, - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), ), ), @@ -114,7 +108,7 @@ class _GroupChatDetailsScreenState extends State { color: _isGroupNameEmpty ? StreamChatTheme.of(context).colorTheme.textLowEmphasis : StreamChatTheme.of(context).colorTheme.accentPrimary, - icon: const StreamSvgIcon(icon: StreamSvgIcons.check), + icon: Icon(context.streamIcons.checkmark20), onPressed: _isGroupNameEmpty ? null : () async { @@ -122,16 +116,17 @@ class _GroupChatDetailsScreenState extends State { final groupName = _groupNameController.text; final client = StreamChat.of(context).client; final router = GoRouter.of(context); - final channel = client.channel('messaging', - id: const Uuid().v4(), - extraData: { - 'members': [ - client.state.currentUser!.id, - ...widget.groupChatState.users - .map((e) => e.id), - ], - 'name': groupName, - }); + final channel = client.channel( + 'messaging', + id: const Uuid().v4(), + extraData: { + 'members': [ + client.state.currentUser!.id, + ...widget.groupChatState.users.map((e) => e.id), + ], + 'name': groupName, + }, + ); await channel.watch(); router.goNamed( Routes.CHANNEL_PAGE.name, @@ -172,8 +167,7 @@ class _GroupChatDetailsScreenState extends State { Container( width: double.maxFinite, decoration: BoxDecoration( - gradient: - StreamChatTheme.of(context).colorTheme.bgGradient, + gradient: StreamChatTheme.of(context).colorTheme.bgGradient, ), child: Padding( padding: const EdgeInsets.symmetric( @@ -183,80 +177,67 @@ class _GroupChatDetailsScreenState extends State { child: Text( '$_totalUsers ${_totalUsers > 1 ? AppLocalizations.of(context).members : AppLocalizations.of(context).member}', style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), ), ), ), AnimatedBuilder( - animation: widget.groupChatState, - builder: (context, child) { - return Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onPanDown: (_) => FocusScope.of(context).unfocus(), - child: ListView.separated( - itemCount: widget.groupChatState.users.length + 1, - separatorBuilder: (_, __) => Container( - height: 1, - color: StreamChatTheme.of(context) - .colorTheme - .borders, - ), - itemBuilder: (_, index) { - if (index == - widget.groupChatState.users.length) { - return Container( - height: 1, - color: StreamChatTheme.of(context) - .colorTheme - .borders, - ); - } - final user = widget.groupChatState.users - .elementAt(index); - return ListTile( - key: ObjectKey(user), - leading: StreamUserAvatar( - user: user, - constraints: const BoxConstraints.tightFor( - width: 40, - height: 40, - ), - ), - title: Text( - user.name, - style: const TextStyle( - fontWeight: FontWeight.bold), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - trailing: IconButton( - icon: Icon( - Icons.clear_rounded, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, - ), - padding: EdgeInsets.zero, - splashRadius: 24, - onPressed: () { - widget.groupChatState.removeUser(user); - if (widget.groupChatState.users.isEmpty) { - GoRouter.of(context).pop(); - } - }, - ), - ); - }, + animation: widget.groupChatState, + builder: (context, child) { + return Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onPanDown: (_) => FocusScope.of(context).unfocus(), + child: ListView.separated( + itemCount: widget.groupChatState.users.length + 1, + separatorBuilder: (_, __) => Container( + height: 1, + color: StreamChatTheme.of(context).colorTheme.borders, ), + itemBuilder: (_, index) { + if (index == widget.groupChatState.users.length) { + return Container( + height: 1, + color: StreamChatTheme.of(context).colorTheme.borders, + ); + } + final user = widget.groupChatState.users.elementAt(index); + return ListTile( + key: ObjectKey(user), + leading: StreamUserAvatar( + size: .lg, + user: user, + ), + title: Text( + user.name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + trailing: IconButton( + icon: Icon( + Icons.clear_rounded, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, + ), + padding: EdgeInsets.zero, + splashRadius: 24, + onPressed: () { + widget.groupChatState.removeUser(user); + if (widget.groupChatState.users.isEmpty) { + GoRouter.of(context).pop(); + } + }, + ), + ); + }, ), - ); - }), + ), + ); + }, + ), ], ), ); @@ -271,10 +252,11 @@ class _GroupChatDetailsScreenState extends State { backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, context: context, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - )), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), builder: (context) { return Column( mainAxisSize: MainAxisSize.min, @@ -282,8 +264,8 @@ class _GroupChatDetailsScreenState extends State { const SizedBox( height: 26, ), - StreamSvgIcon( - icon: StreamSvgIcons.error, + Icon( + context.streamIcons.exclamationCircleFill20, color: StreamChatTheme.of(context).colorTheme.accentError, size: 24, ), @@ -302,10 +284,7 @@ class _GroupChatDetailsScreenState extends State { height: 36, ), Container( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(.08), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(.08), height: 1, ), Row( @@ -315,13 +294,9 @@ class _GroupChatDetailsScreenState extends State { onPressed: GoRouter.of(context).pop, child: Text( AppLocalizations.of(context).ok, - style: StreamChatTheme.of(context) - .textTheme - .bodyBold - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .accentPrimary), + style: StreamChatTheme.of( + context, + ).textTheme.bodyBold.copyWith(color: StreamChatTheme.of(context).colorTheme.accentPrimary), ), ), ], diff --git a/sample_app/lib/pages/group_info_screen.dart b/sample_app/lib/pages/group_info_screen.dart index a3ffef7d48..31364ebd55 100644 --- a/sample_app/lib/pages/group_info_screen.dart +++ b/sample_app/lib/pages/group_info_screen.dart @@ -25,13 +25,11 @@ class GroupInfoScreen extends StatefulWidget { } class _GroupInfoScreenState extends State { - late final TextEditingController _nameController = - TextEditingController.fromValue( + late final TextEditingController _nameController = TextEditingController.fromValue( TextEditingValue(text: (channel.extraData['name'] as String?) ?? ''), ); - late final TextEditingController _searchController = TextEditingController() - ..addListener(_userNameListener); + late final TextEditingController _searchController = TextEditingController()..addListener(_userNameListener); String _userNameQuery = ''; @@ -62,13 +60,10 @@ class _GroupInfoScreenState extends State { _userNameQuery = _searchController.text; _userListController.filter = Filter.and( [ - if (_searchController.text.isNotEmpty) - Filter.autoComplete('name', _userNameQuery), + if (_searchController.text.isNotEmpty) Filter.autoComplete('name', _userNameQuery), Filter.notIn('id', [ StreamChat.of(context).currentUser!.id, - ...channel.state!.members - .map((e) => e.userId) - .whereType(), + ...channel.state!.members.map((e) => e.userId).whereType(), ]), ], ); @@ -94,21 +89,15 @@ class _GroupInfoScreenState extends State { limit: 25, filter: Filter.and( [ - if (_searchController.text.isNotEmpty) - Filter.autoComplete('name', _userNameQuery), + if (_searchController.text.isNotEmpty) Filter.autoComplete('name', _userNameQuery), Filter.notIn('id', [ StreamChat.of(context).currentUser!.id, - ...channel.state!.members - .map((e) => e.userId) - .whereType(), + ...channel.state!.members.map((e) => e.userId).whereType(), ]), ], ), sort: [ - const SortOption( - 'name', - direction: 1, - ), + const SortOption.asc(UserSortKey.name), ], ); super.didChangeDependencies(); @@ -125,109 +114,100 @@ class _GroupInfoScreenState extends State { @override Widget build(BuildContext context) { return StreamBuilder>( - stream: channel.state!.membersStream, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return ColoredBox( - color: StreamChatTheme.of(context).colorTheme.disabled, - child: const Center(child: CircularProgressIndicator()), - ); - } + stream: channel.state!.membersStream, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return ColoredBox( + color: StreamChatTheme.of(context).colorTheme.disabled, + child: const Center(child: CircularProgressIndicator()), + ); + } - return Scaffold( - backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, - appBar: AppBar( - elevation: 1, - toolbarHeight: 56, - backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, - leading: const StreamBackButton(), - title: Column( - children: [ - StreamBuilder( - stream: channel.state?.channelStateStream, - builder: (context, state) { - if (!state.hasData) { - return Text( - AppLocalizations.of(context).loading, - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, - fontSize: 16, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - } + return Scaffold( + backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, + appBar: AppBar( + elevation: 1, + toolbarHeight: 56, + backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg, + leading: const StreamBackButton(), + title: Column( + children: [ + StreamBuilder( + stream: channel.state?.channelStateStream, + builder: (context, state) { + if (!state.hasData) { + return Text( + AppLocalizations.of(context).loading, + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, + fontSize: 16, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } - return Text( - _getChannelName( - 2 * MediaQuery.of(context).size.width / 3, - members: snapshot.data, - extraData: state.data!.channel!.extraData, - maxFontSize: 16, - )!, - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, - fontSize: 16, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); - }), - const SizedBox( - height: 3, - ), - Text( - '${channel.memberCount} ${AppLocalizations.of(context).members}, ${snapshot.data?.where((e) => e.user!.online).length ?? 0} ${AppLocalizations.of(context).online}', - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), - fontSize: 12, - ), - ), - ], - ), - centerTitle: true, - actions: [ - if (channel.ownCapabilities - .contains(PermissionType.updateChannelMembers)) - StreamNeumorphicButton( - child: InkWell( - onTap: () { - _buildAddUserModal(context); - }, - child: Padding( - padding: const EdgeInsets.all(8), - child: StreamSvgIcon( - icon: StreamSvgIcons.userAdd, - color: StreamChatTheme.of(context) - .colorTheme - .accentPrimary), + return Text( + _getChannelName( + 2 * MediaQuery.of(context).size.width / 3, + members: snapshot.data, + extraData: state.data!.channel!.extraData, + maxFontSize: 16, + )!, + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, + fontSize: 16, ), - ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + }, + ), + const SizedBox( + height: 3, + ), + Text( + '${channel.memberCount} ${AppLocalizations.of(context).members}, ${snapshot.data?.where((e) => e.user!.online).length ?? 0} ${AppLocalizations.of(context).online}', + style: TextStyle( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), + fontSize: 12, ), - ], - ), - body: ListView( - children: [ - _buildMembers(snapshot.data!), - Container( - height: 8, - color: StreamChatTheme.of(context).colorTheme.disabled, ), - if (channel.ownCapabilities - .contains(PermissionType.updateChannel)) - _buildNameTile(), - _buildOptionListTiles(), ], ), - ); - }); + centerTitle: true, + actions: [ + if (channel.canUpdateChannelMembers) + StreamNeumorphicButton( + child: InkWell( + onTap: () { + _buildAddUserModal(context); + }, + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + context.streamIcons.userAdd20, + color: StreamChatTheme.of(context).colorTheme.accentPrimary, + ), + ), + ), + ), + ], + ), + body: ListView( + children: [ + _buildMembers(snapshot.data!), + Container( + height: 8, + color: StreamChatTheme.of(context).colorTheme.disabled, + ), + if (channel.canUpdateChannel) _buildNameTile(), + _buildOptionListTiles(), + ], + ), + ); + }, + ); } Widget _buildMembers(List members) { @@ -260,8 +240,7 @@ class _GroupInfoScreenState extends State { final userMember = groupMembers.firstWhereOrNull( (e) => e.user!.id == StreamChat.of(context).currentUser!.id, ); - _showUserInfoModal( - member.user, userMember?.userId == channel.createdBy?.id); + _showUserInfoModal(member.user, userMember?.userId == channel.createdBy?.id); }, child: SizedBox( height: 65, @@ -275,11 +254,8 @@ class _GroupInfoScreenState extends State { vertical: 12, ), child: StreamUserAvatar( + size: .lg, user: member.user!, - constraints: const BoxConstraints.tightFor( - height: 40, - width: 40, - ), ), ), Expanded( @@ -289,8 +265,7 @@ class _GroupInfoScreenState extends State { children: [ Text( member.user!.name, - style: const TextStyle( - fontWeight: FontWeight.bold), + style: const TextStyle(fontWeight: FontWeight.bold), ), const SizedBox( height: 1, @@ -298,10 +273,8 @@ class _GroupInfoScreenState extends State { Text( _getLastSeen(member.user!), style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5)), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), + ), ), ], ), @@ -309,14 +282,10 @@ class _GroupInfoScreenState extends State { Padding( padding: const EdgeInsets.all(8), child: Text( - member.userId == channel.createdBy?.id - ? AppLocalizations.of(context).owner - : '', + member.userId == channel.createdBy?.id ? AppLocalizations.of(context).owner : '', style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5)), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), + ), ), ), ], @@ -349,13 +318,10 @@ class _GroupInfoScreenState extends State { child: Row( children: [ Padding( - padding: const EdgeInsets.symmetric( - horizontal: 21, vertical: 12), - child: StreamSvgIcon( - icon: StreamSvgIcons.down, - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + padding: const EdgeInsets.symmetric(horizontal: 21, vertical: 12), + child: Icon( + context.streamIcons.chevronDown20, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), ), Expanded( @@ -365,10 +331,7 @@ class _GroupInfoScreenState extends State { children: [ Text( '${members.length - groupMembersLength} ${AppLocalizations.of(context).more}', - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis), + style: TextStyle(color: StreamChatTheme.of(context).colorTheme.textLowEmphasis), ), ], ), @@ -404,10 +367,8 @@ class _GroupInfoScreenState extends State { child: Text( AppLocalizations.of(context).name.toUpperCase(), style: StreamChatTheme.of(context).textTheme.footnote.copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5)), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), + ), ), ), const SizedBox( @@ -415,22 +376,16 @@ class _GroupInfoScreenState extends State { ), Expanded( child: TextField( - enabled: channel.ownCapabilities - .contains(PermissionType.updateChannel), + enabled: channel.canUpdateChannel, focusNode: _focusNode, controller: _nameController, - cursorColor: - StreamChatTheme.of(context).colorTheme.textHighEmphasis, + cursorColor: StreamChatTheme.of(context).colorTheme.textHighEmphasis, decoration: InputDecoration.collapsed( - hintText: AppLocalizations.of(context).addAGroupName, - hintStyle: StreamChatTheme.of(context) - .textTheme - .bodyBold - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5))), + hintText: AppLocalizations.of(context).addAGroupName, + hintStyle: StreamChatTheme.of(context).textTheme.bodyBold.copyWith( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), + ), + ), style: const TextStyle( fontWeight: FontWeight.bold, height: 0.82, @@ -442,7 +397,7 @@ class _GroupInfoScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ IconButton( - icon: const StreamSvgIcon(icon: StreamSvgIcons.closeSmall), + icon: Icon(context.streamIcons.xmark16), onPressed: () { setState(() { _nameController.text = _getChannelName( @@ -457,7 +412,7 @@ class _GroupInfoScreenState extends State { ), IconButton( color: StreamChatTheme.of(context).colorTheme.accentPrimary, - icon: const StreamSvgIcon(icon: StreamSvgIcons.check), + icon: Icon(context.streamIcons.checkmark20), onPressed: () { try { channel.update({ @@ -482,10 +437,10 @@ 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, + icon: context.streamIcons.mute20, channelStream: channel.isMutedStream, localNotifier: mutedBool, onTurnOff: channel.unmute, @@ -493,7 +448,7 @@ class _GroupInfoScreenState extends State { ), _GroupInfoToggle( title: AppLocalizations.of(context).pinGroup, - icon: StreamSvgIcons.pin, + icon: context.streamIcons.pin20, channelStream: channel.isPinnedStream, localNotifier: isPinned, onTurnOff: channel.unpin, @@ -501,7 +456,7 @@ class _GroupInfoScreenState extends State { ), _GroupInfoToggle( title: AppLocalizations.of(context).archiveGroup, - icon: StreamSvgIcons.save, + icon: context.streamIcons.save20, channelStream: channel.isArchivedStream, localNotifier: isArchived, onTurnOff: channel.unarchive, @@ -509,7 +464,7 @@ class _GroupInfoScreenState extends State { ), _GroupInfoListTile( title: AppLocalizations.of(context).pinnedMessages, - icon: StreamSvgIcons.pin, + icon: context.streamIcons.pin20, iconSize: 24, iconPadding: 16, onTap: () { @@ -528,7 +483,7 @@ class _GroupInfoScreenState extends State { ), _GroupInfoListTile( title: AppLocalizations.of(context).photosAndVideos, - icon: StreamSvgIcons.pictures, + icon: context.streamIcons.image32, iconSize: 32, iconPadding: 12, onTap: () { @@ -549,7 +504,7 @@ class _GroupInfoScreenState extends State { ), _GroupInfoListTile( title: AppLocalizations.of(context).files, - icon: StreamSvgIcons.files, + icon: context.streamIcons.file32, iconSize: 32, iconPadding: 12, onTap: () { @@ -568,8 +523,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, @@ -577,13 +531,10 @@ class _GroupInfoScreenState extends State { titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamSvgIcon( - icon: StreamSvgIcons.userRemove, + child: Icon( + context.streamIcons.userRemove20, size: 24, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), ), ), trailing: const SizedBox( @@ -598,11 +549,10 @@ class _GroupInfoScreenState extends State { context, title: AppLocalizations.of(context).leaveConversation, okText: AppLocalizations.of(context).leave.toUpperCase(), - question: - AppLocalizations.of(context).leaveConversationAreYouSure, + question: AppLocalizations.of(context).leaveConversationAreYouSure, cancelText: AppLocalizations.of(context).cancel.toUpperCase(), - icon: StreamSvgIcon( - icon: StreamSvgIcons.userRemove, + icon: Icon( + context.streamIcons.userRemove20, color: StreamChatTheme.of(context).colorTheme.accentError, ), ); @@ -663,16 +613,13 @@ class _GroupInfoScreenState extends State { children: [ Padding( padding: const EdgeInsets.all(24), - child: StreamSvgIcon( - icon: StreamSvgIcons.search, + child: Icon( + context.streamIcons.search32, size: 96, - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), ), - Text(AppLocalizations.of(context) - .noUserMatchesTheseKeywords), + Text(AppLocalizations.of(context).noUserMatchesTheseKeywords), ], ), ), @@ -712,8 +659,8 @@ class _GroupInfoScreenState extends State { color: theme.colorTheme.textLowEmphasis, ), prefixIconConstraints: BoxConstraints.tight(const Size(40, 24)), - prefixIcon: StreamSvgIcon( - icon: StreamSvgIcons.search, + prefixIcon: Icon( + context.streamIcons.search20, color: theme.colorTheme.textHighEmphasis, size: 24, ), @@ -724,20 +671,21 @@ class _GroupInfoScreenState extends State { ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - borderSide: BorderSide( - color: theme.colorTheme.borders, - )), + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide( + color: theme.colorTheme.borders, + ), + ), contentPadding: EdgeInsets.zero, ), ), ), ), IconButton( - icon: const StreamSvgIcon(icon: StreamSvgIcons.closeSmall), + icon: Icon(context.streamIcons.xmark16), color: theme.colorTheme.textHighEmphasis, onPressed: () => Navigator.pop(context), - ) + ), ], ); } @@ -779,36 +727,33 @@ class _GroupInfoScreenState extends State { child: Padding( padding: const EdgeInsets.all(16), child: StreamUserAvatar( + size: .xl, user: user, - constraints: const BoxConstraints.tightFor( - height: 64, - width: 64, - ), - borderRadius: BorderRadius.circular(32), ), ), ), if (StreamChat.of(context).currentUser!.id != user.id) _buildModalListTile( context, - StreamSvgIcon( - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + Icon( + context.streamIcons.user20, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, size: 24, - icon: StreamSvgIcons.user, ), AppLocalizations.of(context).viewInfo, () async { final client = StreamChat.of(context).client; final router = GoRouter.of(context); - final c = client.channel('messaging', extraData: { - 'members': [ - user.id, - StreamChat.of(context).currentUser!.id, - ], - }); + final c = client.channel( + 'messaging', + extraData: { + 'members': [ + user.id, + StreamChat.of(context).currentUser!.id, + ], + }, + ); await c.watch(); @@ -822,24 +767,25 @@ class _GroupInfoScreenState extends State { if (StreamChat.of(context).currentUser!.id != user.id) _buildModalListTile( context, - StreamSvgIcon( - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + Icon( + context.streamIcons.messageBubble20, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, size: 24, - icon: StreamSvgIcons.message, ), AppLocalizations.of(context).message, () async { final client = StreamChat.of(context).client; final router = GoRouter.of(context); - final c = client.channel('messaging', extraData: { - 'members': [ - user.id, - StreamChat.of(context).currentUser!.id, - ], - }); + final c = client.channel( + 'messaging', + extraData: { + 'members': [ + user.id, + StreamChat.of(context).currentUser!.id, + ], + }, + ); await c.watch(); @@ -849,46 +795,38 @@ class _GroupInfoScreenState extends State { ); }, ), - if (!channel.isDistinct && - StreamChat.of(context).currentUser!.id != user.id && - isUserAdmin) + if (!channel.isDistinct && StreamChat.of(context).currentUser!.id != user.id && isUserAdmin) _buildModalListTile( - context, - StreamSvgIcon( - color: StreamChatTheme.of(context) - .colorTheme - .accentError, - size: 24, - icon: StreamSvgIcons.userRemove, - ), - AppLocalizations.of(context).removeFromGroup, () async { - final router = GoRouter.of(context); - final res = await showConfirmationBottomSheet( - context, - title: AppLocalizations.of(context).removeMember, - okText: - AppLocalizations.of(context).remove.toUpperCase(), - question: - AppLocalizations.of(context).removeMemberAreYouSure, - cancelText: - AppLocalizations.of(context).cancel.toUpperCase(), - ); + context, + Icon( + context.streamIcons.userRemove20, + color: StreamChatTheme.of(context).colorTheme.accentError, + size: 24, + ), + AppLocalizations.of(context).removeFromGroup, + () async { + final router = GoRouter.of(context); + final res = await showConfirmationBottomSheet( + context, + title: AppLocalizations.of(context).removeMember, + okText: AppLocalizations.of(context).remove.toUpperCase(), + question: AppLocalizations.of(context).removeMemberAreYouSure, + cancelText: AppLocalizations.of(context).cancel.toUpperCase(), + ); - if (res == true) { - await channel.removeMembers([user.id]); - } - router.pop(); - }, - color: - StreamChatTheme.of(context).colorTheme.accentError), + if (res == true) { + await channel.removeMembers([user.id]); + } + router.pop(); + }, + color: StreamChatTheme.of(context).colorTheme.accentError, + ), _buildModalListTile( context, - StreamSvgIcon( - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + Icon( + context.streamIcons.xmark16, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, size: 24, - icon: StreamSvgIcons.closeSmall, ), AppLocalizations.of(context).cancel, () { @@ -919,20 +857,12 @@ class _GroupInfoScreenState extends State { if (otherMember.online) { alternativeWidget = Text( AppLocalizations.of(context).online, - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5)), + style: TextStyle(color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5)), ); } else { alternativeWidget = Text( '${AppLocalizations.of(context).lastSeen} ${Jiffy.parseFromDateTime(otherMember.lastActive!).fromNow()}', - style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5)), + style: TextStyle(color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5)), ); } } @@ -940,9 +870,7 @@ class _GroupInfoScreenState extends State { return alternativeWidget; } - Widget _buildModalListTile( - BuildContext context, Widget leading, String title, VoidCallback onTap, - {Color? color}) { + Widget _buildModalListTile(BuildContext context, Widget leading, String title, VoidCallback onTap, {Color? color}) { color ??= StreamChatTheme.of(context).colorTheme.textHighEmphasis; return Material( @@ -966,10 +894,9 @@ class _GroupInfoScreenState extends State { Expanded( child: Text( title, - style: - TextStyle(color: color, fontWeight: FontWeight.bold), + style: TextStyle(color: color, fontWeight: FontWeight.bold), ), - ) + ), ], ), ), @@ -988,8 +915,7 @@ class _GroupInfoScreenState extends State { String? title; final client = StreamChat.of(context); if (extraData['name'] == null) { - final otherMembers = - members!.where((member) => member.user!.id != client.currentUser!.id); + final otherMembers = members!.where((member) => member.user!.id != client.currentUser!.id); if (otherMembers.isNotEmpty) { final maxWidth = width; final maxChars = maxWidth / maxFontSize!; @@ -1039,7 +965,7 @@ class _GroupInfoToggle extends StatelessWidget { }); final String title; - final StreamSvgIconData icon; + final IconData icon; final Stream channelStream; final ValueNotifier localNotifier; final VoidCallback onTurnOff; @@ -1048,46 +974,45 @@ class _GroupInfoToggle extends StatelessWidget { @override Widget build(BuildContext context) { return StreamBuilder( - stream: channelStream, - builder: (context, snapshot) { - localNotifier.value = snapshot.data; + stream: channelStream, + builder: (context, snapshot) { + localNotifier.value = snapshot.data; - return StreamOptionListTile( - tileColor: StreamChatTheme.of(context).colorTheme.appBg, - separatorColor: StreamChatTheme.of(context).colorTheme.disabled, - title: title, - titleTextStyle: StreamChatTheme.of(context).textTheme.body, - leading: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: StreamSvgIcon( - icon: icon, - size: 24, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), - ), + return StreamOptionListTile( + tileColor: StreamChatTheme.of(context).colorTheme.appBg, + separatorColor: StreamChatTheme.of(context).colorTheme.disabled, + title: title, + titleTextStyle: StreamChatTheme.of(context).textTheme.body, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Icon( + icon, + size: 24, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), ), - trailing: snapshot.data == null - ? const CircularProgressIndicator() - : ValueListenableBuilder( - valueListenable: localNotifier, - builder: (context, value, _) { - return CupertinoSwitch( - value: value!, - onChanged: (val) { - localNotifier.value = val; - if (snapshot.data!) { - onTurnOff(); - } else { - onTurnOn(); - } - }, - ); - }), - onTap: () {}, - ); - }); + ), + trailing: snapshot.data == null + ? const CircularProgressIndicator() + : ValueListenableBuilder( + valueListenable: localNotifier, + builder: (context, value, _) { + return CupertinoSwitch( + value: value!, + onChanged: (val) { + localNotifier.value = val; + if (snapshot.data!) { + onTurnOff(); + } else { + onTurnOn(); + } + }, + ); + }, + ), + onTap: () {}, + ); + }, + ); } } @@ -1101,7 +1026,7 @@ class _GroupInfoListTile extends StatelessWidget { }); final String title; - final StreamSvgIconData icon; + final IconData icon; final double iconSize; final double iconPadding; final VoidCallback onTap; @@ -1114,17 +1039,14 @@ class _GroupInfoListTile extends StatelessWidget { titleTextStyle: StreamChatTheme.of(context).textTheme.body, leading: Padding( padding: EdgeInsets.symmetric(horizontal: iconPadding), - child: StreamSvgIcon( - icon: icon, + child: Icon( + icon, size: iconSize, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), ), ), - trailing: StreamSvgIcon( - icon: StreamSvgIcons.right, + trailing: Icon( + context.streamIcons.chevronRight20, color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), onTap: onTap, diff --git a/sample_app/lib/pages/new_chat_screen.dart b/sample_app/lib/pages/new_chat_screen.dart index dfa5daaaf0..5924948728 100644 --- a/sample_app/lib/pages/new_chat_screen.dart +++ b/sample_app/lib/pages/new_chat_screen.dart @@ -17,8 +17,7 @@ class NewChatScreen extends StatefulWidget { } class _NewChatScreenState extends State { - final _chipInputTextFieldStateKey = - GlobalKey>(); + final _chipInputTextFieldStateKey = GlobalKey>(); late TextEditingController _controller; @@ -29,15 +28,11 @@ class _NewChatScreenState extends State { Filter.notEqual('id', StreamChat.of(context).currentUser!.id), ]), sort: [ - const SortOption( - 'name', - direction: 1, - ), + const SortOption.asc(UserSortKey.name), ], ); - ChipInputTextFieldState? get _chipInputTextFieldState => - _chipInputTextFieldStateKey.currentState; + ChipInputTextFieldState? get _chipInputTextFieldState => _chipInputTextFieldStateKey.currentState; String _userNameQuery = ''; @@ -64,8 +59,7 @@ class _NewChatScreenState extends State { }); userListController.filter = Filter.and([ - if (_userNameQuery.isNotEmpty) - Filter.autoComplete('name', _userNameQuery), + if (_userNameQuery.isNotEmpty) Filter.autoComplete('name', _userNameQuery), Filter.notEqual('id', StreamChat.of(context).currentUser!.id), ]); userListController.doInitialLoad(); @@ -94,13 +88,15 @@ class _NewChatScreenState extends State { final res = await chatState.client.queryChannelsOnline( state: false, watch: false, - filter: Filter.raw(value: { - 'members': [ - ..._selectedUsers.map((e) => e.id), - chatState.currentUser!.id, - ], - 'distinct': true, - }), + filter: Filter.raw( + value: { + 'members': [ + ..._selectedUsers.map((e) => e.id), + chatState.currentUser!.id, + ], + 'distinct': true, + }, + ), messageLimit: 0, paginationParams: const PaginationParams( limit: 1, @@ -151,8 +147,9 @@ class _NewChatScreenState extends State { leading: const StreamBackButton(), title: Text( AppLocalizations.of(context).newChat, - style: StreamChatTheme.of(context).textTheme.headlineBold.copyWith( - color: StreamChatTheme.of(context).colorTheme.textHighEmphasis), + style: StreamChatTheme.of( + context, + ).textTheme.headlineBold.copyWith(color: StreamChatTheme.of(context).colorTheme.textHighEmphasis), ), centerTitle: true, ), @@ -200,9 +197,7 @@ class _NewChatScreenState extends State { children: [ Container( decoration: BoxDecoration( - color: StreamChatTheme.of(context) - .colorTheme - .disabled, + color: StreamChatTheme.of(context).colorTheme.disabled, borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.only(left: 24), @@ -212,30 +207,23 @@ class _NewChatScreenState extends State { user.name, maxLines: 1, style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, ), ), ), ), Container( foregroundDecoration: BoxDecoration( - color: StreamChatTheme.of(context) - .colorTheme - .overlay, + color: StreamChatTheme.of(context).colorTheme.overlay, shape: BoxShape.circle, ), child: StreamUserAvatar( - showOnlineStatus: false, + size: .sm, user: user, - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), + showOnlineIndicator: false, ), ), - const StreamSvgIcon(icon: StreamSvgIcons.close), + Icon(context.streamIcons.xmark20), ], ), ); @@ -260,21 +248,17 @@ class _NewChatScreenState extends State { children: [ StreamNeumorphicButton( child: Center( - child: StreamSvgIcon( - color: StreamChatTheme.of(context) - .colorTheme - .accentPrimary, + child: Icon( + context.streamIcons.users20, + color: StreamChatTheme.of(context).colorTheme.accentPrimary, size: 24, - icon: StreamSvgIcons.contacts, ), ), ), const SizedBox(width: 8), Text( AppLocalizations.of(context).createAGroup, - style: StreamChatTheme.of(context) - .textTheme - .bodyBold, + style: StreamChatTheme.of(context).textTheme.bodyBold, ), ], ), @@ -284,8 +268,7 @@ class _NewChatScreenState extends State { Container( width: double.maxFinite, decoration: BoxDecoration( - gradient: - StreamChatTheme.of(context).colorTheme.bgGradient, + gradient: StreamChatTheme.of(context).colorTheme.bgGradient, ), child: Padding( padding: const EdgeInsets.symmetric( @@ -293,17 +276,13 @@ class _NewChatScreenState extends State { horizontal: 8, ), child: Text( - _isSearchActive - ? '${AppLocalizations.of(context).matchesFor} "$_userNameQuery"' - : AppLocalizations.of(context).onThePlatorm, - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(.5))), + _isSearchActive + ? '${AppLocalizations.of(context).matchesFor} "$_userNameQuery"' + : AppLocalizations.of(context).onThePlatorm, + style: StreamChatTheme.of(context).textTheme.footnote.copyWith( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(.5), + ), + ), ), ), Expanded( @@ -323,52 +302,44 @@ class _NewChatScreenState extends State { _chipInputTextFieldState!.removeItem(user); } }, - itemBuilder: ( - context, - users, - index, - defaultWidget, - ) { - return defaultWidget.copyWith( - selected: - _selectedUsers.contains(users[index]), - ); - }, + itemBuilder: + ( + context, + users, + index, + defaultWidget, + ) { + return defaultWidget.copyWith( + selected: _selectedUsers.contains(users[index]), + ); + }, emptyBuilder: (_) { return LayoutBuilder( builder: (context, viewportConstraints) { return SingleChildScrollView( - physics: - const AlwaysScrollableScrollPhysics(), + physics: const AlwaysScrollableScrollPhysics(), child: ConstrainedBox( constraints: BoxConstraints( - minHeight: - viewportConstraints.maxHeight, + minHeight: viewportConstraints.maxHeight, ), child: Center( child: Column( children: [ - const Padding( - padding: EdgeInsets.all(24), - child: StreamSvgIcon( - icon: StreamSvgIcons.search, + Padding( + padding: const EdgeInsets.all(24), + child: Icon( + context.streamIcons.search32, size: 96, color: Colors.grey, ), ), Text( - AppLocalizations.of(context) - .noUserMatchesTheseKeywords, - style: StreamChatTheme.of( - context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme - .of(context) - .colorTheme - .textHighEmphasis - .withOpacity(.5)), + AppLocalizations.of(context).noUserMatchesTheseKeywords, + style: StreamChatTheme.of(context).textTheme.footnote.copyWith( + color: StreamChatTheme.of( + context, + ).colorTheme.textHighEmphasis.withOpacity(.5), + ), ), ], ), @@ -392,10 +363,7 @@ class _NewChatScreenState extends State { AppLocalizations.of(context).noChatsHereYet, style: TextStyle( fontSize: 12, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(.5), + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(.5), ), ), ); diff --git a/sample_app/lib/pages/new_group_chat_screen.dart b/sample_app/lib/pages/new_group_chat_screen.dart index d9fd969568..dbc56ee418 100644 --- a/sample_app/lib/pages/new_group_chat_screen.dart +++ b/sample_app/lib/pages/new_group_chat_screen.dart @@ -16,8 +16,7 @@ class NewGroupChatScreen extends StatefulWidget { } class _NewGroupChatScreenState extends State { - late final TextEditingController _controller = TextEditingController() - ..addListener(_userNameListener); + late final TextEditingController _controller = TextEditingController()..addListener(_userNameListener); String _userNameQuery = ''; @@ -45,8 +44,7 @@ class _NewGroupChatScreenState extends State { _isSearchActive = _userNameQuery.isNotEmpty; }); userListController.filter = Filter.and([ - if (_userNameQuery.isNotEmpty) - Filter.autoComplete('name', _userNameQuery), + if (_userNameQuery.isNotEmpty) Filter.autoComplete('name', _userNameQuery), Filter.notEqual('id', StreamChat.of(context).currentUser!.id), ]); userListController.doInitialLoad(); @@ -87,14 +85,14 @@ class _NewGroupChatScreenState extends State { if (state.users.isNotEmpty) IconButton( color: StreamChatTheme.of(context).colorTheme.accentPrimary, - icon: const StreamSvgIcon(icon: StreamSvgIcons.arrowRight), + icon: Icon(context.streamIcons.arrowRight20), onPressed: () async { GoRouter.of(context).pushNamed( Routes.NEW_GROUP_CHAT_DETAILS.name, extra: state, ); }, - ) + ), ], ), body: StreamConnectionStatusBuilder( @@ -121,8 +119,7 @@ class _NewGroupChatScreenState extends State { message: statusString, child: NestedScrollView( floatHeaderSlivers: true, - headerSliverBuilder: - (BuildContext context, bool innerBoxIsScrolled) { + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverToBoxAdapter( child: SearchTextField( @@ -138,8 +135,7 @@ class _NewGroupChatScreenState extends State { scrollDirection: Axis.horizontal, itemCount: state.users.length, padding: const EdgeInsets.all(8), - separatorBuilder: (_, __) => - const SizedBox(width: 16), + separatorBuilder: (_, __) => const SizedBox(width: 16), itemBuilder: (_, index) { final user = state.users.elementAt(index); return Column( @@ -147,16 +143,8 @@ class _NewGroupChatScreenState extends State { Stack( children: [ StreamUserAvatar( - onlineIndicatorAlignment: - const Alignment(0.9, 0.9), + size: .xl, user: user, - borderRadius: - BorderRadius.circular(32), - constraints: - const BoxConstraints.tightFor( - height: 64, - width: 64, - ), ), Positioned( top: -4, @@ -167,29 +155,20 @@ class _NewGroupChatScreenState extends State { }, child: DecoratedBox( decoration: BoxDecoration( - color: - StreamChatTheme.of(context) - .colorTheme - .appBg, + color: StreamChatTheme.of(context).colorTheme.appBg, shape: BoxShape.circle, border: Border.all( - color: StreamChatTheme.of( - context) - .colorTheme - .appBg, + color: StreamChatTheme.of(context).colorTheme.appBg, ), ), - child: StreamSvgIcon( - color: - StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis, + child: Icon( + context.streamIcons.xmark20, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, size: 24, - icon: StreamSvgIcons.close, ), ), ), - ) + ), ], ), const SizedBox(height: 4), @@ -213,9 +192,7 @@ class _NewGroupChatScreenState extends State { child: Container( width: double.maxFinite, decoration: BoxDecoration( - gradient: StreamChatTheme.of(context) - .colorTheme - .bgGradient, + gradient: StreamChatTheme.of(context).colorTheme.bgGradient, ), child: Padding( padding: const EdgeInsets.symmetric( @@ -227,9 +204,7 @@ class _NewGroupChatScreenState extends State { ? '${AppLocalizations.of(context).matchesFor} "$_userNameQuery"' : AppLocalizations.of(context).onThePlatorm, style: TextStyle( - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), ), ), @@ -263,25 +238,17 @@ class _NewGroupChatScreenState extends State { children: [ Padding( padding: const EdgeInsets.all(24), - child: StreamSvgIcon( - icon: StreamSvgIcons.search, + child: Icon( + context.streamIcons.search32, size: 96, - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, ), ), Text( - AppLocalizations.of(context) - .noUserMatchesTheseKeywords, - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, - ), + AppLocalizations.of(context).noUserMatchesTheseKeywords, + style: StreamChatTheme.of(context).textTheme.footnote.copyWith( + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, + ), ), ], ), @@ -312,8 +279,7 @@ class _HeaderDelegate extends SliverPersistentHeaderDelegate { final double height; @override - Widget build( - BuildContext context, double shrinkOffset, bool overlapsContent) { + Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { return ColoredBox( color: StreamChatTheme.of(context).colorTheme.barsBg, child: child, diff --git a/sample_app/lib/pages/pinned_messages_screen.dart b/sample_app/lib/pages/pinned_messages_screen.dart index 2bcc792fc6..8f81eb43f7 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, ); @@ -57,8 +54,8 @@ class _PinnedMessagesScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - StreamSvgIcon( - icon: StreamSvgIcons.pin, + Icon( + context.streamIcons.pin32, size: 136, color: StreamChatTheme.of(context).colorTheme.disabled, ), @@ -67,37 +64,32 @@ class _PinnedMessagesScreenState extends State { AppLocalizations.of(context).noPinnedItems, style: TextStyle( fontSize: 17, - color: - StreamChatTheme.of(context).colorTheme.textHighEmphasis, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), RichText( textAlign: TextAlign.center, - text: TextSpan(children: [ - TextSpan( - text: '${AppLocalizations.of(context).longPressMessage} ', - style: TextStyle( - fontSize: 14, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + text: TextSpan( + children: [ + TextSpan( + text: '${AppLocalizations.of(context).longPressMessage} ', + style: TextStyle( + fontSize: 14, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), + ), ), - ), - TextSpan( - text: AppLocalizations.of(context).pinToConversation, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + TextSpan( + text: AppLocalizations.of(context).pinToConversation, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), + ), ), - ), - ]), + ], + ), ), ], ), @@ -114,7 +106,7 @@ class _PinnedMessagesScreenState extends State { if (channel.state == null) { await channel.watch(); } - router.pushNamed( + router.goNamed( Routes.CHANNEL_PAGE.name, pathParameters: Routes.CHANNEL_PAGE.params(channel), queryParameters: Routes.CHANNEL_PAGE.queryParams(message), diff --git a/sample_app/lib/pages/reminders_page.dart b/sample_app/lib/pages/reminders_page.dart index 18507e9e83..4934cd48e7 100644 --- a/sample_app/lib/pages/reminders_page.dart +++ b/sample_app/lib/pages/reminders_page.dart @@ -15,20 +15,21 @@ class RemindersPage extends StatefulWidget { } class _RemindersPageState extends State { - late final controller = StreamMessageReminderListController( - client: StreamChat.of(context).client, - )..eventListener = (event) { - if (event.type == EventType.connectionRecovered || - event.type == EventType.notificationReminderDue) { - // This will create the query filter with the updated current date - // and time, so that the reminders list is updated with the new - // reminders that are due. - onFilterChanged(_currentFilter); - } - - // Returning false as we also want the controller to handle the event. - return false; - }; + late final controller = + StreamMessageReminderListController( + client: StreamChat.of(context).client, + ) + ..eventListener = (event) { + if (event.type == EventType.connectionRecovered || event.type == EventType.notificationReminderDue) { + // This will create the query filter with the updated current date + // and time, so that the reminders list is updated with the new + // reminders that are due. + onFilterChanged(_currentFilter); + } + + // Returning false as we also want the controller to handle the event. + return false; + }; @override void dispose() { @@ -80,9 +81,9 @@ class _RemindersPageState extends State { children: [ CustomSlidableAction( backgroundColor: theme.colorTheme.inputBg, - child: StreamSvgIcon( + child: Icon( + context.streamIcons.edit20, size: 24, - icon: StreamSvgIcons.edit, color: theme.colorTheme.accentPrimary, ), onPressed: (_) async { @@ -104,9 +105,9 @@ class _RemindersPageState extends State { ), CustomSlidableAction( backgroundColor: theme.colorTheme.inputBg, - child: StreamSvgIcon( + child: Icon( + context.streamIcons.delete20, size: 24, - icon: StreamSvgIcons.delete, color: theme.colorTheme.accentError, ), onPressed: (context) { @@ -209,7 +210,8 @@ enum MessageRemindersFilter { overdue('Overdue'), upcoming('Upcoming'), scheduled('Scheduled'), - savedForLater('Saved for later'); + savedForLater('Saved for later') + ; const MessageRemindersFilter(this.label); final String label; @@ -238,12 +240,10 @@ class MessageRemindersFilterSelection extends StatefulWidget { final ValueSetter onSelected; @override - State createState() => - _MessageRemindersFilterSelectionState(); + State createState() => _MessageRemindersFilterSelectionState(); } -class _MessageRemindersFilterSelectionState - extends State { +class _MessageRemindersFilterSelectionState extends State { final _filterKeys = {}; @override diff --git a/sample_app/lib/pages/splash_screen.dart b/sample_app/lib/pages/splash_screen.dart index 0bbdcd9ebe..a7fb8c9269 100644 --- a/sample_app/lib/pages/splash_screen.dart +++ b/sample_app/lib/pages/splash_screen.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; -mixin SplashScreenStateMixin on State - implements TickerProvider { +mixin SplashScreenStateMixin on State implements TickerProvider { late final _animationController = AnimationController( vsync: this, duration: const Duration( @@ -20,29 +19,38 @@ mixin SplashScreenStateMixin on State ), ); - late final _circleAnimation = Tween( - begin: 0, - end: 1000, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); + late final _circleAnimation = + Tween( + begin: 0, + end: 1000, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); - late final _colorAnimation = ColorTween( - begin: const Color(0xff005FFF), - end: Colors.transparent, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeInOut, - )); + late final _colorAnimation = + ColorTween( + begin: const Color(0xff005FFF), + end: Colors.transparent, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ), + ); - late final _scaleAnimation = Tween( - begin: 1, - end: 1.5, - ).animate(CurvedAnimation( - parent: _scaleAnimationController, - curve: Curves.easeInOutCubic, - )); + late final _scaleAnimation = + Tween( + begin: 1, + end: 1.5, + ).animate( + CurvedAnimation( + parent: _scaleAnimationController, + curve: Curves.easeInOutCubic, + ), + ); bool animationCompleted = false; @@ -75,50 +83,46 @@ mixin SplashScreenStateMixin on State } Widget buildAnimation() => Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - AnimatedBuilder( - animation: _scaleAnimation, - builder: (context, child) => - Transform.scale(scale: _scaleAnimation.value, child: child), - child: AnimatedBuilder( - animation: _colorAnimation, - builder: (context, child) { - return DecoratedBox( - decoration: BoxDecoration(color: _colorAnimation.value), - child: Center( - child: !_animationController.isAnimating - ? child - : const SizedBox(), - ), - ); - }, - child: RepaintBoundary( - child: Lottie.asset( - 'assets/floating_boat.json', - alignment: Alignment.center, - ), + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) => Transform.scale(scale: _scaleAnimation.value, child: child), + child: AnimatedBuilder( + animation: _colorAnimation, + builder: (context, child) { + return DecoratedBox( + decoration: BoxDecoration(color: _colorAnimation.value), + child: Center( + child: !_animationController.isAnimating ? child : const SizedBox(), ), + ); + }, + child: RepaintBoundary( + child: Lottie.asset( + 'assets/floating_boat.json', + alignment: Alignment.center, ), ), - AnimatedBuilder( - animation: _circleAnimation, - builder: (context, snapshot) { - return Transform.scale( - scale: _circleAnimation.value, - child: Container( - width: 1, - height: 1, - decoration: BoxDecoration( - color: Colors.white - .withOpacity(1 - _animationController.value), - shape: BoxShape.circle, - ), - ), - ); - }, - ), - ], - ); + ), + ), + AnimatedBuilder( + animation: _circleAnimation, + builder: (context, snapshot) { + return Transform.scale( + scale: _circleAnimation.value, + child: Container( + width: 1, + height: 1, + decoration: BoxDecoration( + color: Colors.white.withOpacity(1 - _animationController.value), + shape: BoxShape.circle, + ), + ), + ); + }, + ), + ], + ); } diff --git a/sample_app/lib/pages/thread_list_page.dart b/sample_app/lib/pages/thread_list_page.dart index 8dc69ef3df..4201b30e28 100644 --- a/sample_app/lib/pages/thread_list_page.dart +++ b/sample_app/lib/pages/thread_list_page.dart @@ -1,6 +1,8 @@ import 'package:collection/collection.dart'; 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:stream_chat_flutter/stream_chat_flutter.dart'; class ThreadListPage extends StatefulWidget { @@ -29,9 +31,7 @@ class _ThreadListPageState extends State { valueListenable: controller.unseenThreadIds, builder: (_, unreadThreads, __) => StreamUnreadThreadsBanner( unreadThreads: unreadThreads, - onTap: () => controller - .refresh(resetValue: false) - .then((_) => controller.clearUnseenThreadIds()), + onTap: () => controller.refresh(resetValue: false).then((_) => controller.clearUnseenThreadIds()), ), ), Expanded( @@ -41,9 +41,9 @@ class _ThreadListPageState extends State { final channelCid = thread.channelCid; final channel = StreamChat.of(context).client.channel( - channelCid.split(':')[0], - id: channelCid.split(':')[1], - ); + channelCid.split(':')[0], + id: channelCid.split(':')[1], + ); Navigator.of(context).push( MaterialPageRoute( @@ -51,14 +51,27 @@ class _ThreadListPageState extends State { return StreamChannel( channel: channel, initialMessageId: thread.draft?.parentId, - child: BetterStreamBuilder( - stream: channel.state?.messagesStream.map( - (messages) => messages.firstWhereOrNull( - (m) => m.id == thread.parentMessage?.id, - ), - ), + child: BetterStreamBuilder( + initialData: thread.parentMessage, + stream: channel.state?.messagesStream + .map( + (messages) => messages.firstWhereOrNull( + (m) => m.id == thread.parentMessage?.id, + ), + ) + .where((msg) => msg != null) + .cast(), builder: (_, parentMessage) { - return ThreadPage(parent: parentMessage); + return ThreadPage( + parent: parentMessage, + onViewInChannelTap: (message) { + GoRouter.of(context).goNamed( + Routes.CHANNEL_PAGE.name, + pathParameters: Routes.CHANNEL_PAGE.params(channel), + queryParameters: {'mid': message.id}, + ); + }, + ); }, ), ); diff --git a/sample_app/lib/pages/thread_page.dart b/sample_app/lib/pages/thread_page.dart index 622b5772ca..c4c401e627 100644 --- a/sample_app/lib/pages/thread_page.dart +++ b/sample_app/lib/pages/thread_page.dart @@ -7,10 +7,12 @@ class ThreadPage extends StatefulWidget { required this.parent, this.initialScrollIndex, this.initialAlignment, + this.onViewInChannelTap, }); final Message parent; final int? initialScrollIndex; final double? initialAlignment; + final void Function(Message message)? onViewInChannelTap; @override State createState() => _ThreadPageState(); @@ -45,9 +47,7 @@ class _ThreadPageState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, - appBar: StreamThreadHeader( - parent: widget.parent, - ), + appBar: StreamThreadHeader(parent: widget.parent), body: Column( children: [ Expanded( @@ -55,26 +55,12 @@ class _ThreadPageState extends State { parentMessage: widget.parent, initialScrollIndex: widget.initialScrollIndex, initialAlignment: widget.initialAlignment, - //onMessageSwiped: _reply, + onReplyTap: _reply, + swipeToReply: true, messageFilter: defaultFilter, showScrollToBottom: false, highlightInitialMessage: true, - messageBuilder: (context, details, messages, defaultMessage) { - return defaultMessage.copyWith( - onReplyTap: _reply, - bottomRowBuilderWithDefaultWidget: ( - context, - message, - defaultWidget, - ) { - return defaultWidget.copyWith( - deletedBottomRowBuilder: (context, message) { - return const StreamVisibleFootnote(); - }, - ); - }, - ); - }, + onViewInChannelTap: widget.onViewInChannelTap, ), ), if (widget.parent.type != 'deleted') diff --git a/sample_app/lib/pages/user_mentions_page.dart b/sample_app/lib/pages/user_mentions_page.dart index d4bb19e273..b02b559a39 100644 --- a/sample_app/lib/pages/user_mentions_page.dart +++ b/sample_app/lib/pages/user_mentions_page.dart @@ -41,21 +41,17 @@ class _UserMentionsPageState extends State { children: [ Padding( padding: const EdgeInsets.all(24), - child: StreamSvgIcon( - icon: StreamSvgIcons.mentions, + child: Icon( + context.streamIcons.mention32, size: 96, - color: - StreamChatTheme.of(context).colorTheme.disabled, + color: StreamChatTheme.of(context).colorTheme.disabled, ), ), Text( AppLocalizations.of(context).noMentionsExistYet, - style: - StreamChatTheme.of(context).textTheme.body.copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textLowEmphasis, - ), + style: StreamChatTheme.of(context).textTheme.body.copyWith( + color: StreamChatTheme.of(context).colorTheme.textLowEmphasis, + ), ), ], ), diff --git a/sample_app/lib/routes/app_routes.dart b/sample_app/lib/routes/app_routes.dart index 094060eb44..ef0f41dce5 100644 --- a/sample_app/lib/routes/app_routes.dart +++ b/sample_app/lib/routes/app_routes.dart @@ -19,24 +19,19 @@ final appRoutes = [ GoRoute( name: Routes.CHANNEL_LIST_PAGE.name, path: Routes.CHANNEL_LIST_PAGE.path, - builder: (BuildContext context, GoRouterState state) => - const ChannelListPage(), + builder: (BuildContext context, GoRouterState state) => const ChannelListPage(), routes: [ GoRoute( name: Routes.CHANNEL_PAGE.name, path: Routes.CHANNEL_PAGE.path, builder: (context, state) { - final channel = StreamChat.of(context) - .client - .state - .channels[state.pathParameters['cid']]; + final channel = StreamChat.of(context).client.state.channels[state.pathParameters['cid']]; final messageId = state.uri.queryParameters['mid']; final parentId = state.uri.queryParameters['pid']; Message? parentMessage; if (parentId != null) { - parentMessage = channel?.state!.messages - .firstWhereOrNull((it) => it.id == parentId); + parentMessage = channel?.state!.messages.firstWhereOrNull((it) => it.id == parentId); } return StreamChannel( @@ -58,10 +53,7 @@ final appRoutes = [ name: Routes.CHAT_INFO_SCREEN.name, path: Routes.CHAT_INFO_SCREEN.path, builder: (BuildContext context, GoRouterState state) { - final channel = StreamChat.of(context) - .client - .state - .channels[state.pathParameters['cid']]; + final channel = StreamChat.of(context).client.state.channels[state.pathParameters['cid']]; return StreamChannel( channel: channel!, child: ChatInfoScreen( @@ -75,10 +67,7 @@ final appRoutes = [ name: Routes.GROUP_INFO_SCREEN.name, path: Routes.GROUP_INFO_SCREEN.path, builder: (BuildContext context, GoRouterState state) { - final channel = StreamChat.of(context) - .client - .state - .channels[state.pathParameters['cid']]; + final channel = StreamChat.of(context).client.state.channels[state.pathParameters['cid']]; return StreamChannel( channel: channel!, child: GroupInfoScreen( @@ -116,13 +105,11 @@ final appRoutes = [ GoRoute( name: Routes.CHOOSE_USER.name, path: Routes.CHOOSE_USER.path, - builder: (BuildContext context, GoRouterState state) => - const ChooseUserPage(), + builder: (BuildContext context, GoRouterState state) => const ChooseUserPage(), ), GoRoute( name: Routes.ADVANCED_OPTIONS.name, path: Routes.ADVANCED_OPTIONS.path, - builder: (BuildContext context, GoRouterState state) => - const AdvancedOptionsPage(), + builder: (BuildContext context, GoRouterState state) => const AdvancedOptionsPage(), ), ]; diff --git a/sample_app/lib/routes/routes.dart b/sample_app/lib/routes/routes.dart index d60383edd2..ac62189d37 100644 --- a/sample_app/lib/routes/routes.dart +++ b/sample_app/lib/routes/routes.dart @@ -4,24 +4,24 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Application routes abstract class Routes { - static const RouteConfig CHOOSE_USER = - RouteConfig(name: 'choose_user', path: '/users'); - static const RouteConfig ADVANCED_OPTIONS = - RouteConfig(name: 'advanced_options', path: '/options'); - static const ChannelRouteConfig CHANNEL_PAGE = - ChannelRouteConfig(name: 'channel_page', path: 'channel/:cid'); - static const RouteConfig NEW_CHAT = - RouteConfig(name: 'new_chat', path: '/new_chat'); - static const RouteConfig NEW_GROUP_CHAT = - RouteConfig(name: 'new_group_chat', path: '/new_group_chat'); + static const RouteConfig CHOOSE_USER = RouteConfig(name: 'choose_user', path: '/users'); + static const RouteConfig ADVANCED_OPTIONS = RouteConfig(name: 'advanced_options', path: '/options'); + static const ChannelRouteConfig CHANNEL_PAGE = ChannelRouteConfig(name: 'channel_page', path: 'channel/:cid'); + static const RouteConfig NEW_CHAT = RouteConfig(name: 'new_chat', path: '/new_chat'); + static const RouteConfig NEW_GROUP_CHAT = RouteConfig(name: 'new_group_chat', path: '/new_group_chat'); static const RouteConfig NEW_GROUP_CHAT_DETAILS = RouteConfig( - name: 'new_group_chat_details', path: '/new_group_chat_details'); - static const ChannelRouteConfig CHAT_INFO_SCREEN = - ChannelRouteConfig(name: 'chat_info_screen', path: 'chat_info_screen'); - static const ChannelRouteConfig GROUP_INFO_SCREEN = - ChannelRouteConfig(name: 'group_info_screen', path: 'group_info_screen'); - static const RouteConfig CHANNEL_LIST_PAGE = - RouteConfig(name: 'channel_list_page', path: '/channels'); + name: 'new_group_chat_details', + path: '/new_group_chat_details', + ); + static const ChannelRouteConfig CHAT_INFO_SCREEN = ChannelRouteConfig( + name: 'chat_info_screen', + path: 'chat_info_screen', + ); + static const ChannelRouteConfig GROUP_INFO_SCREEN = ChannelRouteConfig( + name: 'group_info_screen', + path: 'group_info_screen', + ); + static const RouteConfig CHANNEL_LIST_PAGE = RouteConfig(name: 'channel_list_page', path: '/channels'); } class RouteConfig { @@ -36,7 +36,7 @@ class ChannelRouteConfig extends RouteConfig { Map params(Channel channel) => {'cid': channel.cid!}; Map queryParams(Message message) => { - 'mid': message.id, - if (message.parentId != null) 'pid': message.parentId! - }; + 'mid': message.id, + if (message.parentId != null) 'pid': message.parentId!, + }; } diff --git a/sample_app/lib/state/init_data.dart b/sample_app/lib/state/init_data.dart index 7f23896afd..3bb7fc4f1b 100644 --- a/sample_app/lib/state/init_data.dart +++ b/sample_app/lib/state/init_data.dart @@ -31,6 +31,5 @@ class InitData { final StreamChatClient client; final StreamingSharedPreferences preferences; - InitData copyWith({required StreamChatClient client}) => - InitData(client, preferences); + InitData copyWith({required StreamChatClient client}) => InitData(client, preferences); } diff --git a/sample_app/lib/utils/app_config.dart b/sample_app/lib/utils/app_config.dart index e006cddfee..d6443747e6 100644 --- a/sample_app/lib/utils/app_config.dart +++ b/sample_app/lib/utils/app_config.dart @@ -1,81 +1,68 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -const sentryDsn = - 'https://6381ef88de4140db8f5e25ab37e0f08c@o1213503.ingest.sentry.io/6352870'; +const sentryDsn = 'https://6381ef88de4140db8f5e25ab37e0f08c@o1213503.ingest.sentry.io/6352870'; const kDefaultStreamApiKey = 'kv7mcsxr24p8'; final defaultUsers = { 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FsdmF0b3JlIn0.pgiJz7sIc7iP29BHKFwe3nLm5-OaR_1l2P-SlgiC9a8': User( - id: 'salvatore', - extraData: const { - 'name': 'Salvatore Giordano', - 'image': - 'https://avatars.githubusercontent.com/u/20601437?s=460&u=3f66c22a7483980624804054ae7f357cf102c784&v=4', - }, - ), - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FoaWwifQ.WnIUoB5gR2kcAsFhiDvkiD6zdHXZ-VSU2aQWWkhsvfo': - User( + id: 'salvatore', + extraData: const { + 'name': 'Salvatore Giordano', + 'image': + 'https://avatars.githubusercontent.com/u/20601437?s=460&u=3f66c22a7483980624804054ae7f357cf102c784&v=4', + }, + ), + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoic2FoaWwifQ.WnIUoB5gR2kcAsFhiDvkiD6zdHXZ-VSU2aQWWkhsvfo': User( id: 'sahil', extraData: const { 'name': 'Sahil Kumar', - 'image': - 'https://avatars.githubusercontent.com/u/25670178?s=400&u=30ded3784d8d2310c5748f263fd5e6433c119aa1&v=4', + 'image': 'https://avatars.githubusercontent.com/u/25670178?s=400&u=30ded3784d8d2310c5748f263fd5e6433c119aa1&v=4', }, ), - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYmVuIn0.nAz2sNFGQwY7rl2Og2z3TGHUsdpnN53tOsUglJFvLmg': - User( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYmVuIn0.nAz2sNFGQwY7rl2Og2z3TGHUsdpnN53tOsUglJFvLmg': User( id: 'ben', extraData: const { 'name': 'Ben Golden', 'image': 'https://avatars.githubusercontent.com/u/1581974?s=400&v=4', }, ), - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidGhpZXJyeSJ9.lEq6TrZtHzjoNtf7HHRufUPyGo_pa8vg4_XhEBp4ckY': - User( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidGhpZXJyeSJ9.lEq6TrZtHzjoNtf7HHRufUPyGo_pa8vg4_XhEBp4ckY': User( id: 'thierry', extraData: const { 'name': 'Thierry Schellenbach', - 'image': - 'https://avatars.githubusercontent.com/u/265409?s=400&u=2d0e3bb1820db992066196bff7b004f0eee8e28d&v=4', + 'image': 'https://avatars.githubusercontent.com/u/265409?s=400&u=2d0e3bb1820db992066196bff7b004f0eee8e28d&v=4', }, ), - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidG9tbWFzbyJ9.GLSI0ESshERMo2WjUpysD709NEtn1zmGimUN2an7g9o': - User( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidG9tbWFzbyJ9.GLSI0ESshERMo2WjUpysD709NEtn1zmGimUN2an7g9o': User( id: 'tommaso', extraData: const { 'name': 'Tommaso Barbugli', 'image': 'https://avatars.githubusercontent.com/u/88735?s=400&v=4', }, ), - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZGV2ZW4ifQ.z3zI4PqJnNhc-1o-VKcmb6BnnQ0oxFNCRHwEulHqcWc': - User( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiZGV2ZW4ifQ.z3zI4PqJnNhc-1o-VKcmb6BnnQ0oxFNCRHwEulHqcWc': User( id: 'deven', extraData: const { 'name': 'Deven Joshi', - 'image': - 'https://avatars.githubusercontent.com/u/26357843?s=400&u=0c61d890866e67bf69f58878be58915e9bfd39ee&v=4', + 'image': 'https://avatars.githubusercontent.com/u/26357843?s=400&u=0c61d890866e67bf69f58878be58915e9bfd39ee&v=4', }, ), - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibmVldmFzaCJ9.3EdHegTxibrz3A9cTiKmpEyawwcCVB8FXnoFzr4eKvw': - User( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibmVldmFzaCJ9.3EdHegTxibrz3A9cTiKmpEyawwcCVB8FXnoFzr4eKvw': User( id: 'neevash', extraData: const { 'name': 'Neevash Ramdial', - 'image': - 'https://avatars.githubusercontent.com/u/25674767?s=400&u=1d7333baf7dd9d143db8bfcdb31a838b89cfff9c&v=4', + 'image': 'https://avatars.githubusercontent.com/u/25674767?s=400&u=1d7333baf7dd9d143db8bfcdb31a838b89cfff9c&v=4', }, ), - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MSJ9.fnelU7HcP7QoEEsCGteNlF1fppofzNlrnpDQuIgeKCU': - User( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MSJ9.fnelU7HcP7QoEEsCGteNlF1fppofzNlrnpDQuIgeKCU': User( id: 'qatest1', extraData: const { 'name': 'QA test 1', }, ), - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MiJ9.vSCqAEbs2WVmMWsOsa7065Fsjq-rsTih6qsHPynl7XM': - User( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicWF0ZXN0MiJ9.vSCqAEbs2WVmMWsOsa7065Fsjq-rsTih6qsHPynl7XM': User( id: 'qatest2', extraData: const { 'name': 'QA test 2', diff --git a/sample_app/lib/utils/local_notification_observer.dart b/sample_app/lib/utils/local_notification_observer.dart index 4b2fc12a69..7be9674c63 100644 --- a/sample_app/lib/utils/local_notification_observer.dart +++ b/sample_app/lib/utils/local_notification_observer.dart @@ -12,18 +12,17 @@ class LocalNotificationObserver extends NavigatorObserver { ) { _subscription = client .on( - EventType.messageNew, - EventType.notificationMessageNew, - ) + EventType.messageNew, + EventType.notificationMessageNew, + ) .listen((event) { - _handleEvent(event, client, navigatorKey); - }); + _handleEvent(event, client, navigatorKey); + }); } Route? currentRoute; late final StreamSubscription _subscription; - void _handleEvent(Event event, StreamChatClient client, - GlobalKey navigatorKey) { + void _handleEvent(Event event, StreamChatClient client, GlobalKey navigatorKey) { if (event.message?.user?.id == client.state.currentUser?.id) { return; } diff --git a/sample_app/lib/utils/localizations.dart b/sample_app/lib/utils/localizations.dart index 7ce1005291..2d761d8501 100644 --- a/sample_app/lib/utils/localizations.dart +++ b/sample_app/lib/utils/localizations.dart @@ -20,20 +20,17 @@ class AppLocalizations { 'create_a_group': 'Create a Group', 'custom_settings': 'Custom settings', 'delete': 'Delete', - 'delete_conversation_are_you_sure': - 'Are you sure you want to delete this conversation?', + 'delete_conversation_are_you_sure': 'Are you sure you want to delete this conversation?', 'delete_conversation_title': 'Delete Conversation', 'disconnected': 'Disconnected', 'error_connecting': 'Error connecting, retry', 'files': 'Files', 'files_appear_here': 'Files sent in this chat will appear here', - 'group_shared_with_user_appear_here': - 'Group shared with User will appear here.', + 'group_shared_with_user_appear_here': 'Group shared with User will appear here.', 'last_seen': 'Last seen', 'leave': 'Leave', 'leave_conversation': 'Leave conversation', - 'leave_conversation_are_you_sure': - 'Are you sure you want to leave this conversation?', + 'leave_conversation_are_you_sure': 'Are you sure you want to leave this conversation?', 'leave_group': 'Leave Group', 'loading': 'Loading...', 'login': 'Login', @@ -65,21 +62,18 @@ class AppLocalizations { 'ok': 'OK', 'online': 'Online', 'on_the_platform': 'On the platform', - 'operation_could_not_be_completed': - "The operation couldn't be completed.", + 'operation_could_not_be_completed': "The operation couldn't be completed.", 'owner': 'Owner', 'pin_group': 'Pin group', 'photos_and_videos': 'Photos & Videos', - 'photos_or_videos_will_appear_here': - 'Photos or videos sent in this chat will \nappear here', + 'photos_or_videos_will_appear_here': 'Photos or videos sent in this chat will \nappear here', 'pinned_messages': 'Pinned Messages', 'pin_to_conversation': 'Pin to conversation', 'reconnecting': 'Reconnecting...', 'remove': 'Remove', 'remove_from_group': 'Remove From Group', 'remove_member': 'Remove member', - 'remove_member_are_you_sure': - 'Are you sure you want to remove this member?', + 'remove_member_are_you_sure': 'Are you sure you want to remove this member?', 'search': 'Search', 'select_user_to_try_flutter_sdk': 'Select a user to try the Flutter SDK', 'shared_groups': 'Shared Groups', @@ -113,20 +107,17 @@ class AppLocalizations { 'create_a_group': 'Crea un Gruppo', 'custom_settings': 'Opzioni Personalizzate', 'delete': 'Cancella', - 'delete_conversation_are_you_sure': - 'Sei sicuro di voler eliminare la conversazione?', + 'delete_conversation_are_you_sure': 'Sei sicuro di voler eliminare la conversazione?', 'delete_conversation_title': 'Elimina Conversazione', 'disconnected': 'Disconnesso', 'error_connecting': 'Errore durante la connessione, riprova', 'files': 'File', 'files_appear_here': 'I file inviati in questa chat compariranno qui', - 'group_shared_with_user_appear_here': - "I gruppi in comune con quest'utente compariranno qui", + 'group_shared_with_user_appear_here': "I gruppi in comune con quest'utente compariranno qui", 'last_seen': 'Ultimo accesso', 'leave': 'Lascia', 'leave_conversation': 'Lascia conversazione', - 'leave_conversation_are_you_sure': - 'Sei sicuro di voler lasciare questa conversazione?', + 'leave_conversation_are_you_sure': 'Sei sicuro di voler lasciare questa conversazione?', 'leave_group': 'Lascia Gruppo', 'loading': 'Caricamento...', 'login': 'Login', @@ -158,12 +149,10 @@ class AppLocalizations { 'ok': 'OK', 'online': 'Online', 'on_the_platform': 'Sulla piattaforma', - 'operation_could_not_be_completed': - "Non é stato possibile completare l'operazione.", + 'operation_could_not_be_completed': "Non é stato possibile completare l'operazione.", 'owner': 'Proprietario', 'photos_and_videos': 'Foto & Video', - 'photos_or_videos_will_appear_here': - 'Foto or video inviati in questa chat \ncompariranno qui', + 'photos_or_videos_will_appear_here': 'Foto or video inviati in questa chat \ncompariranno qui', 'pinned_messages': 'Messaggi in evidenza', 'pin_group': 'Gruppo di evidenziazione', 'pin_to_conversation': 'Metti in evidenza', @@ -171,11 +160,9 @@ class AppLocalizations { 'remove': 'Rimuovi', 'remove_from_group': 'Rimuovi Dal Gruppo', 'remove_member': 'Rimuovi membro', - 'remove_member_are_you_sure': - 'Sei sicuro di voler rimuovere questo membro?', + 'remove_member_are_you_sure': 'Sei sicuro di voler rimuovere questo membro?', 'search': 'Cerca', - 'select_user_to_try_flutter_sdk': - "Seleziona un utente per provare l'SDK Flutter", + 'select_user_to_try_flutter_sdk': "Seleziona un utente per provare l'SDK Flutter", 'shared_groups': 'Gruppi in comune', 'sign_out': 'Sign out', 'something_went_wrong_error_message': 'Qualcosa é andato storto', @@ -256,8 +243,7 @@ class AppLocalizations { } String get deleteConversationAreYouSure { - return _localizedValues[locale.languageCode]![ - 'delete_conversation_are_you_sure']!; + return _localizedValues[locale.languageCode]!['delete_conversation_are_you_sure']!; } String get deleteConversationTitle { @@ -281,8 +267,7 @@ class AppLocalizations { } String get groupSharedWithUserAppearHere { - return _localizedValues[locale.languageCode]![ - 'group_shared_with_user_appear_here']!; + return _localizedValues[locale.languageCode]!['group_shared_with_user_appear_here']!; } String get lastSeen { @@ -298,8 +283,7 @@ class AppLocalizations { } String get leaveConversationAreYouSure { - return _localizedValues[locale.languageCode]![ - 'leave_conversation_are_you_sure']!; + return _localizedValues[locale.languageCode]!['leave_conversation_are_you_sure']!; } String get leaveGroup { @@ -339,8 +323,7 @@ class AppLocalizations { } String get messageChannelDescription { - return _localizedValues[locale.languageCode]![ - 'message_channel_description']!; + return _localizedValues[locale.languageCode]!['message_channel_description']!; } String get messageChannelName { @@ -412,8 +395,7 @@ class AppLocalizations { } String get noUserMatchesTheseKeywords { - return _localizedValues[locale.languageCode]![ - 'no_user_matches_these_keywords']!; + return _localizedValues[locale.languageCode]!['no_user_matches_these_keywords']!; } String get ok { @@ -429,8 +411,7 @@ class AppLocalizations { } String get operationCouldNotBeCompleted { - return _localizedValues[locale.languageCode]![ - 'operation_could_not_be_completed']!; + return _localizedValues[locale.languageCode]!['operation_could_not_be_completed']!; } String get owner { @@ -442,8 +423,7 @@ class AppLocalizations { } String get photosOrVideosWillAppearHere { - return _localizedValues[locale.languageCode]![ - 'photos_or_videos_will_appear_here']!; + return _localizedValues[locale.languageCode]!['photos_or_videos_will_appear_here']!; } String get pinGroup { @@ -475,8 +455,7 @@ class AppLocalizations { } String get removeMemberAreYouSure { - return _localizedValues[locale.languageCode]![ - 'remove_member_are_you_sure']!; + return _localizedValues[locale.languageCode]!['remove_member_are_you_sure']!; } String get search { @@ -484,8 +463,7 @@ class AppLocalizations { } String get selectUserToTryFlutterSDK { - return _localizedValues[locale.languageCode]![ - 'select_user_to_try_flutter_sdk']!; + return _localizedValues[locale.languageCode]!['select_user_to_try_flutter_sdk']!; } String get sharedGroups { @@ -497,8 +475,7 @@ class AppLocalizations { } String get somethingWentWrongErrorMessage { - return _localizedValues[locale.languageCode]![ - 'something_went_wrong_error_message']!; + return _localizedValues[locale.languageCode]!['something_went_wrong_error_message']!; } String get streamSDK { @@ -556,8 +533,7 @@ class AppLocalizationsDelegate extends LocalizationsDelegate { const AppLocalizationsDelegate(); @override - bool isSupported(Locale locale) => - AppLocalizations.languages().contains(locale.languageCode); + bool isSupported(Locale locale) => AppLocalizations.languages().contains(locale.languageCode); @override Future load(Locale locale) { diff --git a/sample_app/lib/utils/location_provider.dart b/sample_app/lib/utils/location_provider.dart new file mode 100644 index 0000000000..67215d2efb --- /dev/null +++ b/sample_app/lib/utils/location_provider.dart @@ -0,0 +1,99 @@ +// 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/notifications_service.dart b/sample_app/lib/utils/notifications_service.dart index 9c34099c4f..6a8b2e809b 100644 --- a/sample_app/lib/utils/notifications_service.dart +++ b/sample_app/lib/utils/notifications_service.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart' - hide Message; +import 'package:flutter_local_notifications/flutter_local_notifications.dart' hide Message; import 'package:go_router/go_router.dart'; import 'package:sample_app/routes/routes.dart'; import 'package:sample_app/utils/localizations.dart'; 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..27e04868dd --- /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..2b0a46e7d6 100644 --- a/sample_app/lib/widgets/channel_list.dart +++ b/sample_app/lib/widgets/channel_list.dart @@ -21,8 +21,7 @@ class ChannelList extends StatefulWidget { class _ChannelList extends State { final ScrollController _scrollController = ScrollController(); - late final StreamMessageSearchListController _messageSearchListController = - StreamMessageSearchListController( + late final StreamMessageSearchListController _messageSearchListController = StreamMessageSearchListController( client: StreamChat.of(context).client, filter: Filter.in_('members', [StreamChat.of(context).currentUser!.id]), limit: 5, @@ -33,8 +32,7 @@ class _ChannelList extends State { ], ); - late final TextEditingController _controller = TextEditingController() - ..addListener(_channelQueryListener); + late final TextEditingController _controller = TextEditingController()..addListener(_channelQueryListener); bool _isSearchActive = false; @@ -61,7 +59,7 @@ class _ChannelList extends State { ChannelSortKey.pinnedAt, nullOrdering: NullOrdering.nullsLast, ), - const SortOption.desc(ChannelSortKey.lastMessageAt), + const SortOption.desc(ChannelSortKey.lastUpdated), ], limit: 30, ); @@ -86,8 +84,7 @@ class _ChannelList extends State { }, child: NotificationListener( onNotification: (ScrollNotification scrollInfo) { - if (_scrollController.position.userScrollDirection == - ScrollDirection.reverse) { + if (_scrollController.position.userScrollDirection == ScrollDirection.reverse) { FocusScope.of(context).unfocus(); } return true; @@ -119,96 +116,89 @@ class _ChannelListDefault extends StatelessWidget { @override Widget build(BuildContext context) { + final chatTheme = StreamChatTheme.of(context); return SlidableAutoCloseBehavior( child: RefreshIndicator( onRefresh: channelListController.refresh, child: StreamChannelListView( controller: channelListController, itemBuilder: (context, channels, index, defaultWidget) { - final chatTheme = StreamChatTheme.of(context); final channel = channels[index]; - final backgroundColor = chatTheme.colorTheme.inputBg; - final canDeleteChannel = channel.canDeleteChannel; - return Slidable( - groupTag: 'channels-actions', - endActionPane: ActionPane( - extentRatio: canDeleteChannel ? 0.40 : 0.20, - motion: const BehindMotion(), - children: [ - CustomSlidableAction( - backgroundColor: backgroundColor, - onPressed: (_) { - showChannelInfoModalBottomSheet( - context: context, - channel: channel, - onViewInfoTap: () { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - final isOneToOne = channel.memberCount == 2 && - channel.isDistinct; - return StreamChannel( - channel: channel, - child: isOneToOne - ? ChatInfoScreen( - messageTheme: - chatTheme.ownMessageTheme, - user: channel.state!.members - .where((m) => - m.userId != - channel.client.state - .currentUser!.id) - .first - .user, - ) - : GroupInfoScreen( - messageTheme: - chatTheme.ownMessageTheme, - ), - ); - }, - ), + return StreamBuilder( + stream: channel.isMutedStream, + initialData: channel.isMuted, + builder: (context, snapshot) { + final isMuted = snapshot.data ?? false; + return Slidable( + groupTag: 'channels-actions', + endActionPane: ActionPane( + extentRatio: 0.4, + motion: const BehindMotion(), + children: [ + CustomSlidableAction( + backgroundColor: context.streamColorScheme.backgroundSurface, + onPressed: (_) { + showChannelInfoModalBottomSheet( + context: context, + channel: channel, + onViewInfoTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + final isOneToOne = channel.memberCount == 2 && channel.isDistinct; + return StreamChannel( + channel: channel, + child: isOneToOne + ? ChatInfoScreen( + messageTheme: chatTheme.ownMessageTheme, + user: channel.state!.members + .where((m) => m.userId != channel.client.state.currentUser!.id) + .first + .user, + ) + : GroupInfoScreen( + messageTheme: chatTheme.ownMessageTheme, + ), + ); + }, + ), + ); + }, ); }, - ); - }, - child: const Icon(Icons.more_horiz), - ), - if (canDeleteChannel) - CustomSlidableAction( - backgroundColor: backgroundColor, - child: StreamSvgIcon( - icon: StreamSvgIcons.delete, - color: chatTheme.colorTheme.accentError, + child: const Icon(Icons.more_horiz), ), - onPressed: (_) async { - final res = await showConfirmationBottomSheet( - context, - title: 'Delete Conversation', - question: - 'Are you sure you want to delete this conversation?', - okText: 'Delete', - cancelText: 'Cancel', - icon: StreamSvgIcon( - icon: StreamSvgIcons.delete, - color: chatTheme.colorTheme.accentError, - ), - ); - if (res == true) { - await channelListController.deleteChannel(channel); - } - }, - ), - ], - ), - child: ColoredBox( - color: channel.isPinned - ? chatTheme.colorTheme.highlight - : Colors.transparent, - child: defaultWidget, - ), + CustomSlidableAction( + backgroundColor: chatTheme.colorTheme.accentPrimary, + foregroundColor: Colors.white, + onPressed: (_) async { + if (isMuted) { + await channel.unmute(); + } else { + await channel.mute(); + } + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + isMuted ? context.streamIcons.audio20 : context.streamIcons.mute20, + size: 20, + color: Colors.white, + ), + ], + ), + ), + ], + ), + child: ColoredBox( + color: channel.isPinned ? chatTheme.colorTheme.highlight : Colors.transparent, + child: defaultWidget, + ), + ); + }, ); }, onChannelTap: (channel) { @@ -222,8 +212,8 @@ class _ChannelListDefault extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(8), child: StreamScrollViewEmptyWidget( - emptyIcon: StreamSvgIcon( - icon: StreamSvgIcons.message, + emptyIcon: Icon( + context.streamIcons.messageBubble32, size: 148, color: StreamChatTheme.of(context).colorTheme.disabled, ), @@ -233,14 +223,9 @@ class _ChannelListDefault extends StatelessWidget { }, child: Text( 'Start a chat', - style: StreamChatTheme.of(context) - .textTheme - .bodyBold - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .accentPrimary, - ), + style: StreamChatTheme.of(context).textTheme.bodyBold.copyWith( + color: StreamChatTheme.of(context).colorTheme.accentPrimary, + ), ), ), ), @@ -274,10 +259,10 @@ class _ChannelListSearch extends StatelessWidget { child: Center( child: Column( children: [ - const Padding( - padding: EdgeInsets.all(24), - child: StreamSvgIcon( - icon: StreamSvgIcons.search, + Padding( + padding: const EdgeInsets.all(24), + child: Icon( + context.streamIcons.search32, size: 96, color: Colors.grey, ), @@ -293,35 +278,36 @@ class _ChannelListSearch extends StatelessWidget { }, ); }, - itemBuilder: ( - context, - messageResponses, - index, - defaultWidget, - ) { - final messageResponse = messageResponses[index]; + itemBuilder: + ( + context, + messageResponses, + index, + defaultWidget, + ) { + final messageResponse = messageResponses[index]; - return defaultWidget.copyWith( - onTap: () async { - FocusScope.of(context).requestFocus(FocusNode()); - final client = StreamChat.of(context).client; - final router = GoRouter.of(context); - final message = messageResponse.message; - final channel = client.channel( - messageResponse.channel!.type, - id: messageResponse.channel!.id, - ); - if (channel.state == null) { - await channel.watch(); - } - router.pushNamed( - Routes.CHANNEL_PAGE.name, - pathParameters: Routes.CHANNEL_PAGE.params(channel), - queryParameters: Routes.CHANNEL_PAGE.queryParams(message), + return defaultWidget.copyWith( + onTap: () async { + FocusScope.of(context).requestFocus(FocusNode()); + final client = StreamChat.of(context).client; + final router = GoRouter.of(context); + final message = messageResponse.message; + final channel = client.channel( + messageResponse.channel!.type, + id: messageResponse.channel!.id, + ); + if (channel.state == null) { + await channel.watch(); + } + router.pushNamed( + Routes.CHANNEL_PAGE.name, + pathParameters: Routes.CHANNEL_PAGE.params(channel), + queryParameters: Routes.CHANNEL_PAGE.queryParams(message), + ); + }, ); }, - ); - }, ); } } diff --git a/sample_app/lib/widgets/chips_input_text_field.dart b/sample_app/lib/widgets/chips_input_text_field.dart index 45368e0939..cf7bb45ba6 100644 --- a/sample_app/lib/widgets/chips_input_text_field.dart +++ b/sample_app/lib/widgets/chips_input_text_field.dart @@ -77,14 +77,9 @@ class ChipInputTextFieldState extends State> { padding: const EdgeInsets.symmetric(vertical: 4), child: Text( '${AppLocalizations.of(context).to.toUpperCase()}:', - style: StreamChatTheme.of(context) - .textTheme - .footnote - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(.5)), + style: StreamChatTheme.of(context).textTheme.footnote.copyWith( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(.5), + ), ), ), const SizedBox(width: 12), @@ -114,14 +109,9 @@ class ChipInputTextFieldState extends State> { disabledBorder: InputBorder.none, contentPadding: const EdgeInsets.only(top: 4), hintText: widget.hint, - hintStyle: StreamChatTheme.of(context) - .textTheme - .body - .copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(.5)), + hintStyle: StreamChatTheme.of(context).textTheme.body.copyWith( + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(.5), + ), ), ), ], @@ -132,20 +122,14 @@ class ChipInputTextFieldState extends State> { alignment: Alignment.bottomCenter, child: IconButton( icon: _chips.isEmpty - ? StreamSvgIcon( - icon: StreamSvgIcons.user, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + ? Icon( + context.streamIcons.user20, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), size: 24, ) - : StreamSvgIcon( - icon: StreamSvgIcons.userAdd, - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(0.5), + : Icon( + context.streamIcons.userAdd20, + color: StreamChatTheme.of(context).colorTheme.textHighEmphasis.withOpacity(0.5), size: 24, ), onPressed: resumeItemAddition, diff --git a/sample_app/lib/widgets/custom_message_actions.dart b/sample_app/lib/widgets/custom_message_actions.dart new file mode 100644 index 0000000000..f359ee95e6 --- /dev/null +++ b/sample_app/lib/widgets/custom_message_actions.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:sample_app/widgets/message_info_sheet.dart'; +import 'package:sample_app/widgets/reminder_dialog.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Custom [StreamComponentBuilder] for [StreamMessageWidgetProps] that +/// composes app-specific message action customizations via a delegation +/// chain. +/// +/// Delegation chain: +/// ``` +/// customMessageWidgetBuilder +/// → _ReminderActions (remind me, save for later, edit/remove reminder) +/// → _DeleteForMeAction (delete message for current user only) +/// → _MessageInfoAction (show message delivery info sheet) +/// ``` +Widget customMessageWidgetBuilder( + BuildContext context, + StreamMessageWidgetProps props, +) { + return DefaultStreamMessage( + props: props.copyWith( + actionsBuilder: (context, defaultActions) { + final message = props.message; + return StreamContextMenuAction.partitioned( + items: [ + ...defaultActions, + ..._ReminderActions.build(context, message), + ..._DeleteForMeAction.build(context, message), + ..._MessageInfoAction.build(context, message), + ], + ); + }, + ), + ); +} + +// --------------------------------------------------------------------------- +// Reminder actions +// --------------------------------------------------------------------------- + +abstract final class _ReminderActions { + static List build( + BuildContext context, + Message message, + ) { + final icons = context.streamIcons; + final channel = StreamChannel.of(context).channel; + final channelConfig = channel.config; + if (channelConfig?.userMessageReminders != true) return const []; + + final reminder = message.reminder; + if (reminder != null) { + return [ + StreamContextMenuAction( + label: const Text('Edit Reminder'), + leading: Icon(icons.clock20), + onTap: () => _editReminder(context, message, reminder), + ), + StreamContextMenuAction( + label: const Text('Remove from later'), + leading: Icon(icons.checkmark20), + onTap: () => _removeReminder(context, message), + ), + ]; + } + + return [ + StreamContextMenuAction( + label: const Text('Remind me'), + leading: Icon(icons.bell20), + onTap: () => _createReminder(context, message), + ), + StreamContextMenuAction( + label: const Text('Save for later'), + leading: Icon(icons.file20), + onTap: () => _createBookmark(context, message), + ), + ]; + } + + static Future _editReminder( + BuildContext context, + 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; + return client.updateReminder(message.id, remindAt: option.remindAt).ignore(); + } + + static Future _removeReminder( + BuildContext context, + Message message, + ) async { + final client = StreamChat.of(context).client; + return client.deleteReminder(message.id).ignore(); + } + + static Future _createReminder( + BuildContext context, + Message message, + ) async { + final reminder = await showDialog( + context: context, + builder: (_) => const CreateReminderDialog(), + ); + + if (reminder == null) return; + final client = StreamChat.of(context).client; + return client.createReminder(message.id, remindAt: reminder.remindAt).ignore(); + } + + static Future _createBookmark( + BuildContext context, + Message message, + ) async { + final client = StreamChat.of(context).client; + return client.createReminder(message.id).ignore(); + } +} + +// --------------------------------------------------------------------------- +// Delete-for-me action +// --------------------------------------------------------------------------- + +abstract final class _DeleteForMeAction { + static List build( + BuildContext context, + Message message, + ) { + final icons = context.streamIcons; + final channel = StreamChannel.of(context).channel; + final currentUser = StreamChat.of(context).currentUser; + final isSentByCurrentUser = message.user?.id == currentUser?.id; + if (!isSentByCurrentUser || !channel.canDeleteOwnMessage) return const []; + + return [ + StreamContextMenuAction.destructive( + label: const Text('Delete Message for Me'), + leading: Icon(icons.delete20), + onTap: () => _confirmAndDelete(context, message), + ), + ]; + } + + static Future _confirmAndDelete( + BuildContext context, + Message message, + ) async { + final confirmed = await showStreamDialog( + context: context, + builder: (context) => const StreamMessageActionConfirmationModal( + isDestructiveAction: true, + title: Text('Delete for me'), + content: Text('Are you sure you want to delete this message for you?'), + cancelActionTitle: Text('Cancel'), + confirmActionTitle: Text('Delete'), + ), + ); + + if (confirmed != true) return; + final channel = StreamChannel.of(context).channel; + return channel.deleteMessageForMe(message).ignore(); + } +} + +// --------------------------------------------------------------------------- +// Message info action +// --------------------------------------------------------------------------- + +abstract final class _MessageInfoAction { + static List build( + BuildContext context, + Message message, + ) { + final icons = context.streamIcons; + final channel = StreamChannel.of(context).channel; + if (channel.config?.deliveryEvents != true) return const []; + + return [ + StreamContextMenuAction( + label: const Text('Message Info'), + leading: Icon(icons.info20), + onTap: () => MessageInfoSheet.show(context: context, message: message), + ), + ]; + } +} 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..979e878aa2 --- /dev/null +++ b/sample_app/lib/widgets/location/location_attachment.dart @@ -0,0 +1,243 @@ +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 .symmetric(horizontal: 8), + 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. + /// + /// Receives the [BuildContext] from the widget tree where the attachment + /// is rendered, along with the [Location] data. This allows showing + /// dialogs or navigating from the correct context. + final void Function(BuildContext context, Location location)? onAttachmentTap; + + @override + bool canHandle(Message message, _) => message.sharedLocation != null; + + @override + Widget build( + BuildContext context, + Message message, + Map> attachments, + ) { + assert(debugAssertCanHandle(message, attachments), ''); + + final user = message.user; + final location = message.sharedLocation!; + return LocationAttachment( + user: user, + sharedLocation: location, + constraints: constraints, + padding: padding, + onLocationTap: switch (onAttachmentTap) { + final onTap? => () => onTap(context, 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 .symmetric(horizontal: 8), + 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: MarkerSize.lg, + showLocateMeButton: false, + coordinates: sharedLocation.coordinates, + markerBuilder: (_, __, size) => LocationUserMarker( + user: user, + size: 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..b6e39b48b9 --- /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: MarkerSize.xl, + coordinates: sharedLocation.coordinates, + markerBuilder: (_, __, size) => LocationUserMarker( + user: message.user, + size: 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..207bc752a8 --- /dev/null +++ b/sample_app/lib/widgets/location/location_picker_dialog.dart @@ -0,0 +1,363 @@ +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/location/location_user_marker.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: MarkerSize.sm, + 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.value / 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, + ), + ), + ], + ), + ), + Icon( + context.streamIcons.chevronRight20, + size: 24, + 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..d1aaecb95a --- /dev/null +++ b/sample_app/lib/widgets/location/location_picker_option.dart @@ -0,0 +1,76 @@ +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: Center( + 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..3c8994ab98 --- /dev/null +++ b/sample_app/lib/widgets/location/location_user_marker.dart @@ -0,0 +1,77 @@ +import 'package:avatar_glow/avatar_glow.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +enum MarkerSize { + xs(20), + sm(24), + md(32), + lg(40), + xl(64) + ; + + const MarkerSize(this.value); + + final double value; +} + +class LocationUserMarker extends StatelessWidget { + const LocationUserMarker({ + super.key, + this.user, + this.size = MarkerSize.lg, + required this.sharedLocation, + }); + + final User? user; + final MarkerSize size; + + 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( + size: _avatarSizeForMarkerSize(size), + user: user, + showOnlineIndicator: false, + ), + ), + ); + + if (sharedLocation.isExpired) return avatar; + + return AvatarGlow( + glowColor: colorTheme.accentPrimary, + child: avatar, + ); + } + + return Icon( + size: size.value, + Icons.person_pin, + color: colorTheme.accentPrimary, + ); + } + + StreamAvatarSize _avatarSizeForMarkerSize( + MarkerSize size, + ) => switch (size) { + .xs => StreamAvatarSize.xs, + .sm => StreamAvatarSize.sm, + .md => StreamAvatarSize.md, + .lg => StreamAvatarSize.lg, + .xl => StreamAvatarSize.xl, + }; +} diff --git a/sample_app/lib/widgets/message_info_sheet.dart b/sample_app/lib/widgets/message_info_sheet.dart index 6f0a8cec57..dda04a2198 100644 --- a/sample_app/lib/widgets/message_info_sheet.dart +++ b/sample_app/lib/widgets/message_info_sheet.dart @@ -158,7 +158,7 @@ class MessageInfoSheet extends StatelessWidget { ), IconButton( iconSize: 32, - icon: const StreamSvgIcon(icon: StreamSvgIcons.close), + icon: Icon(context.streamIcons.xmark32), onPressed: Navigator.of(context).maybePop, color: colorTheme.textHighEmphasis, padding: const EdgeInsets.all(4), @@ -251,11 +251,8 @@ class _UserReadTile extends StatelessWidget { children: [ // User avatar StreamUserAvatar( + size: .lg, user: read.user, - constraints: const BoxConstraints.tightFor( - height: 40, - width: 40, - ), ), const SizedBox(width: 12), @@ -271,9 +268,9 @@ class _UserReadTile extends StatelessWidget { ), // Status icon - StreamSvgIcon( + Icon( + context.streamIcons.checks20, size: 18, - icon: StreamSvgIcons.checkAll, color: switch (isDelivered) { true => theme.colorTheme.textLowEmphasis, false => theme.colorTheme.accentPrimary, diff --git a/sample_app/lib/widgets/search_text_field.dart b/sample_app/lib/widgets/search_text_field.dart index ec3084e47e..0c544c3389 100644 --- a/sample_app/lib/widgets/search_text_field.dart +++ b/sample_app/lib/widgets/search_text_field.dart @@ -20,18 +20,24 @@ class SearchTextField extends StatelessWidget { @override Widget build(BuildContext context) { + final spacing = context.streamSpacing; + final colorScheme = context.streamColorScheme; + final textTheme = context.streamTextTheme; + return Container( - height: 36, + height: 44, decoration: BoxDecoration( - color: StreamChatTheme.of(context).colorTheme.barsBg, + color: Colors.transparent, border: Border.all( - color: StreamChatTheme.of(context).colorTheme.borders, + color: colorScheme.borderDefault, ), borderRadius: BorderRadius.circular(24), ), - margin: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, + margin: EdgeInsets.only( + top: spacing.md, + bottom: spacing.xs, + left: spacing.md, + right: spacing.md, ), child: Row( children: [ @@ -41,27 +47,19 @@ class SearchTextField extends StatelessWidget { controller: controller, onChanged: onChanged, decoration: InputDecoration( - prefixText: ' ', - prefixIconConstraints: BoxConstraints.tight(const Size(40, 24)), + prefixIconConstraints: BoxConstraints.tight(const Size(36, 24)), prefixIcon: Padding( - padding: const EdgeInsets.only( - left: 8, - right: 8, - ), - child: StreamSvgIcon( - icon: StreamSvgIcons.search, - color: - StreamChatTheme.of(context).colorTheme.textHighEmphasis, - size: 24, + padding: .directional(start: spacing.md), + child: Icon( + context.streamIcons.search20, + color: colorScheme.textTertiary, + size: 20, ), ), hintText: hintText, - hintStyle: StreamChatTheme.of(context).textTheme.body.copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(.5)), - contentPadding: EdgeInsets.zero, + hintStyle: textTheme.bodyDefault.copyWith( + color: colorScheme.textTertiary, + ), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.circular(24), @@ -75,7 +73,7 @@ class SearchTextField extends StatelessWidget { child: IconButton( color: Colors.grey, padding: EdgeInsets.zero, - icon: const StreamSvgIcon(icon: StreamSvgIcons.closeSmall), + icon: Icon(context.streamIcons.xmark16), splashRadius: 24, onPressed: () { if (controller!.text.isNotEmpty) { 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..ab75abcff6 --- /dev/null +++ b/sample_app/lib/widgets/simple_map_view.dart @@ -0,0 +1,154 @@ +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:sample_app/widgets/location/location_user_marker.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +typedef MarkerBuilder = + Widget Function( + BuildContext context, + Animation animation, + MarkerSize markerSize, + ); + +class SimpleMapView extends StatefulWidget { + const SimpleMapView({ + super.key, + this.cameraZoom = 15, + this.markerSize = MarkerSize.lg, + required this.coordinates, + this.showLocateMeButton = true, + this.markerBuilder = _defaultMarkerBuilder, + }); + + final double cameraZoom; + + final MarkerSize markerSize; + + final LocationCoordinates coordinates; + + final bool showLocateMeButton; + + final MarkerBuilder markerBuilder; + static Widget _defaultMarkerBuilder(BuildContext context, _, MarkerSize size) { + final theme = StreamChatTheme.of(context); + final iconColor = theme.colorTheme.accentPrimary; + return Icon(size: size.value, 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.value, + width: widget.markerSize.value, + 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/lib/widgets/stream_version.dart b/sample_app/lib/widgets/stream_version.dart index dfafee4cd3..416c9adabf 100644 --- a/sample_app/lib/widgets/stream_version.dart +++ b/sample_app/lib/widgets/stream_version.dart @@ -23,8 +23,7 @@ class StreamVersion extends StatelessWidget { final pubspec = snapshot.data!; final yaml = loadYaml(pubspec); - final streamChatDep = - yaml['packages']['stream_chat_flutter']['version']; + final streamChatDep = yaml['packages']['stream_chat_flutter']['version']; return Text( '${AppLocalizations.of(context).streamSDK} v $streamChatDep', 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 852164a056..30c1638d6e 100644 --- a/sample_app/pubspec.yaml +++ b/sample_app/pubspec.yaml @@ -16,10 +16,11 @@ version: 2.2.0 # 2. Add it to the melos.yaml file for future updates. environment: - sdk: ^3.6.2 - flutter: ">=3.27.4" + sdk: ^3.10.0 + flutter: ">=3.38.1" 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.23.0 - stream_chat_localizations: ^9.23.0 - stream_chat_persistence: ^9.23.0 + stream_chat_flutter: ^10.0.0-beta.13 + stream_chat_localizations: ^10.0.0-beta.13 + stream_chat_persistence: ^10.0.0-beta.13 streaming_shared_preferences: ^2.0.0 uuid: ^4.4.0 video_player: ^2.8.7