* chore(flutter): change BoxFit of VLC player

* chore(flutter): remove unused imports

* chore(flutter sample): tweak breakpoints, ignore some lints

* chore(flutter): minor code readability improvements

* chore(flutter): extract out platform_widget_builder to its own "package"

* fix(flutter): fix a theme bug with PlatformDialog

* chore(persistence): test plugin on macOS

* Generated the macos runner for the example
* Re-generated the android runner because it was using the v1 embedding

* chore(persistence): test plugin on web

* chore(persistence): add Windows runner to example

* chore(persistence): add linux runner to example

* feat(flutter): branded context menus

* chore(flutter): fix bug where lines would be shown in between menu items

* chore(flutter): import/export fixes

* chore(flutter): move file

* chore(flutter): /src/channel

Also refactors several /channel widgets to use class widgets instead of methods. Some methods, however, have been left as those cases do actually produce cleaner code.

chore(flutter): fix bad directory placement

chore(flutter): fix bad directory placement

* chore(flutter): more directory organization

* chore(flutter): navigator improvements

* chore(flutter): void async improvements

* chore(flutter): fix imports/exports

* docs(flutter): documentation updates for /attachment

* docs(flutter): doc updates

For:
* /avatars
* /bottom_sheets
* /channel

* feat(flutter): selectable message text on web & desktop

* Squashed commit of the following:

commit 50b249edb6b2ee2d744e4e00e97f81979026aab4
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 14:55:05 2022 -0400

    chore(flutter): wrapping up docs

commit 7d289ae45e20c0109bcdb4496f7b693e2adc4ef9
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 14:42:09 2022 -0400

    chore(flutter): more docs & typedefs

commit 6b36f6f3864f15b52e441d58ee7e62577ff8f843
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 14:22:34 2022 -0400

    chore(flutter): more doc fixes

commit 1a9a6de3c836e9ee5d6373033f1d9a7da830bddc
Merge: 59188f5d 88abf99f
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 13:17:52 2022 -0400

    Merge branch 'desk_web_dev' into utils

commit 59188f5d9c1dd68edd531086cdfd9d42faf93fc8
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 12:48:16 2022 -0400

    chore(flutter): add todo

commit 9b4a986ca5757f284677eb5335637d11ccb1d642
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 12:43:12 2022 -0400

    chore(flutter): more docs and typedefs updates

commit 8786d2dd82b94f673b36371fb2b33a1ebcc9840a
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 12:14:14 2022 -0400

    chore(flutter): more typedef and doc template updates

commit 9e906beb0e7a054c13a32c8c8c5b0e2a9e27bb21
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 10:46:04 2022 -0400

    chore(flutter): create /uitils and move files

    Fixes subsequent imports/exports

* chore(flutter): update file_selector plugin

* chore(flutter): include utils/typedefs.dart in library export

* Squashed commit of the following:

commit 501228afa614b0d740858dec1ebedd3f671e2c96
Author: GroovinChip <[email protected]>
Date:   Thu Mar 17 16:18:12 2022 -0400

    feat(flutter): ensure fullscreen media feature works on ALL platforms

commit cd0aab92bc7d90af03258829a5aae66125578af5
Author: GroovinChip <[email protected]>
Date:   Thu Mar 17 14:26:48 2022 -0400

    feat(flutter): working conditional import for initializing VLC

* fix(flutter): remove stray import causing web to break

* chore(flutter): ensure context menus can show on web in sample

* feat(flutter) FullScreenMedia context menus for Web

* feat(flutter): branded context menus

* chore(flutter): no success dialog on file download (web)

* chore(flutter): remove platform UI packages

* chore(flutter): properly show/hide delete message menu item

Also deletes deprecated PlatformDialog

* feat(flutter): 'view info' channel preview context menu

* feat(flutter): more menu actions for ChannelPreview

Also new ConfirmationDialog and add missing docs

* feat(flutter/localizations): translations related to channel mutes

* chore(flutter): remove prints and fix super.didChangeDependencies order

* chore(flutter): rename some things in ChannelBottomSheet

* chore(flutter): more sensible name in TypingIndicator

* chore(flutter): remove unused code in MessageWidget

* feat(flutter): ChannelInfoDialog

* chore(flutter): organize files more

Also refactors out some internal MessageListView widgets

* fix(flutter): fix render overflow issue

* chore(flutter): move MediaListView

* chore(flutter): remove unused imports

* fix(flutter): fix weird share menu placement on macos

* chore(flutter): clean up fullscreenmedia conditional imports

* chore(flutter): adjust package exports

* chore(flutter): update dart_vlc

* chore(flutter): clean up platform checks

* chore(flutter): run flutter format on lib

* chore(flutter): minor sample cleanup

* chore(flutter): ensure vlc doesn't get initialized in non-desktop testing environments

* test(flutter): reformat existing tests

* fix(flutter): pass onThreadReplyTap down to ThreadReplyButton

* First round of new tests

commit a90bed427521123fdaa4c5e276744a224fac2b68
Author: GroovinChip <[email protected]>
Date:   Thu Apr 7 12:34:19 2022 -0400

    test(flutter): Username test

commit d7bc179c20eae2713c9248191518bf1d955104a9
Author: GroovinChip <[email protected]>
Date:   Thu Apr 7 12:30:47 2022 -0400

    chore(flutter): remove unused imports

commit 09c9517de2917dc5ccd7de019b50a7875386991c
Author: GroovinChip <[email protected]>
Date:   Thu Apr 7 12:29:58 2022 -0400

    test(flutter): ThreadSeparator test

commit 9422d1129211aaaac9a51ca4d29fe94bde06593c
Author: GroovinChip <[email protected]>
Date:   Thu Apr 7 12:19:43 2022 -0400

    chore(flutter): rename LoadingIndicator parameter

commit 63af943a1a16e78a1e0377d76176420bd7e1fbc7
Author: GroovinChip <[email protected]>
Date:   Thu Apr 7 11:47:46 2022 -0400

    test(flutter): FloatingDateDivider test

commit d8e67cf6fca2b76ac091a9047104c0910d5fdfef
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 15:19:39 2022 -0400

    test(flutter): InputAttachment tests

commit d2b7d5dd3304c8be6f74bf5b0281d7a95dd027ec
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 14:41:40 2022 -0400

    test(flutter): DmCheckbox tests

commit 01567f235a2f00880bb530b232489e3790159b25
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 14:16:08 2022 -0400

    chore(flutter): remove unused imports

commit c9625858e840df6082506ae9374f1d2710053cbd
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 14:15:44 2022 -0400

    test(flutter): CountdownButton tests

commit d770474bb11ee8b36a3b0e18db31695fd812a6b3
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 14:11:37 2022 -0400

    test(flutter): CommandButton tests

commit 95e63706d1dd971373d39d471894a38bb2a634f2
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 14:07:43 2022 -0400

    test(flutter): ClearInputItem tests

commit 4156586945c91408cb7943b6c57c2ae560a2857c
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 14:03:59 2022 -0400

    test(flutter): AttachmentButton tests

commit bba042806326999aabf338636c6e33900de86160
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 13:00:42 2022 -0400

    test(flutter): golden test for SendButton

commit e2cd0f00b9eeb8d81399f6316edb298234de12dc
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 12:56:52 2022 -0400

    chore(flutter): deal with lints

commit b4e045f9cbd54c4d42c3f30acfe72d5f8ccf9d82
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 12:56:00 2022 -0400

    test(flutter): SendButton test

commit 4e3d5db0bddb97e508fcdda2e7272fe31e70808f
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 12:38:21 2022 -0400

    test(flutter): KeyboardShortcutRunner tests

commit 0dfd7a0bd6fd6bc76902e87f62ac29da0b01bac6
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 12:17:30 2022 -0400

    test(flutter): UploadProgressIndicator

commit ec50c8aea716bdc19e4308e367c8331211c61ea3
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 11:47:53 2022 -0400

    test(flutter): SendingIndicator tests

commit e87bd1450fb5bfb713a867c7e831968f76428848
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 11:32:35 2022 -0400

    test(flutter): more gallery tests

commit 93d46ca649277fe6d289a70f0bdd23584fd0ab94
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 11:17:29 2022 -0400

    test(flutter): remove bad mock

commit c7b03a39f0f3ca7a334dfb908f3473b5e4deab38
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 10:45:45 2022 -0400

    chore(flutter): minor refactor

commit 33aeb7b5ed398287cb930f3b4702f47a82fce2c6
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 09:49:07 2022 -0400

    test(flutter): MessageDialog tests

commit 220b3ae591e135d9606254666e04d4771945d0ae
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 09:42:51 2022 -0400

    test(flutter): DeleteMessageDialog tests

commit db508478bb96749971f563de87b6c2a49e1e4e57
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 14:46:41 2022 -0400

    test(flutter): ConfirmationDialog tests

commit e8172dad6bfe18f40b65554ef2c6c16d7437cb56
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 14:35:50 2022 -0400

    test(flutter): ChannelInfoDialog test

commit 04c21458a601af3c3f4a5fc5ce9e8b61493c208c
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 13:57:02 2022 -0400

    test(flutter): DownloadMenuItem tests

commit afee1dace543e2fef8543f9b035f684b2ee00f3b
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 13:53:21 2022 -0400

    test(flutter): StreamChatContextMenuItem tests

commit 4616b1a973d0cf5224b5a71914c9908339c8462a
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 13:02:07 2022 -0400

    test(flutter): golden test for ErrorAlertSheet

commit 22fb8241ae333f57ade8e8550f87695929836b2b
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 12:59:01 2022 -0400

    test(flutter): golden test for EditMessageSheet

commit 0be79964827f344ba66e99cb9e5bd3d627aed01d
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 12:56:08 2022 -0400

    test(flutter): golden test for AttachmentModalSheet

commit 0ae842a98143478885bc211c788353c8bca57b40
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 12:53:26 2022 -0400

    test(flutter): GroupAvatar tests

commit 697d294c6c7e1bba1d9089a53d5c60d8609ea64e
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 12:30:25 2022 -0400

    test(flutter): UserAvatar tests

commit c03ba4124068c83d3a49c4db00ca18a659801c23
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 11:04:55 2022 -0400

    test(flutter): ErrorAlertSheet test

commit 3414f28530b0e22614b83af20361f21dd4967fae
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 10:45:37 2022 -0400

    test(flutter): EditMessageSheet test

commit b2962b641dfbf14c11d3da7f80a4be27f9980d90
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 10:37:18 2022 -0400

    test(flutter): AttachmentModalSheet test

    test(flutter): AttachmentModalSheet test

    chore(flutter): update exports

commit 7d7e278692c65dea84e7ef3b2d3c1311ebb28408
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 09:52:29 2022 -0400

    test(flutter): more organization

commit 4479b649835acc62d8a0553f9505a0af8077abfc
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 09:33:06 2022 -0400

    test(flutter): tweak AttachmentHandler tests

commit b4a41106567e3c08105ce8dc272b1ebf40b3b0bb
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 09:24:00 2022 -0400

    chore(flutter): streamline attachment handling

    * MobileAttachmentHandler for streamlining related code for mobile
    * Break out DesktopAttachmentHandler into its own file
    * Update minSdk version in Android runner

commit bfb06d31b79cd941d8b611c7e89e2d7728a394cb
Merge: a44b3b39 15c83d03
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 13:00:27 2022 -0400

    Merge branch 'desk_web_dev' into tests

commit a44b3b390adc25a00d1597bb1460973e8555fa9e
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:59:14 2022 -0400

    chore(flutter): start cleaning up test dir

commit de104a3491d942d12f72dd66152b62c6e8b10a5b
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:56:48 2022 -0400

    test(flutter): VideoAttachment test

commit 4748a1c081a3272f7fdbdeb69c501344e221e01e
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:52:30 2022 -0400

    test(flutter): UrlAttachment test

commit 54408ef70f07c60adf8350553fdf3173e5edd7c7
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:48:24 2022 -0400

    test(flutter): ImageGroup test

commit 811cff069e490c6bd03be0c0ac2f5ec86fb990e9
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:46:04 2022 -0400

    test(flutter): ImageAttachment test

commit a72677976c6524e76793432b0520e99cac4e7a62
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:34:06 2022 -0400

    test(flutter): GiphyAttachment test

commit 32ddac022e2c149726b4b6607f41850d556055b8
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:28:01 2022 -0400

    test(flutter): FileAttachment test

commit 6534958cab775be1fb9bda56e67caa4746aa1502
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:14:25 2022 -0400

    test(flutter): AttachmentError test

commit 718a83c10c4e883ce01c9c002ced24de671d30b9
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:06:43 2022 -0400

    test(flutter): AttachmentUploadStateBuilder test

commit a05108c460e36b2e425e4ec06f8bf408b0c20e97
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 11:44:34 2022 -0400

    chore(flutter): minor cleanup

commit 6e73038ac3714a2bd96f8c587af7f743bf2b2023
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 11:43:23 2022 -0400

    test(flutter): AttachmentTitle test

commit c6ee52ec951877feef31dc1427eec3bfc2bd8fcb
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 11:43:11 2022 -0400

    test(flutter): minor cleanup

commit 09845cd845aa8c546043f4ae04805a8e6f53f61c
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 11:42:52 2022 -0400

    chore(flutter): improve device segmentation & vlc init check

commit 172e6b25a064b5147b7641f29ebff3c920156f70
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 10:37:43 2022 -0400

    test(flutter): attachment_handler tests

commit 5f5a91c70cd7c069f39518f1f50bb74ab484c7e9
Author: GroovinChip <[email protected]>
Date:   Wed Mar 23 13:55:27 2022 -0400

    test(flutter): test PlatformWidgetBuilder & DesktopWidgetBuilder

commit 5a457b7557cb4127b74ee022389a24e5e9150601
Author: GroovinChip <[email protected]>
Date:   Wed Mar 23 13:55:05 2022 -0400

    chore(flutter): make base platform widgets testable

    Also fix environment check in stream_chat.dart

commit 88fb16018103c581236aeebd978e705b223b2243
Author: GroovinChip <[email protected]>
Date:   Wed Mar 23 11:59:19 2022 -0400

    chore(flutter): add readme to conditional_parent_widget library

commit e9307f8a6a5639d59358c245f033f9d9b209fe20
Author: GroovinChip <[email protected]>
Date:   Wed Mar 23 11:54:54 2022 -0400

    test(flutter): ConditionalParentBuilder tests

* chore(flutter): add thumblr dependency

* chore(flutter): fix typos

* chore(flutter): properly order super calls

* feat(flutter) video thumbnails on desktop

* chore(flutter): fix incorrect file name

* chore(flutter): fix docs for generateVideoThumbnail

* chore(flutter): remove incorrect comment

* chore(flutter): run flutter format

* chore(flutter): ignore lint

* chore(flutter): flutter format

* chore(flutter): fix tests

* fix(flutter): fix MessageListView bug caused by rebase

* chore(flutter): migrate desktop stuff to v4/stream_message_input.dart

* chore(flutter): migrate over desktop changes for message replies

* chore(flutter): fix message replies on desktop

* chore(flutter): address lints

* chore(flutter): update dart_vlc

* chore(flutter): melos clean & update photo_view

* fix(ui): port #1118

* fix(ui): use attachmentButtonBuilder

* fix(ui): fix file attachment

* format

* fix(ui): fix message bottom row

* chore(flutter sample): use modern window look

* chore(flutter): pass userAvatarBuilder through to MessageWidgetContent

Also pass onUserAvatarTap & userAvatarBuilder through to UserAvatarTransform

* chore: fix MessageWidget menu actions

* fix(flutter): fix "enter" sending empty messages in StreamMessageInput

* fix(flutter): fix Giphys not loading in quoted messages

* chore(flutter): update thumblr

* feat(flutter): keyboard navigation for gallery items

* fix(ui): fix double date divider

* fix(ui): add scroll to bottom back

* fix(ui): use mouse region

* fix(ui): remove backbutton in desktop example

* fix(ui): rename full screen media widget

* feat(flutter): elevation prop for gallery header

* chore(flutter): update dart_vlc

* fix(flutter): fix "show in chat" showing when it shouldn't

* fix(flutter): fix "show in chat" for mobile

* docs(flutter): minor doc update

* chore(flutter): add onMessageSwiped in example app

* chore(flutter): pass usernameBuilder through to BottomRow

* fix(flutter): hacky fix for chonky link previews

* fix(flutter): fix chonky text messages

* feat(flutter): press "esc" to close gallery

Also refactors KeyboardShortcutRunner API

* chore(flutter): fix innaccurate name

* fix(ui): fix attachment + long text sizing

* add key to channelpage

* fix(ui): better example

* fix format

* fix example

* chore(flutter): revert modern window look

* improvement(flutter): improve `GalleryNavigationItem`

* Make buttons more visible
* Spacing from the sides of the screen

* fix video thumbnail

* fix split view

* fix(ui): fix giphy in thread

* feat(flutter): desktop message reactions

commit 4b880ecb6d4dcbf315a7d41d863174817a9c530a
Author: Reuben Turner <[email protected]>
Date:   Thu Jun 9 12:25:21 2022 -0400

    fix(flutter): fixes for desktop reactions

    * Fix reactions for reverse messages
    * Ensure reactions near the edges of MessageListView show properly

commit f511513f8dbb272397e81d08bad33ee60108d738
Author: Salvatore Giordano <[email protected]>
Date:   Thu Jun 9 16:43:03 2022 +0200

    fix(ui): fix DesktopReactionBuilder and Portal

commit c4885648330552f5212c5ba6b771c0f07023f6ae
Merge: 2b710a2b 72b34c21
Author: Reuben Turner <[email protected]>
Date:   Wed Jun 8 14:12:44 2022 -0400

    Merge branch 'desk_web_dev' into desktop_reactions

commit 2b710a2bb610326123d5b7f75c26caa705114c9b
Author: Reuben Turner <[email protected]>
Date:   Mon Jun 6 12:19:12 2022 -0400

    chore(flutter): add debug props

commit e24dabc1de12da8dce697b6d4e1a3617e9835549
Author: Reuben Turner <[email protected]>
Date:   Mon Jun 6 12:00:03 2022 -0400

    feat(flutter): first pass at desktop reactions popup on mouse hover

* fix(flutter): potential fix for #1199

* fix(flutter): fix non-reversed reactions placement

* refactor(flutter): extract bottom reaction, stacked reaction

with debug props

* fix(flutter): fixes for desktop reactions

* Fix bottom row being rendered above reaction buttons
* Fix padding issues related to avatars

* chore(flutter): add back lost changes from 2b1fb6a

Also removed commented code

* chore(flutter): stacked reactions update

* Show "You" if userId matches the reaction username (per design)
* Ensure long names don't throw off alignment

* chore(flutter): format

* fix(ui): fix mime type generation

* chore(flutter): tweak desktop video

* chore(flutter): add some missing things from develop

* merge develop

* fix merge

* fix merge

* format

* fix tests

* increate test action timeout

* remove test

* refactor(ui,core): remove deprecated code and cleanup

* run builder

* update coverage threasholds

* refactor(ui): remove arrow function from countdown button

* Update packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart

* Update packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart

* update comment

* fix(ui): fix attachment + link solution

* fix(ui): fix attachment + link solution

* chore(flutter) update changelog and version

* chore(localizations): update changelog and versions

* chore(llc,core,ui,persistence,localizations): update changelog and pubspec

* chore(llc,core,ui,persistence,localizations): update changelog and pubspec

* fix example

* fix deps

* fix(ui): fix enter sending the message while in slowmode

* fix(ui): fix reaction list

* fix web

* fix(ui): fix image download when using assetUrl

* fix(ui): fix typo in changelog

* fix(ui): fix gallery navigation

* fix(ui): fix giphy sending size

* fix(ui): add keyboard shorcuts to gallery view

* fix(ui): fix gallery footer

* fix(core): allow scroll notification bubbling in lazy load scroll view

* fix(ui): fix lazy load scroll view

* fix(ui): fix loading to unread position

* fix(ui): fix last message preview

* chore(ui): update changelog

* fix tests

* fix(llc): fix connecting while connecting and disconneting (#1237)

* fix(llc): fix connecting while connecting and disconneting

* chore(llc): update changelog

* chore(llc): add test

* chore(llc): improve test

* fix(llc): _resetRequestFlags regardless of the connection status

* feat(llc): add support for AttachmentFileUploaderProvider. (#1246)

* feat(llc): add support for `AttachmentFileUploaderProvider`.

Signed-off-by: xsahil03x <[email protected]>

* chore(llc): update CHANGELOG.md

Signed-off-by: xsahil03x <[email protected]>

* refactor(llc): Remove deprecated attachmentFileUploader field.

Signed-off-by: xsahil03x <[email protected]>

* port no localization

* fix(ui): fix jiffy locale initialization (#1255)

* fix(ui): fix jiffy locale initialization

* chore(ui): update changelog

* fix(ui): cleanup

* chore(llc,core,ui,persistence,localizations): align changelog

* chore(llc,core,ui,persistence,localizations): add links to 4.x changelogs

* fix(ui): fix date divider not showing (#1259)

* fix(ui): fix date divider not showing

* chore(ui): update changelog

* chore(ui): minor changes

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): fix separator

Signed-off-by: xsahil03x <[email protected]>

Co-authored-by: Sahil Kumar <[email protected]>

* chore(ui): add contextmenu requirements for web (#1261)

* fix unread separator

* feat(ui): `StreamChatConfiguration` class (#1128)

* feat: StreamChatConfiguration class

See #1125 for more details

* chore(flutter): update changelog

* fix analysis

* cleanup

* feat(ui): add enforceUniqueReactions to `StreamChatConfiguration` widget

* add tests

* refactor(ui): rename StreamChatConfigurationProvider and StreamChatConfiguration

* refactor(ui): rename StreamChat.config to streamChatConfigData

* refactor(ui): make enforceUniqueReactions non nullable

* chore(ui): fix analysis errors

* refactor(ui): make config fields non nullable

* chore(ui): format

Co-authored-by: Salvatore Giordano <[email protected]>

* feat(ui, core): add StreamAutocomplete (#1263)

* feat(ui, core): add stream_autocomplete.dart

Signed-off-by: xsahil03x <[email protected]>

* refactor(core, ui): refactor message input controller

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): minor fixes

Signed-off-by: xsahil03x <[email protected]>

* feat(ui): add StreamAutocompleteOptions widget

Signed-off-by: xsahil03x <[email protected]>

* Update packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart

* fix(ui): cleanup

* chore(core, ui): cleanup

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): ignore no empty block

* chore(ui): more cleanup

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): fix command button for bigger screens, other fixes.

Signed-off-by: xsahil03x <[email protected]>

Co-authored-by: Salvatore Giordano <[email protected]>

* fix(ui): increase default file limit to 100MB (#1279)

* fix(ui): increase default file limit to 100MB

* Update packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart

* fix(ui): export kDefaultMaxAttachmentSize and use it in `StreamAttachmentPicker` default

* feat(ui): show dialog after clicking on the camera button and permission is denied (#1262)

* feat(ui): show dialog after clicking on the camera button and permission is denied

* chore(ui): update changelog

* Remove permission_handler plugin and refactor

* cleanup

* cleanup

* add tests

* fix(ui): reset the permission state on resume

* cleanup

* fix concurrent calls

* fix(ui): cleanup

* fix(ui): add try catch

* fix(llc, ui): copy file to tempdir before uploading a file and do not serialize bytes (#1285)

* fix(llc, ui): copy file to tempdir before uploading a file and do not serialize bytes

* fix(llc): do not save failed messages with attachments on web

* fix(llc): replace every with any

* chore(llc): add test

* fix analysis

* fix analysis

* chore(llc,core,ui,persistence): align changelog with v4

* fix(ui): do not linkify system message mentions

* chore(llc,core,ui,persistence,localizations): update changelog and pubspec (#1289)

* chore(ui): update deps

* chore(llc,core,ui,persistence,localizations): update changelog and pubspec

* chore(ui): publish beta.2

* feat(core,ui): add StreamMemberListView, StreamMemberGridView, and StreamMemberListController (#1291)

* feat(core,ui): add StreamMemberListView, StreamMemberGridView, and StreamMemberListController

* chore(core,ui): update changelog

* fix(ui): fix exports

* chore(ui): add tests

* chore(ui): add StreamMemberGridTile typedef

* chore(ui): minor changes

Signed-off-by: xsahil03x <[email protected]>

Co-authored-by: Sahil Kumar <[email protected]>

* fix(ui): do not send empty message

* refactor(ui,localization): removed emoji picker and connected (#1292)

* refactor(ui,localization): removed emoji picker and connected

* chore(ui,localizations): update changelog

* chore(repo): update ui coverage threashold

* doc(ui): add autocomplete_triggers guide

* chore(ui): add tests for isOnlyEmoji

* chore(ui): add tests for isOnlyEmoji

* feat(ui): add `StreamAutocompleteTrigger.triggerOnlyAfterSpace`

* fix(llc): Fix `Channel.removeMessage` not able to remove thread message. (#1294)

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): flutter format

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): fix `stopTyping` unhandled exceptions when network is off or spotty. (#1296)

* feat(llc, ui): Introduce `keyStrokeHandler` to properly handle keyStrokes.

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): update CHANGELOG.md

Signed-off-by: xsahil03x <[email protected]>

* test(llc): add key_stroke_handler_test.dart

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): update CHANGELOG.md

Signed-off-by: xsahil03x <[email protected]>

* fix(core): only handle the channel list event if the state is success.

Signed-off-by: xsahil03x <[email protected]>

* feat(llc): add `hide_history` flag in `channel.addMembers`.

Signed-off-by: xsahil03x <[email protected]>

* refactor(llc): Convert positional argument to optional arguments

Signed-off-by: xsahil03x <[email protected]>

* test(llc): update tests

Signed-off-by: xsahil03x <[email protected]>

* perf: improve image resizing

* fix(core): use copyWith while setting controller items.

Signed-off-by: xsahil03x <[email protected]>

* feat(ui): Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params to `StreamMessageWidget` and `StreamAttachmentPicker` to customize the appearance of image thumbnails.

Signed-off-by: xsahil03x <[email protected]>

* fix analysis

* Release v4.5.0 (#1308)

* chore(llc, ui, core): bump version to v4.5.0

Signed-off-by: xsahil03x <[email protected]>

* chore(ui, core): update CHANGELOG.md

Signed-off-by: xsahil03x <[email protected]>

Signed-off-by: xsahil03x <[email protected]>

* third party video integration

* fix llc

* refactor: clean up and add docs

* chore(llc): update changelog

* chore(llc): add tests

* chore(llc): add more tests

* fix analysis

* chore(llc): add more tests

* chore(llc): add more tests

* fix(llc): only start listening to event once connected.

Signed-off-by: xsahil03x <[email protected]>

* chore(llc): update CHANGELOG.md

Signed-off-by: xsahil03x <[email protected]>

* chore(llc): move files to correct folders.

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): Fix message text hiding because of a flutter bug (#1327)

* fix(ui): remove clip

* chore(ui): update changelog

* cleanup

* fix(ui): hitting "enter" on the android keyboard sends the message instead of going to a new line.

Signed-off-by: xsahil03x <[email protected]>

* feat(ui): add support for additional text field params in message input.

Signed-off-by: xsahil03x <[email protected]>

* fix analysis

* remove print

* chore(llc,core,ui): update pubspec and changelogs (#1333)

* fix(ui): show placeholder for web video thumbs

* fix file download on web

* fix reply and show buttons

* remove tests

* treat svg as files

* fix(ui): change default image resize to clip

* solve some todos

* polish v5 fixing analysis issues

* feat(ui): stream attachment picker bottom sheet (#1336)

* refactor(ui): improve stream_attachment_handler.dart, add stream_attachment_picker_bottom_sheet.dart

Signed-off-by: xsahil03x <[email protected]>

* feat(ui): add copyWith in command_button.dart

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): remove permission_manager.dart

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): some fixes

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): more fixes

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): fix lints, add doc comments

Signed-off-by: xsahil03x <[email protected]>

* chore(ci): update flutter version

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): more fixes

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): more fixes

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): more fixes

Signed-off-by: xsahil03x <[email protected]>

* chore(ci): update melos and flutter version

Signed-off-by: xsahil03x <[email protected]>

Signed-off-by: xsahil03x <[email protected]>
Co-authored-by: Salvatore Giordano <[email protected]>

* remove todo

* update changelog

* update changelogs

* revert

* update coverage threasholds

Signed-off-by: xsahil03x <[email protected]>
Co-authored-by: GroovinChip <[email protected]>
Co-authored-by: Sahil Kumar <[email protected]>
Co-authored-by: Gordon Hayes <[email protected]>
Co-authored-by: Sacha Arbonel <[email protected]>
This commit is contained in:
Salvatore Giordano
2022-09-30 17:55:57 +02:00
committed by GitHub
co-authored by Sahil Kumar GroovinChip Gordon Hayes Sacha Arbonel
parent 3e1d6e91c7
commit 4f2542291c
542 changed files with 28064 additions and 136563 deletions
+18 -8
View File
@@ -1,3 +1,21 @@
## 5.0.0
- Included the changes from version [4.5.0](#450).
✅ Added
- Added `StreamMemberListController`.
## 5.0.0-beta.2
- Included the changes from version [4.4.0](#440) and [4.4.1](#441).
## 5.0.0-beta.1
- Updated `stream_chat` dependency
to [`5.0.0-beta.1`](https://pub.dev/packages/stream_chat/changelog).
- Removed deprecated code.
## 4.6.0
- Updated `stream_chat` dependency to [`4.6.0`](https://pub.dev/packages/stream_chat/changelog).
@@ -5,19 +23,11 @@
## 4.5.0
- Updated `stream_chat` dependency to [`4.5.0`](https://pub.dev/packages/stream_chat/changelog).
🐞 Fixed
- [#1269](https://github.com/GetStream/stream-chat-flutter/issues/1269)
Fix `ChannelListEventHandler` castError at PagedValue.asSuccess.
- [#1241](https://github.com/GetStream/stream-chat-flutter/issues/1241) StreamChannelListView load
more indicator non stop.
✅ Added
- Added setters to update `ChannelListController.filter` and `ChannelListController.sort` properties
in order to support runtime changes.
## 4.4.1
- Updated `stream_chat` dependency to [`4.4.1`](https://pub.dev/packages/stream_chat/changelog).
@@ -319,8 +319,7 @@ class _MessageScreenState extends State<MessageScreen> {
children: [
Expanded(
child: TextField(
controller: messageInputController.textEditingController,
onChanged: (s) => messageInputController.text = s,
controller: messageInputController.textFieldController,
decoration: const InputDecoration(
hintText: 'Enter your message',
),
@@ -332,8 +331,7 @@ class _MessageScreenState extends State<MessageScreen> {
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () async {
if (messageInputController.message.text?.isNotEmpty ==
true) {
if (messageInputController.text.isNotEmpty) {
await channel.sendMessage(
messageInputController.message,
);
@@ -26,7 +26,7 @@ dependencies:
cupertino_icons: ^1.0.3
flutter:
sdk: flutter
stream_chat_flutter_core: ^4.6.0
stream_chat_flutter_core: ^5.0.0-beta.2
dev_dependencies:
flutter_test:
sdk: flutter
@@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
@@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST})
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
@@ -1,261 +0,0 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/better_stream_builder.dart';
import 'package:stream_chat_flutter_core/src/channels_bloc.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
/// [ChannelListCore] is a simplified class that allows fetching a list of
/// channels while exposing UI builders.
/// A [ChannelListController] is used to reload and paginate data.
///
///
/// ```dart
/// class ChannelListPage extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: ChannelListCore(
/// filter: Filter.in_(
/// 'members',
/// [StreamChat.of(context).user!.id],
/// ),
/// sort: [SortOption('last_message_at')],
/// pagination: PaginationParams(
/// limit: 20,
/// ),
/// errorBuilder: (context, err) {
/// return Center(
/// child: Text('An error has occured'),
/// );
/// },
/// emptyBuilder: (context) {
/// return Center(
/// child: Text('Nothing here...'),
/// );
/// },
/// loadingBuilder: (context) {
/// return Center(
/// child: CircularProgressIndicator(),
/// );
/// },
/// listBuilder: (context, list) {
/// return ChannelPage(list);
/// }
/// ),
/// );
/// }
/// }
/// ```
///
/// Make sure to have a [StreamChatCore] ancestor in order to provide the
/// information about the channels.
@Deprecated('''
ChannelListCore is deprecated and will be removed in the next
major version. Use StreamChannelListController instead to create your custom list.
More details here https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter_core/stream_channel_list_controller
''')
class ChannelListCore extends StatefulWidget {
/// Instantiate a new ChannelListView
const ChannelListCore({
super.key,
required this.errorBuilder,
required this.emptyBuilder,
required this.loadingBuilder,
required this.listBuilder,
this.filter,
this.state = true,
this.watch = true,
this.presence = false,
this.memberLimit,
this.messageLimit,
this.sort,
this.channelListController,
this.limit = 25,
});
/// A [ChannelListController] allows reloading and pagination.
/// Use [ChannelListController.loadData] and
/// [ChannelListController.paginateData] respectively for reloading and
/// pagination.
final ChannelListController? channelListController;
/// The builder that will be used in case of error
final ErrorBuilder errorBuilder;
/// The builder that will be used in case of loading
final WidgetBuilder loadingBuilder;
/// The builder which is used when list of channels loads
final Function(BuildContext, List<Channel>) listBuilder;
/// The builder used when the channel list is empty.
final WidgetBuilder emptyBuilder;
/// The query filters to use.
/// You can query on any of the custom fields you've defined on the [Channel].
/// You can also filter other built-in channel fields.
final Filter? filter;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be
/// provided.
/// You can sort based on last_updated, last_message_at, updated_at, created
/// _at or member_count. Direction can be ascending or descending.
final List<SortOption<ChannelModel>>? sort;
/// If true returns the Channel state
final bool state;
/// If true listen to changes to this Channel in real time.
final bool watch;
/// If true youll receive user presence updates via the websocket events
final bool presence;
/// Number of members to fetch in each channel
final int? memberLimit;
/// Number of messages to fetch in each channel
final int? messageLimit;
/// The amount of channels requested per API call.
final int limit;
@override
ChannelListCoreState createState() => ChannelListCoreState();
}
/// The current state of the [ChannelListCore].
class ChannelListCoreState extends State<ChannelListCore> {
late ChannelsBlocState _channelsBloc;
StreamChatCoreState? _streamChatCoreState;
@override
Widget build(BuildContext context) => _buildListView(_channelsBloc);
BetterStreamBuilder<List<Channel>> _buildListView(
ChannelsBlocState channelsBlocState,
) =>
BetterStreamBuilder<List<Channel>>(
stream: channelsBlocState.channelsStream,
errorBuilder: widget.errorBuilder,
noDataBuilder: widget.loadingBuilder,
builder: (context, channels) {
if (channels.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, channels);
},
);
/// Fetches initial channels and updates the widget
Future<void> loadData() => _channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
state: widget.state,
watch: widget.watch,
presence: widget.presence,
memberLimit: widget.memberLimit,
messageLimit: widget.messageLimit,
paginationParams: PaginationParams(limit: widget.limit, offset: 0),
);
/// Fetches more channels with updated pagination and updates the widget
Future<void> paginateData() => _channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
state: widget.state,
watch: widget.watch,
presence: widget.presence,
memberLimit: widget.memberLimit,
messageLimit: widget.messageLimit,
paginationParams: PaginationParams(
limit: widget.limit,
offset: _channelsBloc.channels?.length ?? 0,
),
);
StreamSubscription<Event>? _subscription;
@override
void initState() {
super.initState();
_setupController();
}
@override
void didChangeDependencies() {
_channelsBloc = ChannelsBloc.of(context);
final newStreamChatCoreState = StreamChatCore.of(context);
if (newStreamChatCoreState != _streamChatCoreState) {
_streamChatCoreState = newStreamChatCoreState;
loadData();
final client = _streamChatCoreState!.client;
_subscription?.cancel();
_subscription = client
.on(
EventType.connectionRecovered,
EventType.notificationAddedToChannel,
EventType.notificationMessageNew,
EventType.channelVisible,
)
.listen((event) => loadData());
}
super.didChangeDependencies();
}
@override
void didUpdateWidget(ChannelListCore oldWidget) {
super.didUpdateWidget(oldWidget);
if (jsonEncode(widget.filter) != jsonEncode(oldWidget.filter) ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.state != oldWidget.state ||
widget.watch != oldWidget.watch ||
widget.presence != oldWidget.presence ||
widget.messageLimit != oldWidget.messageLimit ||
widget.memberLimit != oldWidget.memberLimit ||
widget.limit != oldWidget.limit) {
loadData();
}
if (widget.channelListController != oldWidget.channelListController) {
_setupController();
}
}
void _setupController() {
if (widget.channelListController != null) {
widget.channelListController!.loadData = loadData;
widget.channelListController!.paginateData = paginateData;
}
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
}
/// Controller used for loading more data and controlling pagination in
/// [ChannelListCore].
class ChannelListController {
/// This function calls Stream's servers to load a list of channels.
/// If there is existing data, calling this function causes a reload.
AsyncCallback? loadData;
/// This function is used to load another page of data. Note, [loadData]
/// should be used to populate the initial page of data. Calling
/// [paginateData] performs a query to load subsequent pages.
AsyncCallback? paginateData;
}
@@ -1,253 +0,0 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/channel_list_core.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart';
/// Widget dedicated to the management of a channel list with pagination
/// [ChannelsBloc] is used together with [ChannelListCore] to manage a list of
/// [Channel]s with pagination, re-ordering, querying and other operations
/// associated with [Channel]s.
///
/// [ChannelsBloc] can be access at anytime by using the static [of] method
/// using Flutter's [BuildContext].
///
/// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/
@Deprecated("Use 'StreamChannelListController' instead")
class ChannelsBloc extends StatefulWidget {
/// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and
/// not null.
const ChannelsBloc({
super.key,
required this.child,
this.lockChannelsOrder = false,
this.channelsComparator,
this.shouldAddChannel,
});
/// The widget child
final Widget child;
/// Set this to true to prevent channels to be brought to the top of the list
/// when a new message arrives
final bool lockChannelsOrder;
/// Comparator used to sort the channels when a message.new event is received
final Comparator<Channel>? channelsComparator;
/// Function used to evaluate if a channel should be added to the list when a
/// message.new event is received
final bool Function(Event)? shouldAddChannel;
@override
ChannelsBlocState createState() => ChannelsBlocState();
/// Use this method to get the current [ChannelsBlocState] instance
static ChannelsBlocState of(BuildContext context) {
ChannelsBlocState? streamChatState;
streamChatState = context.findAncestorStateOfType<ChannelsBlocState>();
assert(
streamChatState != null,
'You must have a ChannelsBloc widget as ancestor',
);
return streamChatState!;
}
}
/// The current state of the [ChannelsBloc].
class ChannelsBlocState extends State<ChannelsBloc>
with AutomaticKeepAliveClientMixin {
StreamChatCoreState? _streamChatCoreState;
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
/// The current channel list
List<Channel>? get channels => _channelsController.valueOrNull;
/// The current channel list as a stream
Stream<List<Channel>> get channelsStream => _channelsController.stream;
final _queryChannelsLoadingController = BehaviorSubject.seeded(false);
final BehaviorSubject<List<Channel>> _channelsController =
BehaviorSubject<List<Channel>>();
/// The stream notifying the state of queryChannel call
Stream<bool> get queryChannelsLoading =>
_queryChannelsLoadingController.stream;
final List<Channel> _hiddenChannels = [];
bool _paginationEnded = false;
final List<StreamSubscription> _subscriptions = [];
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream
Future<void> queryChannels({
Filter? filter,
List<SortOption<ChannelModel>>? sortOptions,
bool state = true,
bool watch = true,
bool presence = false,
int? memberLimit,
int? messageLimit,
bool waitForConnect = true,
PaginationParams paginationParams = const PaginationParams(limit: 30),
}) async {
final client = _streamChatCoreState!.client;
final offset = paginationParams.offset;
final clear = offset == null || offset == 0;
if (clear && _paginationEnded) {
_paginationEnded = false;
}
if ((!clear && _paginationEnded) || _queryChannelsLoadingController.value) {
return;
}
if (_channelsController.hasValue) {
_queryChannelsLoadingController.safeAdd(true);
}
try {
final oldChannels = List<Channel>.from(channels ?? []);
var newChannels = <Channel>[];
await for (final channels in client.queryChannels(
filter: filter,
sort: sortOptions,
state: state,
watch: watch,
presence: presence,
memberLimit: memberLimit,
messageLimit: messageLimit,
waitForConnect: waitForConnect,
paginationParams: paginationParams,
)) {
newChannels = channels;
if (clear) {
_channelsController.safeAdd(channels);
} else {
final temp = oldChannels + channels;
_channelsController.safeAdd(temp);
}
if (_channelsController.hasValue &&
_queryChannelsLoadingController.value) {
_queryChannelsLoadingController.safeAdd(false);
}
}
if (newChannels.isEmpty || newChannels.length < paginationParams.limit) {
_paginationEnded = true;
}
} catch (e, stk) {
// reset loading controller
_queryChannelsLoadingController.safeAdd(false);
if (_channelsController.hasValue) {
_queryChannelsLoadingController.safeAddError(e, stk);
} else {
_channelsController.safeAddError(e, stk);
}
}
}
@override
void didChangeDependencies() {
final newStreamChatCoreState = StreamChatCore.of(context);
if (newStreamChatCoreState != _streamChatCoreState) {
_streamChatCoreState = newStreamChatCoreState;
final client = _streamChatCoreState!.client;
_cancelSubscriptions();
if (!widget.lockChannelsOrder) {
_subscriptions.add(client
.on(
EventType.messageNew,
)
.listen((e) {
if (e.message?.parentId != null && e.message?.showInChannel != true) {
return;
}
final newChannels = List<Channel>.from(channels ?? []);
final index = newChannels.indexWhere((c) => c.cid == e.cid);
if (index != -1) {
if (index > 0) {
final channel = newChannels.removeAt(index);
newChannels.insert(0, channel);
}
} else if (widget.shouldAddChannel?.call(e) == true) {
final hiddenIndex =
_hiddenChannels.indexWhere((c) => c.cid == e.cid);
if (hiddenIndex != -1) {
newChannels.insert(0, _hiddenChannels[hiddenIndex]);
_hiddenChannels.removeAt(hiddenIndex);
} else {
if (client.state.channels[e.cid] != null) {
newChannels.insert(0, client.state.channels[e.cid]!);
}
}
}
if (widget.channelsComparator != null) {
newChannels.sort(widget.channelsComparator);
}
_channelsController.safeAdd(newChannels);
}));
}
_subscriptions
..add(client.on(EventType.channelHidden).listen((event) async {
final newChannels = List<Channel>.from(channels ?? []);
final channelIndex =
newChannels.indexWhere((c) => c.cid == event.cid);
if (channelIndex > -1) {
final channel = newChannels.removeAt(channelIndex);
_hiddenChannels.add(channel);
_channelsController.safeAdd(newChannels);
}
}))
..add(client
.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)
.listen((e) {
final channel = e.channel;
_channelsController.safeAdd(List.from(
(channels ?? [])..removeWhere((c) => c.cid == channel?.cid),
));
}));
}
super.didChangeDependencies();
}
@override
void dispose() {
_channelsController.close();
_queryChannelsLoadingController.close();
_cancelSubscriptions();
super.dispose();
}
void _cancelSubscriptions() {
_subscriptions
..forEach((s) => s.cancel())
..clear();
}
@override
bool get wantKeepAlive => true;
}
@@ -1,170 +0,0 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart';
/// [MessageSearchBloc] is used to manage a list of messages with pagination.
/// This class can be used to load messages, perform queries, etc.
///
/// [MessageSearchBloc] can be access at anytime by using the static [of] method
/// using Flutter's [BuildContext].
///
/// API docs: https://getstream.io/chat/docs/flutter-dart/send_message/
@Deprecated("Use 'StreamMessageSearchListController' instead")
class MessageSearchBloc extends StatefulWidget {
/// Instantiate a new MessageSearchBloc
const MessageSearchBloc({
super.key,
required this.child,
});
/// The widget child
final Widget child;
@override
MessageSearchBlocState createState() => MessageSearchBlocState();
/// Use this method to get the current [MessageSearchBlocState] instance
static MessageSearchBlocState of(BuildContext context) {
MessageSearchBlocState? state;
state = context.findAncestorStateOfType<MessageSearchBlocState>();
assert(
state != null,
'You must have a MessageSearchBloc widget as ancestor',
);
return state!;
}
}
/// The current state of the [MessageSearchBloc]
class MessageSearchBlocState extends State<MessageSearchBloc>
with AutomaticKeepAliveClientMixin {
late StreamChatCoreState _streamChatCoreState;
/// The key used to paginate next items.
String? nextId;
/// The key used to paginate previous items.
String? previousId;
/// The current messages list
List<GetMessageResponse>? get messageResponses =>
_messageResponses.valueOrNull;
/// The current messages list as a stream
Stream<List<GetMessageResponse>> get messagesStream =>
_messageResponses.stream;
final _messageResponses = BehaviorSubject<List<GetMessageResponse>>();
final _queryMessagesLoadingController = BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryMessagesLoading =>
_queryMessagesLoadingController.stream;
bool _paginationEnded = false;
/// Calls [StreamChatClient.search] updating
/// [messagesStream] and [queryMessagesLoading] stream
Future<void> search({
required Filter filter,
Filter? messageFilter,
List<SortOption>? sort,
String? query,
PaginationParams pagination = const PaginationParams(limit: 30),
}) async {
final client = _streamChatCoreState.client;
var clear = false;
if (sort != null) {
clear |= pagination.next == null;
} else {
final offset = pagination.offset;
clear |= offset == null || offset == 0;
}
if (clear && _paginationEnded) {
_paginationEnded = false;
}
if ((!clear && _paginationEnded) || _queryMessagesLoadingController.value) {
return;
}
if (_messageResponses.hasValue) {
_queryMessagesLoadingController.safeAdd(true);
}
try {
final oldMessages = List<GetMessageResponse>.from(messageResponses ?? []);
final response = await client.search(
filter,
sort: sort,
query: query,
paginationParams: pagination,
messageFilters: messageFilter,
);
final next = response.next;
final previous = response.previous;
nextId = next != null && next.isNotEmpty
? next
: /*reset nextId if we get nothing*/ null;
previousId = previous != null && previous.isNotEmpty
? previous
: /*reset previousId if we get nothing*/ null;
final newMessages = response.results;
if (clear) {
_messageResponses.safeAdd(newMessages);
} else {
final temp = oldMessages + newMessages;
_messageResponses.safeAdd(temp);
}
if (_messageResponses.hasValue && _queryMessagesLoadingController.value) {
_queryMessagesLoadingController.safeAdd(false);
}
if (newMessages.isEmpty || newMessages.length < pagination.limit) {
_paginationEnded = true;
}
} catch (e, stk) {
// reset loading controller
_queryMessagesLoadingController.safeAdd(false);
if (_messageResponses.hasValue) {
_queryMessagesLoadingController.safeAddError(e, stk);
} else {
_messageResponses.safeAddError(e, stk);
}
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
@override
void didChangeDependencies() {
_streamChatCoreState = StreamChatCore.of(context);
super.didChangeDependencies();
}
@override
void dispose() {
_messageResponses.close();
_queryMessagesLoadingController.close();
super.dispose();
}
@override
bool get wantKeepAlive => true;
}
@@ -1,236 +0,0 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/better_stream_builder.dart';
import 'package:stream_chat_flutter_core/src/message_search_bloc.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
///
/// [MessageSearchListCore] is a simplified class that allows searching for
/// messages across channels while exposing UI builders.
/// A [MessageSearchListController] is used to load and paginate data.
///
/// ```dart
/// class MessageSearchPage extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: MessageSearchListCore(
/// messageQuery: _messageFilter,
/// filters: _channelsFilter,
/// limit: 20,
/// ),
/// );
/// }
/// }
/// ```
///
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the
/// information about the messages.
/// The widget uses a [ListView.separated] to render the list of messages.
///
@Deprecated('''
MessageSearchListCore is deprecated and will be removed in the next
major version. Use StreamMessageSearchListController instead to create your custom list.
More details here https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter_core/stream_message_search_list_controller
''')
class MessageSearchListCore extends StatefulWidget {
/// Instantiate a new [MessageSearchListView].
/// The following parameters must be supplied and not null:
/// * [emptyBuilder]
/// * [errorBuilder]
/// * [loadingBuilder]
/// * [childBuilder]
MessageSearchListCore({
super.key,
required this.emptyBuilder,
required this.errorBuilder,
required this.loadingBuilder,
required this.childBuilder,
required this.filters,
this.messageQuery,
this.sortOptions,
@Deprecated(
"'pagination' is deprecated and shouldn't be used. "
"This property is no longer used, Please use 'limit' instead",
)
this.paginationParams,
this.messageFilters,
this.messageSearchListController,
int? limit,
}) : assert(
messageQuery != null || messageFilters != null,
'Provide at least `query` or `messageFilters`',
),
assert(
messageQuery == null || messageFilters == null,
"Can't provide both `query` and `messageFilters` at the same time",
),
assert(
paginationParams?.offset == null ||
paginationParams?.offset == 0 ||
sortOptions == null,
'Cannot specify `offset` with `sortOptions` parameter',
),
limit = limit ?? paginationParams?.limit ?? 30;
/// A [MessageSearchListController] allows reloading and pagination.
/// Use [MessageSearchListController.loadData] and
/// [MessageSearchListController.paginateData] respectively for reloading and
/// pagination.
final MessageSearchListController? messageSearchListController;
/// Message String to search on
final String? messageQuery;
/// The query filters to use.
/// You can query on any of the custom fields you've defined on the [Channel].
/// You can also filter other built-in channel fields.
final Filter filters;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be
/// provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_
/// at or member_count. Direction can be ascending or descending.
final List<SortOption>? sortOptions;
/// Pagination parameters
/// limit: the number of messages to return (max is 30)
/// offset: the offset (max is 1000)
@Deprecated(
"'pagination' is deprecated and shouldn't be used. "
"This property is no longer used, Please use 'limit' instead",
)
final PaginationParams? paginationParams;
/// The amount of messages requested per API call.
final int limit;
/// The message query filters to use.
/// You can query on any of the custom fields you've defined on the [Channel].
/// You can also filter other built-in channel fields.
final Filter? messageFilters;
/// The builder that is used when the search messages are fetched
final Widget Function(List<GetMessageResponse>) childBuilder;
/// The builder used when the channel list is empty.
final WidgetBuilder emptyBuilder;
/// The builder that will be used in case of error
final ErrorBuilder errorBuilder;
/// The builder that will be used in case of loading
final WidgetBuilder loadingBuilder;
@override
MessageSearchListCoreState createState() => MessageSearchListCoreState();
}
/// The current state of the [MessageSearchListCore].
class MessageSearchListCoreState extends State<MessageSearchListCore> {
MessageSearchBlocState? _messageSearchBloc;
@override
void didChangeDependencies() {
final newMessageSearchBloc = MessageSearchBloc.of(context);
if (newMessageSearchBloc != _messageSearchBloc) {
_messageSearchBloc = newMessageSearchBloc;
loadData();
}
super.didChangeDependencies();
}
void _setupController() {
if (widget.messageSearchListController != null) {
widget.messageSearchListController!.loadData = loadData;
widget.messageSearchListController!.paginateData = paginateData;
}
}
@override
void initState() {
super.initState();
_setupController();
}
@override
Widget build(BuildContext context) => _buildListView(_messageSearchBloc!);
Widget _buildListView(MessageSearchBlocState messageSearchBloc) =>
BetterStreamBuilder<List<GetMessageResponse>>(
stream: messageSearchBloc.messagesStream,
errorBuilder: widget.errorBuilder,
noDataBuilder: widget.loadingBuilder,
builder: (context, items) {
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.childBuilder(items);
},
);
/// Fetches initial messages and updates the widget
Future<void> loadData() => _messageSearchBloc!.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
messageFilter: widget.messageFilters,
pagination: PaginationParams(limit: widget.limit),
);
/// Fetches more messages with updated pagination and updates the widget
Future<void> paginateData() {
var pagination = PaginationParams(limit: widget.limit);
if (widget.sortOptions != null) {
pagination = pagination.copyWith(
next: _messageSearchBloc?.nextId,
);
} else {
pagination = pagination.copyWith(
offset: _messageSearchBloc?.messageResponses?.length,
);
}
return _messageSearchBloc!.search(
filter: widget.filters,
sort: widget.sortOptions,
pagination: pagination,
query: widget.messageQuery,
messageFilter: widget.messageFilters,
);
}
@override
void didUpdateWidget(MessageSearchListCore oldWidget) {
super.didUpdateWidget(oldWidget);
if (jsonEncode(widget.filters) != jsonEncode(oldWidget.filters) ||
jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) ||
widget.messageQuery != oldWidget.messageQuery ||
jsonEncode(widget.messageFilters) !=
jsonEncode(oldWidget.messageFilters) ||
widget.limit != oldWidget.limit) {
loadData();
}
if (widget.messageSearchListController !=
oldWidget.messageSearchListController) {
_setupController();
}
}
}
/// Controller used for paginating data in [ChannelListView]
class MessageSearchListController {
/// Call this function to reload data
AsyncCallback? loadData;
/// Call this function to load further data
AsyncCallback? paginateData;
}
@@ -1,5 +1,6 @@
// 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
part of 'paged_value_notifier.dart';
@@ -11,34 +12,7 @@ part of 'paged_value_notifier.dart';
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
class _$PagedValueTearOff {
const _$PagedValueTearOff();
Success<Key, Value> call<Key, Value>(
{required List<Value> items, Key? nextPageKey, StreamChatError? error}) {
return Success<Key, Value>(
items: items,
nextPageKey: nextPageKey,
error: error,
);
}
Loading<Key, Value> loading<Key, Value>() {
return Loading<Key, Value>();
}
Error<Key, Value> error<Key, Value>(StreamChatError error) {
return Error<Key, Value>(
error,
);
}
}
/// @nodoc
const $PagedValue = _$PagedValueTearOff();
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
/// @nodoc
mixin _$PagedValue<Key, Value> {
@@ -112,23 +86,23 @@ class _$PagedValueCopyWithImpl<Key, Value, $Res>
}
/// @nodoc
abstract class $SuccessCopyWith<Key, Value, $Res> {
factory $SuccessCopyWith(
Success<Key, Value> value, $Res Function(Success<Key, Value>) then) =
_$SuccessCopyWithImpl<Key, Value, $Res>;
abstract class _$$SuccessCopyWith<Key, Value, $Res> {
factory _$$SuccessCopyWith(_$Success<Key, Value> value,
$Res Function(_$Success<Key, Value>) then) =
__$$SuccessCopyWithImpl<Key, Value, $Res>;
$Res call({List<Value> items, Key? nextPageKey, StreamChatError? error});
}
/// @nodoc
class _$SuccessCopyWithImpl<Key, Value, $Res>
class __$$SuccessCopyWithImpl<Key, Value, $Res>
extends _$PagedValueCopyWithImpl<Key, Value, $Res>
implements $SuccessCopyWith<Key, Value, $Res> {
_$SuccessCopyWithImpl(
Success<Key, Value> _value, $Res Function(Success<Key, Value>) _then)
: super(_value, (v) => _then(v as Success<Key, Value>));
implements _$$SuccessCopyWith<Key, Value, $Res> {
__$$SuccessCopyWithImpl(
_$Success<Key, Value> _value, $Res Function(_$Success<Key, Value>) _then)
: super(_value, (v) => _then(v as _$Success<Key, Value>));
@override
Success<Key, Value> get _value => super._value as Success<Key, Value>;
_$Success<Key, Value> get _value => super._value as _$Success<Key, Value>;
@override
$Res call({
@@ -136,9 +110,9 @@ class _$SuccessCopyWithImpl<Key, Value, $Res>
Object? nextPageKey = freezed,
Object? error = freezed,
}) {
return _then(Success<Key, Value>(
return _then(_$Success<Key, Value>(
items: items == freezed
? _value.items
? _value._items
: items // ignore: cast_nullable_to_non_nullable
as List<Value>,
nextPageKey: nextPageKey == freezed
@@ -157,20 +131,27 @@ class _$SuccessCopyWithImpl<Key, Value, $Res>
class _$Success<Key, Value> extends Success<Key, Value>
with DiagnosticableTreeMixin {
const _$Success({required this.items, this.nextPageKey, this.error})
: super._();
const _$Success(
{required final List<Value> items, this.nextPageKey, this.error})
: _items = items,
super._();
@override
/// List with all items loaded so far.
final List<Value> _items;
/// List with all items loaded so far.
final List<Value> items;
@override
List<Value> get items {
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_items);
}
/// The key for the next page to be fetched.
final Key? nextPageKey;
@override
final Key? nextPageKey;
/// The current error, if any.
@override
final StreamChatError? error;
@override
@@ -192,8 +173,8 @@ class _$Success<Key, Value> extends Success<Key, Value>
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is Success<Key, Value> &&
const DeepCollectionEquality().equals(other.items, items) &&
other is _$Success<Key, Value> &&
const DeepCollectionEquality().equals(other._items, _items) &&
const DeepCollectionEquality()
.equals(other.nextPageKey, nextPageKey) &&
const DeepCollectionEquality().equals(other.error, error));
@@ -202,14 +183,15 @@ class _$Success<Key, Value> extends Success<Key, Value>
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(items),
const DeepCollectionEquality().hash(_items),
const DeepCollectionEquality().hash(nextPageKey),
const DeepCollectionEquality().hash(error));
@JsonKey(ignore: true)
@override
$SuccessCopyWith<Key, Value, Success<Key, Value>> get copyWith =>
_$SuccessCopyWithImpl<Key, Value, Success<Key, Value>>(this, _$identity);
_$$SuccessCopyWith<Key, Value, _$Success<Key, Value>> get copyWith =>
__$$SuccessCopyWithImpl<Key, Value, _$Success<Key, Value>>(
this, _$identity);
@override
@optionalTypeArgs
@@ -288,41 +270,41 @@ class _$Success<Key, Value> extends Success<Key, Value>
abstract class Success<Key, Value> extends PagedValue<Key, Value> {
const factory Success(
{required List<Value> items,
Key? nextPageKey,
StreamChatError? error}) = _$Success<Key, Value>;
{required final List<Value> items,
final Key? nextPageKey,
final StreamChatError? error}) = _$Success<Key, Value>;
const Success._() : super._();
/// List with all items loaded so far.
List<Value> get items;
List<Value> get items => throw _privateConstructorUsedError;
/// The key for the next page to be fetched.
Key? get nextPageKey;
Key? get nextPageKey => throw _privateConstructorUsedError;
/// The current error, if any.
StreamChatError? get error;
StreamChatError? get error => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$SuccessCopyWith<Key, Value, Success<Key, Value>> get copyWith =>
_$$SuccessCopyWith<Key, Value, _$Success<Key, Value>> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LoadingCopyWith<Key, Value, $Res> {
factory $LoadingCopyWith(
Loading<Key, Value> value, $Res Function(Loading<Key, Value>) then) =
_$LoadingCopyWithImpl<Key, Value, $Res>;
abstract class _$$LoadingCopyWith<Key, Value, $Res> {
factory _$$LoadingCopyWith(_$Loading<Key, Value> value,
$Res Function(_$Loading<Key, Value>) then) =
__$$LoadingCopyWithImpl<Key, Value, $Res>;
}
/// @nodoc
class _$LoadingCopyWithImpl<Key, Value, $Res>
class __$$LoadingCopyWithImpl<Key, Value, $Res>
extends _$PagedValueCopyWithImpl<Key, Value, $Res>
implements $LoadingCopyWith<Key, Value, $Res> {
_$LoadingCopyWithImpl(
Loading<Key, Value> _value, $Res Function(Loading<Key, Value>) _then)
: super(_value, (v) => _then(v as Loading<Key, Value>));
implements _$$LoadingCopyWith<Key, Value, $Res> {
__$$LoadingCopyWithImpl(
_$Loading<Key, Value> _value, $Res Function(_$Loading<Key, Value>) _then)
: super(_value, (v) => _then(v as _$Loading<Key, Value>));
@override
Loading<Key, Value> get _value => super._value as Loading<Key, Value>;
_$Loading<Key, Value> get _value => super._value as _$Loading<Key, Value>;
}
/// @nodoc
@@ -340,13 +322,13 @@ class _$Loading<Key, Value> extends Loading<Key, Value>
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>.loading'));
.add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>.loading'));
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is Loading<Key, Value>);
(other.runtimeType == runtimeType && other is _$Loading<Key, Value>);
}
@override
@@ -433,29 +415,29 @@ abstract class Loading<Key, Value> extends PagedValue<Key, Value> {
}
/// @nodoc
abstract class $ErrorCopyWith<Key, Value, $Res> {
factory $ErrorCopyWith(
Error<Key, Value> value, $Res Function(Error<Key, Value>) then) =
_$ErrorCopyWithImpl<Key, Value, $Res>;
abstract class _$$ErrorCopyWith<Key, Value, $Res> {
factory _$$ErrorCopyWith(
_$Error<Key, Value> value, $Res Function(_$Error<Key, Value>) then) =
__$$ErrorCopyWithImpl<Key, Value, $Res>;
$Res call({StreamChatError error});
}
/// @nodoc
class _$ErrorCopyWithImpl<Key, Value, $Res>
class __$$ErrorCopyWithImpl<Key, Value, $Res>
extends _$PagedValueCopyWithImpl<Key, Value, $Res>
implements $ErrorCopyWith<Key, Value, $Res> {
_$ErrorCopyWithImpl(
Error<Key, Value> _value, $Res Function(Error<Key, Value>) _then)
: super(_value, (v) => _then(v as Error<Key, Value>));
implements _$$ErrorCopyWith<Key, Value, $Res> {
__$$ErrorCopyWithImpl(
_$Error<Key, Value> _value, $Res Function(_$Error<Key, Value>) _then)
: super(_value, (v) => _then(v as _$Error<Key, Value>));
@override
Error<Key, Value> get _value => super._value as Error<Key, Value>;
_$Error<Key, Value> get _value => super._value as _$Error<Key, Value>;
@override
$Res call({
Object? error = freezed,
}) {
return _then(Error<Key, Value>(
return _then(_$Error<Key, Value>(
error == freezed
? _value.error
: error // ignore: cast_nullable_to_non_nullable
@@ -490,7 +472,7 @@ class _$Error<Key, Value> extends Error<Key, Value>
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is Error<Key, Value> &&
other is _$Error<Key, Value> &&
const DeepCollectionEquality().equals(other.error, error));
}
@@ -500,8 +482,8 @@ class _$Error<Key, Value> extends Error<Key, Value>
@JsonKey(ignore: true)
@override
$ErrorCopyWith<Key, Value, Error<Key, Value>> get copyWith =>
_$ErrorCopyWithImpl<Key, Value, Error<Key, Value>>(this, _$identity);
_$$ErrorCopyWith<Key, Value, _$Error<Key, Value>> get copyWith =>
__$$ErrorCopyWithImpl<Key, Value, _$Error<Key, Value>>(this, _$identity);
@override
@optionalTypeArgs
@@ -579,11 +561,11 @@ class _$Error<Key, Value> extends Error<Key, Value>
}
abstract class Error<Key, Value> extends PagedValue<Key, Value> {
const factory Error(StreamChatError error) = _$Error<Key, Value>;
const factory Error(final StreamChatError error) = _$Error<Key, Value>;
const Error._() : super._();
StreamChatError get error;
StreamChatError get error => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ErrorCopyWith<Key, Value, Error<Key, Value>> get copyWith =>
_$$ErrorCopyWith<Key, Value, _$Error<Key, Value>> get copyWith =>
throw _privateConstructorUsedError;
}
@@ -19,7 +19,7 @@ enum QueryDirection {
///
/// Use [StreamChannel.of] to get the current [StreamChannelState] instance.
class StreamChannel extends StatefulWidget {
/// Creates a new instance of [StreamChannel]. Both [child] and [client] must
/// Creates a new instance of [StreamChannel]. Both [child] and [channel] must
/// be supplied and not null.
const StreamChannel({
super.key,
@@ -220,8 +220,6 @@ class StreamChannelState extends State<StreamChannel> {
/// Loads channel at specific message
Future<void> loadChannelAtMessage(
String? messageId, {
@Deprecated('before is deprecated, use limit instead') int before = 20,
@Deprecated('after is deprecated, use limit instead') int after = 20,
int limit = 20,
bool preferOffline = false,
}) =>
@@ -231,9 +229,21 @@ class StreamChannelState extends State<StreamChannel> {
preferOffline: preferOffline,
);
/// Loads channel at specific message
Future<void> loadChannelAtTimestamp(
DateTime timestamp, {
int limit = 40,
bool preferOffline = false,
}) =>
_queryAtTimestamp(
timestamp: timestamp,
limit: limit,
preferOffline: preferOffline,
);
Future<ChannelState?> _queryAtMessage({
String? messageId,
int limit = 20,
int limit = 40,
bool preferOffline = false,
}) async {
if (channel.state == null) return null;
@@ -251,28 +261,32 @@ class StreamChannelState extends State<StreamChannel> {
return null;
}
return queryAroundMessage(
messageId,
limit: limit,
return channel.query(
messagesPagination: PaginationParams(
idAround: messageId,
limit: limit,
),
preferOffline: preferOffline,
);
}
///
Future<ChannelState> queryAroundMessage(
String messageId, {
@Deprecated('before is deprecated, use limit instead') int before = 20,
@Deprecated('after is deprecated, use limit instead') int after = 20,
int limit = 20,
Future<ChannelState?> _queryAtTimestamp({
required DateTime timestamp,
int limit = 40,
bool preferOffline = false,
}) =>
channel.query(
messagesPagination: PaginationParams(
idAround: messageId,
limit: limit,
),
preferOffline: preferOffline,
);
}) async {
if (channel.state == null) return null;
channel.state!.isUpToDate = false;
channel.state!.truncate();
return channel.query(
messagesPagination: PaginationParams(
createdAtAround: timestamp.toUtc(),
limit: limit,
),
preferOffline: preferOffline,
);
}
///
Future<ChannelState> queryBeforeMessage(
@@ -349,6 +363,15 @@ class StreamChannelState extends State<StreamChannel> {
}
}
Future<bool> _loadChannelAtTimestamp(DateTime timestamp) async {
try {
await loadChannelAtTimestamp(timestamp);
return true;
} catch (_) {
rethrow;
}
}
@override
void initState() {
super.initState();
@@ -359,6 +382,18 @@ class StreamChannelState extends State<StreamChannel> {
_futures = [widget.channel.initialized];
if (initialMessageId != null) {
_futures.add(_loadChannelAtMessage);
} else if (channel.state != null && channel.state!.unreadCount > 0) {
final read = channel.state!.read.firstWhereOrNull(
(it) => it.user.id == channel.client.state.currentUser?.id,
);
if (read != null &&
!(channel.state!.messages
.any((it) => it.createdAt.compareTo(read.lastRead) > 0) &&
channel.state!.messages
.any((it) => it.createdAt.compareTo(read.lastRead) <= 0))) {
_futures.add(_loadChannelAtTimestamp(read.lastRead));
}
}
}
@@ -383,7 +418,7 @@ class StreamChannelState extends State<StreamChannel> {
future: Future.wait(_futures),
initialData: [
channel.state != null,
if (initialMessageId != null) false,
_futures.length == 1,
],
builder: (context, snapshot) {
if (snapshot.hasError) {
@@ -398,10 +433,9 @@ class StreamChannelState extends State<StreamChannel> {
}
return Center(child: Text(message));
}
final initialized = snapshot.data![0];
// ignore: avoid_bool_literals_in_conditional_expressions
final dataLoaded = initialMessageId == null ? true : snapshot.data![1];
if (widget.showLoading && (!initialized || !dataLoaded)) {
final dataLoaded = snapshot.data?.every((it) => it) == true;
if (widget.showLoading && !dataLoaded) {
return const Center(
child: CircularProgressIndicator(),
);
@@ -50,9 +50,7 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
this.limit = defaultChannelPagedLimit,
this.messageLimit,
this.memberLimit,
}) : _activeFilter = filter,
_activeSort = sort,
_eventHandler = eventHandler ?? StreamChannelListEventHandler(),
}) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(),
super(const PagedValue.loading());
/// Creates a [StreamChannelListController] from the passed [value].
@@ -66,9 +64,7 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
this.limit = defaultChannelPagedLimit,
this.messageLimit,
this.memberLimit,
}) : _activeFilter = filter,
_activeSort = sort,
_eventHandler = eventHandler ?? StreamChannelListEventHandler();
}) : _eventHandler = eventHandler ?? StreamChannelListEventHandler();
/// The client to use for the channels list.
final StreamChatClient client;
@@ -82,7 +78,6 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
///
/// You can also filter other built-in channel fields.
final Filter? filter;
Filter? _activeFilter;
/// The sorting used for the channels matching the filters.
///
@@ -94,7 +89,6 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
///
/// Direction can be ascending or descending.
final List<SortOption<ChannelModel>>? sort;
List<SortOption<ChannelModel>>? _activeSort;
/// If true youll receive user presence updates via the websocket events
final bool presence;
@@ -109,18 +103,6 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
/// Number of members to fetch in each channel.
final int? memberLimit;
/// Allows for the change of filters used for channel queries.
///
/// Use this if you need to support runtime filter changes,
/// through custom filters UI.
set filter(Filter? value) => _activeFilter = value;
/// Allows for the change of sort used for channel queries.
///
/// Use this if you need to support runtime sort changes,
/// through custom sort UI.
set sort(List<SortOption<ChannelModel>>? value) => _activeSort = value;
@override
Future<void> doInitialLoad() async {
final limit = min(
@@ -129,8 +111,8 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
);
try {
await for (final channels in client.queryChannels(
filter: _activeFilter,
sort: _activeSort,
filter: filter,
sort: sort,
memberLimit: memberLimit,
messageLimit: messageLimit,
presence: presence,
@@ -158,8 +140,8 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
try {
await for (final channels in client.queryChannels(
filter: _activeFilter,
sort: _activeSort,
filter: filter,
sort: sort,
memberLimit: memberLimit,
messageLimit: messageLimit,
presence: presence,
@@ -181,15 +163,6 @@ class StreamChannelListController extends PagedValueNotifier<int, Channel> {
}
}
@override
Future<void> refresh({bool resetValue = true}) {
if (resetValue) {
_activeFilter = filter;
_activeSort = sort;
}
return super.refresh(resetValue: resetValue);
}
/// Replaces the previously loaded channels with the passed [channels].
set channels(List<Channel> channels) {
if (value.isSuccess) {
@@ -0,0 +1,154 @@
import 'dart:async';
import 'dart:math';
import 'package:stream_chat/stream_chat.dart' hide Success;
import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart';
/// The default channel page limit to load.
const defaultMemberPagedLimit = 10;
const _kDefaultBackendPaginationLimit = 30;
/// A controller for a member list.
///
/// This class lets you perform tasks such as:
/// * Load initial data.
/// * Load more data using [loadMore].
/// * Replace the previously loaded members.
class StreamMemberListController extends PagedValueNotifier<int, Member> {
/// Creates a Stream member list controller.
///
/// * `client` is the Stream chat client to use for the channels list.
///
/// * `filter` is the query filters to use.
///
/// * `sort` is the sorting used for the members matching the filters.
///
/// * `limit` is the limit to apply to the member list.
StreamMemberListController({
required this.channel,
this.filter,
this.sort,
this.limit = defaultMemberPagedLimit,
}) : _activeFilter = filter,
_activeSort = sort,
super(const PagedValue.loading());
/// Creates a [StreamMemberListController] from the passed [value].
StreamMemberListController.fromValue(
super.value, {
required this.channel,
this.filter,
this.sort,
this.limit = defaultMemberPagedLimit,
}) : _activeFilter = filter,
_activeSort = sort;
/// The client to use for the channels list.
final Channel channel;
/// The query filters to use.
///
/// You can query on any of the custom fields you've defined on the [Member].
///
/// You can also filter other built-in channel fields.
final Filter? filter;
Filter? _activeFilter;
/// The sorting used for the members matching the filters.
///
/// Sorting is based on field and direction, multiple sorting options
/// can be provided.
///
/// Direction can be ascending or descending.
final List<SortOption>? sort;
List<SortOption>? _activeSort;
/// The limit to apply to the member list. The default is set to
/// [defaultMemberPagedLimit].
final int limit;
/// Allows for the change of filters used for member queries.
///
/// Use this if you need to support runtime filter changes,
/// through custom filters UI.
set filter(Filter? value) => _activeFilter = value;
/// Allows for the change of the query sort used for member queries.
///
/// Use this if you need to support runtime sort changes,
/// through custom sort UI.
set sort(List<SortOption>? value) => _activeSort = value;
@override
Future<void> doInitialLoad() async {
final limit = min(
this.limit * defaultInitialPagedLimitMultiplier,
_kDefaultBackendPaginationLimit,
);
try {
final memberResponse = await channel.queryMembers(
filter: _activeFilter,
sort: _activeSort,
pagination: PaginationParams(limit: limit),
);
final members = memberResponse.members;
final nextKey = members.length < limit ? null : members.length;
value = PagedValue(
items: members.where((it) => it.user != null).toList(),
nextPageKey: nextKey,
);
} on StreamChatError catch (error) {
value = PagedValue.error(error);
} catch (error) {
final chatError = StreamChatError(error.toString());
value = PagedValue.error(chatError);
}
}
@override
Future<void> loadMore(int nextPageKey) async {
final previousValue = value.asSuccess;
try {
final memberResponse = await channel.queryMembers(
filter: _activeFilter,
sort: _activeSort,
pagination: PaginationParams(limit: limit, offset: nextPageKey),
);
final members = memberResponse.members;
final previousItems = previousValue.items;
final newItems = previousItems + members;
final nextKey = members.length < limit ? null : newItems.length;
value = PagedValue(
items: newItems.where((it) => it.user != null).toList(),
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<void> refresh({bool resetValue = true}) {
if (resetValue) {
_activeFilter = filter;
_activeSort = sort;
}
return super.refresh(resetValue: resetValue);
}
/// Replaces the previously loaded members with [members] and updates
/// the nextPageKey.
set members(List<Member> members) {
value = PagedValue(
items: members,
nextPageKey: members.length,
);
}
}
@@ -11,7 +11,9 @@ import 'package:stream_chat_flutter_core/src/message_text_field_controller.dart'
/// Pass in a [StreamMessageInputController] as the `valueListenable`.
typedef StreamMessageValueListenableBuilder = ValueListenableBuilder<Message>;
/// {@template stream_chat_flutter.StreamMessageInputController}
/// Controller for storing and mutating a [Message] value.
/// {@endtemplate}
class StreamMessageInputController extends ValueNotifier<Message> {
/// Creates a controller for an editable text field.
///
@@ -50,111 +52,121 @@ class StreamMessageInputController extends ValueNotifier<Message> {
StreamMessageInputController._({
required Message initialMessage,
Map<RegExp, TextStyleBuilder>? textPatternStyle,
}) : _textEditingController = MessageTextFieldController.fromValue(
initialMessage.text == null
? TextEditingValue.empty
: TextEditingValue(
text: initialMessage.text!,
composing: TextRange.collapsed(initialMessage.text!.length),
),
}) : _initialMessage = initialMessage,
_textFieldController = MessageTextFieldController.fromValue(
_textEditingValueFromMessage(initialMessage),
textPatternStyle: textPatternStyle,
),
_initialMessage = initialMessage,
super(initialMessage) {
addListener(_textEditingSyncer);
_textFieldController.addListener(_textFieldListener);
}
void _textEditingSyncer() {
final cleanText = value.command == null
? value.text
: value.text?.replaceFirst('/${value.command} ', '');
/// Returns the controller of the text field linked to this controller.
MessageTextFieldController get textFieldController => _textFieldController;
MessageTextFieldController _textFieldController;
if (cleanText != _textEditingController.text) {
final previousOffset = _textEditingController.value.selection.start;
final previousText = _textEditingController.text;
final diff = (cleanText?.length ?? 0) - previousText.length;
_textEditingController
..text = cleanText ?? ''
..selection = TextSelection.collapsed(
offset: previousOffset + diff,
);
Message _initialMessage;
static TextEditingValue _textEditingValueFromMessage(Message message) {
final messageText = message.text;
var textEditingValue = TextEditingValue.empty;
if (messageText != null) {
textEditingValue = TextEditingValue(
text: messageText,
selection: TextSelection.collapsed(offset: messageText.length),
);
}
return textEditingValue;
}
void _textFieldListener() {
final text = _textFieldController.text;
message = message.copyWith(text: text);
}
/// Returns the current message associated with this controller.
Message get message => value;
/// Returns the controller of the text field linked to this controller.
MessageTextFieldController get textEditingController =>
_textEditingController;
final MessageTextFieldController _textEditingController;
/// Sets the current message associated with this controller.
set message(Message message) => value = message;
/// Returns the text of the message.
String get text => _textEditingController.text;
@override
set value(Message message) {
super.value = message;
Message _initialMessage;
/// Sets the message.
set message(Message message) {
value = message;
// Update text field controller only if message text has changed.
final messageText = message.text;
final textFieldText = _textFieldController.text;
if (messageText != textFieldText) {
textEditingValue = _textEditingValueFromMessage(message);
}
}
/// Sets the message that's being quoted.
set quotedMessage(Message message) {
value = value.copyWith(
quotedMessage: message,
quotedMessageId: message.id,
/// Text of the message.
String get text => _textFieldController.text;
/// Sets the text of the message.
set text(String text) {
_textFieldController.text = text;
}
/// The currently selected [text].
///
/// If the selection is collapsed, then this property gives the offset of the
/// cursor within the text.
TextSelection get selection => _textFieldController.selection;
set selection(TextSelection newSelection) {
_textFieldController.selection = selection;
}
/// Returns the textEditingValue associated with this controller.
TextEditingValue get textEditingValue => _textFieldController.value;
set textEditingValue(TextEditingValue value) {
_textFieldController.value = value;
}
set quotedMessage(Message quotedMessage) {
message = message.copyWith(
quotedMessage: quotedMessage,
quotedMessageId: quotedMessage.id,
);
}
/// Clears the quoted message.
void clearQuotedMessage() {
value = value.copyWith(
message = message.copyWith(
quotedMessageId: null,
quotedMessage: null,
);
}
/// Sets a command for the message.
set command(Command command) {
value = value.copyWith(
command: command.name,
text: '/${command.name} ',
set command(String command) {
// Setting the command should also clear the text and attachments.
message = message.copyWith(
text: '',
attachments: [],
command: command,
);
}
/// Sets the text of the message.
set text(String newText) {
var newTextWithCommand = newText;
if (value.command != null) {
if (!newText.startsWith('/${value.command}')) {
newTextWithCommand = '/${value.command} $newText';
}
}
value = value.copyWith(text: newTextWithCommand);
}
/// Returns the baseOffset of the text field.
int get baseOffset => textEditingController.selection.baseOffset;
/// Returns the start of the selection of the text field.
int get selectionStart => textEditingController.selection.start;
/// Sets the [showInChannel] flag of the message.
set showInChannel(bool newValue) {
value = value.copyWith(showInChannel: newValue);
message = message.copyWith(showInChannel: newValue);
}
/// Returns true if the message is in a thread and
/// should be shown in the main channel as well.
bool get showInChannel => value.showInChannel ?? false;
bool get showInChannel => message.showInChannel ?? false;
/// Returns the attachments of the message.
List<Attachment> get attachments => value.attachments;
List<Attachment> get attachments => message.attachments;
/// Sets the list of [attachments] for the message.
set attachments(List<Attachment> attachments) {
value = value.copyWith(attachments: attachments);
message = message.copyWith(attachments: attachments);
}
/// Adds a new attachment to the message.
@@ -212,11 +224,11 @@ class StreamMessageInputController extends ValueNotifier<Message> {
}
/// Returns the list of mentioned users in the message.
List<User> get mentionedUsers => value.mentionedUsers;
List<User> get mentionedUsers => message.mentionedUsers;
/// Sets the mentioned users.
set mentionedUsers(List<User> users) {
value = value.copyWith(mentionedUsers: users);
message = message.copyWith(mentionedUsers: users);
}
/// Adds a user to the list of mentioned users.
@@ -239,7 +251,7 @@ class StreamMessageInputController extends ValueNotifier<Message> {
mentionedUsers = [];
}
/// Sets the [message], or [value], to empty.
/// Sets the [message], to empty.
///
/// After calling this function, [text], [attachments] and [mentionedUsers]
/// will all be empty.
@@ -250,23 +262,24 @@ class StreamMessageInputController extends ValueNotifier<Message> {
/// this method should only be called between frames, e.g. in response to user
/// actions, not during the build, layout, or paint phases.
void clear() {
value = Message();
_textEditingController.clear();
message = Message();
}
/// Sets the [value] to the initial [Message] value.
/// Sets the [message] to the initial [Message] value.
void reset({bool resetId = true}) {
if (resetId) {
final newId = const Uuid().v4();
_initialMessage = _initialMessage.copyWith(id: newId);
}
value = _initialMessage;
// Reset the message to the initial value.
message = _initialMessage;
}
@override
void dispose() {
removeListener(_textEditingSyncer);
_textEditingController.dispose();
_textFieldController
..removeListener(_textFieldListener)
..dispose();
super.dispose();
}
}
@@ -276,7 +289,7 @@ class StreamMessageInputController extends ValueNotifier<Message> {
///
/// The [StreamMessageInputController] is accessible via the [value] getter.
/// During state restoration,
/// the property will restore [StreamMessageInputController.value]
/// the property will restore [StreamMessageInputController.message]
/// to the value it had when the restoration data it is getting restored from
/// was collected.
class StreamRestorableMessageInputController
@@ -306,5 +319,5 @@ class StreamRestorableMessageInputController
}
@override
String toPrimitives() => json.encode(value.value);
String toPrimitives() => json.encode(value.message);
}
@@ -1,286 +0,0 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
///
/// [UserListCore] is a simplified class that allows fetching users while
/// exposing UI builders.
/// A [UserListController] is used to load and paginate data.
///
/// ```dart
/// class UsersListPage extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: UsersListCore(
/// filter: {
/// 'members': {
/// '\$in': [StreamChat.of(context).user.id],
/// }
/// },
/// sort: [SortOption('last_message_at')],
/// pagination: PaginationParams(
/// limit: 20,
/// ),
/// errorBuilder: (err) {
/// return Center(
/// child: Text('An error has occured'),
/// );
/// },
/// emptyBuilder: (context) {
/// return Center(
/// child: Text('Nothing here...'),
/// );
/// },
/// emptyBuilder: (context) {
/// return Center(
/// child: CircularProgressIndicator(),
/// );
/// },
/// listBuilder: (context, list) {
/// return UsersPage(list);
/// }
/// ),
/// );
/// }
/// }
/// ```
///
/// [UsersBloc] must be the ancestor of this widget. This is necessary since
/// [UserListCore] depends on functionality contained within [UsersBloc].
///
/// The parameters [listBuilder], [loadingBuilder], [emptyBuilder] and
/// [errorBuilder] must all be supplied and not null.
@Deprecated('''
UserListCore is deprecated and will be removed in the next
major version. Use StreamUserListController instead to create your custom list.
More details here https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter_core/stream_user_list_controller
''')
class UserListCore extends StatefulWidget {
/// Instantiate a new [UserListCore]
const UserListCore({
required this.errorBuilder,
required this.emptyBuilder,
required this.loadingBuilder,
required this.listBuilder,
super.key,
this.filter = const Filter.empty(),
this.sort,
this.presence,
this.groupAlphabetically = false,
this.userListController,
this.limit = 30,
});
/// A [UserListController] allows reloading and pagination.
/// Use [UserListController.loadData] and [UserListController.paginateData]
/// respectively for reloading and pagination.
final UserListController? userListController;
/// The builder that will be used in case of error
final ErrorBuilder errorBuilder;
/// The builder that will be used to build the list
final Widget Function(BuildContext context, List<ListItem> users) listBuilder;
/// The builder that will be used for loading
final WidgetBuilder loadingBuilder;
/// The builder used when the channel list is empty.
final WidgetBuilder emptyBuilder;
/// The query filters to use.
/// You can query on any of the custom fields you've defined on the [Channel].
/// You can also filter other built-in channel fields.
final Filter filter;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be
/// provided. You can sort based on last_updated, last_message_at, updated_at,
/// created_at or member_count. Direction can be ascending or descending.
final List<SortOption>? sort;
/// If true youll receive user presence updates via the websocket events
final bool? presence;
/// The amount of users requested per API call.
final int limit;
/// Set it to true to group users by their first character
///
/// defaults to false
final bool groupAlphabetically;
@override
UserListCoreState createState() => UserListCoreState();
}
/// The current state of the [UserListCore].
class UserListCoreState extends State<UserListCore>
with WidgetsBindingObserver {
UsersBlocState? _usersBloc;
@override
void didChangeDependencies() {
final newUsersBloc = UsersBloc.of(context);
if (newUsersBloc != _usersBloc) {
_usersBloc = newUsersBloc;
loadData();
}
super.didChangeDependencies();
}
@override
void initState() {
super.initState();
_setupController();
}
void _setupController() {
if (widget.userListController != null) {
widget.userListController!.loadData = loadData;
widget.userListController!.paginateData = paginateData;
}
}
@override
Widget build(BuildContext context) => _buildListView();
bool get _isListAlreadySorted =>
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
Stream<List<ListItem>> _buildUserStream() => _usersBloc!.usersStream.map(
(users) {
if (widget.groupAlphabetically) {
var temp = users;
if (!_isListAlreadySorted) {
temp = users
..sort((curr, next) => curr.name.compareTo(next.name));
}
final groupedUsers = <String, List<User>>{};
for (final e in temp) {
final alphabet = e.name[0].toUpperCase();
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
}
final items = <ListItem>[];
for (final key in groupedUsers.keys) {
items
..add(ListHeaderItem(key))
..addAll(groupedUsers[key]!.map(ListUserItem.new));
}
return items;
}
return users.map(ListUserItem.new).toList();
},
);
BetterStreamBuilder<List<ListItem>> _buildListView() => BetterStreamBuilder(
stream: _buildUserStream(),
errorBuilder: widget.errorBuilder,
noDataBuilder: widget.loadingBuilder,
builder: (context, items) {
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, items);
},
);
/// Fetches initial users and updates the widget
Future<void> loadData() => _usersBloc!.queryUsers(
filter: widget.filter,
sort: widget.sort,
presence: widget.presence,
pagination: PaginationParams(limit: widget.limit),
);
/// Fetches more users with updated pagination and updates the widget
Future<void> paginateData() => _usersBloc!.queryUsers(
filter: widget.filter,
sort: widget.sort,
presence: widget.presence,
pagination: PaginationParams(
limit: widget.limit,
offset: _usersBloc!.users?.length ?? 0,
),
);
@override
void didUpdateWidget(UserListCore oldWidget) {
super.didUpdateWidget(oldWidget);
if (jsonEncode(widget.filter) != jsonEncode(oldWidget.filter) ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.presence != oldWidget.presence ||
widget.limit != oldWidget.limit) {
loadData();
}
if (widget.userListController != oldWidget.userListController) {
_setupController();
}
}
}
/// Represents an item in a the user stream list.
/// Header items are prefixed with the key `HEADER` While users are prefixed
/// with `USER`.
abstract class ListItem {
/// Unique key per list item
String? get key {
if (this is ListHeaderItem) {
final header = (this as ListHeaderItem).heading;
return 'HEADER-${header.toLowerCase()}';
}
if (this is ListUserItem) {
final user = (this as ListUserItem).user;
return 'USER-${user.id}';
}
return null;
}
/// Helper function to build widget based on ListItem type
// ignore: missing_return
Widget when({
required Widget Function(String heading) headerItem,
required Widget Function(User user) userItem,
}) {
if (this is ListHeaderItem) {
return headerItem((this as ListHeaderItem).heading);
}
if (this is ListUserItem) {
return userItem((this as ListUserItem).user);
}
return Container();
}
}
/// Header Item
class ListHeaderItem extends ListItem {
/// Constructs a new [ListHeaderItem]
ListHeaderItem(this.heading);
/// Heading used to build the item.
final String heading;
}
/// User Item
class ListUserItem extends ListItem {
/// Constructs a new [ListUserItem]
ListUserItem(this.user);
/// [User] used to build the item.
final User user;
}
/// Controller used for paginating data in [ChannelListView]
class UserListController {
/// Call this function to reload data
AsyncCallback? loadData;
/// Call this function to load further data
AsyncCallback? paginateData;
}
@@ -1,145 +0,0 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Widget dedicated to the management of a users list with pagination.
///
/// [UsersBloc] can be access at anytime by using the static [of] method
/// using Flutter's [BuildContext].
///
/// API docs: https://getstream.io/chat/docs/flutter-dart/init_and_users/
@Deprecated("Use 'StreamUserListController' instead")
class UsersBloc extends StatefulWidget {
/// Instantiate a new [UsersBloc]. The parameter [child] must be supplied and
/// not null.
const UsersBloc({
required this.child,
super.key,
});
/// The widget child
final Widget child;
@override
UsersBlocState createState() => UsersBlocState();
/// Use this method to get the current [UsersBlocState] instance
static UsersBlocState of(BuildContext context) {
UsersBlocState? state;
state = context.findAncestorStateOfType<UsersBlocState>();
assert(
state != null,
'You must have a UsersBloc widget as ancestor',
);
return state!;
}
}
/// The current state of the [UsersBloc]
class UsersBlocState extends State<UsersBloc>
with AutomaticKeepAliveClientMixin {
/// The current users list
List<User>? get users => _usersController.valueOrNull;
/// The current users list as a stream
Stream<List<User>> get usersStream => _usersController.stream;
final _usersController = BehaviorSubject<List<User>>();
final _queryUsersLoadingController = BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryUsersLoading => _queryUsersLoadingController.stream;
late StreamChatCoreState _streamChatCore;
bool _paginationEnded = false;
/// The Query Users method allows you to search for users and see if they are
/// online/offline.
/// [API Reference](https://getstream.io/chat/docs/flutter-dart/query_users/?language=dart)
Future<void> queryUsers({
Filter? filter,
List<SortOption>? sort,
bool? presence,
PaginationParams pagination = const PaginationParams(limit: 30),
}) async {
final client = _streamChatCore.client;
final offset = pagination.offset;
final clear = offset == null || offset == 0;
if (clear && _paginationEnded) {
_paginationEnded = false;
}
if ((!clear && _paginationEnded) || _queryUsersLoadingController.value) {
return;
}
if (_usersController.hasValue) {
_queryUsersLoadingController.safeAdd(true);
}
try {
final oldUsers = List<User>.from(users ?? []);
final usersResponse = await client.queryUsers(
filter: filter,
sort: sort,
presence: presence,
pagination: pagination,
);
final newUsers = usersResponse.users;
if (clear) {
_usersController.safeAdd(usersResponse.users);
} else {
final temp = oldUsers + usersResponse.users;
_usersController.safeAdd(temp);
}
if (_usersController.hasValue && _queryUsersLoadingController.value) {
_queryUsersLoadingController.safeAdd(false);
}
if (newUsers.isEmpty || newUsers.length < pagination.limit) {
_paginationEnded = true;
}
} catch (e, stk) {
// reset loading controller
_queryUsersLoadingController.safeAdd(false);
if (_usersController.hasValue) {
_queryUsersLoadingController.safeAddError(e, stk);
} else {
_usersController.safeAddError(e, stk);
}
}
}
@override
void didChangeDependencies() {
_streamChatCore = StreamChatCore.of(context);
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
@override
void dispose() {
_usersController.close();
_queryUsersLoadingController.close();
super.dispose();
}
@override
bool get wantKeepAlive => true;
}
@@ -4,12 +4,8 @@ export 'package:connectivity_plus/connectivity_plus.dart';
export 'package:stream_chat/stream_chat.dart';
export 'src/better_stream_builder.dart';
export 'src/channel_list_core.dart' hide ChannelListCoreState;
export 'src/channels_bloc.dart';
export 'src/lazy_load_scroll_view.dart';
export 'src/message_list_core.dart' hide MessageListCoreState;
export 'src/message_search_bloc.dart';
export 'src/message_search_list_core.dart' hide MessageSearchListCoreState;
export 'src/message_text_field_controller.dart';
export 'src/paged_value_notifier.dart'
show PagedValueListenableBuilder, PagedValue, PagedValueNotifier;
@@ -18,9 +14,8 @@ export 'src/stream_channel.dart';
export 'src/stream_channel_list_controller.dart';
export 'src/stream_channel_list_event_handler.dart';
export 'src/stream_chat_core.dart';
export 'src/stream_member_list_controller.dart';
export 'src/stream_message_input_controller.dart';
export 'src/stream_message_search_list_controller.dart';
export 'src/stream_user_list_controller.dart';
export 'src/typedef.dart';
export 'src/user_list_core.dart' hide UserListCoreState;
export 'src/users_bloc.dart';
@@ -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: 4.6.0
version: 5.0.0
repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -17,7 +17,7 @@ dependencies:
freezed_annotation: ^2.0.3
meta: ^1.3.0
rxdart: ^0.27.0
stream_chat: ^4.6.0
stream_chat: ^5.0.0
dev_dependencies:
build_runner: ^2.0.1
dart_code_metrics: ^4.4.0
@@ -1,526 +0,0 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter_core/src/channel_list_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
const pagination = PaginationParams(limit: 3, offset: 0);
List<Channel> _generateChannels(
StreamChatClient client, {
int count = 3,
int offset = 0,
}) =>
List.generate(
count,
(index) {
index = index + offset;
return Channel(
client,
'testType$index',
'testId$index',
extraData: {'extra_data_key': 'extra_data_value_$index'},
);
},
);
testWidgets(
'should throw if ChannelListCore is used where ChannelsBloc is not present '
'in the widget tree',
(tester) async {
const channelListCoreKey = Key('channelListCore');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
);
await tester.pumpWidget(channelListCore);
expect(find.byKey(channelListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<AssertionError>());
},
);
testWidgets(
'should render ChannelListCore if used with ChannelsBloc as an ancestor',
(tester) async {
const channelListCoreKey = Key('channelListCore');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
);
final mockClient = MockClient();
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
expect(find.byKey(channelListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign loadData and paginateData callback to '
'ChannelListController if passed',
(tester) async {
const channelListCoreKey = Key('channelListCore');
final controller = ChannelListController();
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
channelListController: controller,
);
expect(controller.loadData, isNull);
expect(controller.paginateData, isNull);
final mockClient = MockClient();
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
expect(find.byKey(channelListCoreKey), findsOneWidget);
expect(controller.loadData, isNotNull);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if channelsBlocState.channelsStream emits error',
(tester) async {
const channelListCoreKey = Key('channelListCore');
const errorWidgetKey = Key('errorWidget');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) =>
Container(key: errorWidgetKey),
limit: pagination.limit,
);
final mockClient = MockClient();
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
const error = 'Error! Error! Error!';
when(() => mockClient.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: pagination,
)).thenThrow(error);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
verify(() => mockClient.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: pagination,
)).called(1);
},
);
testWidgets(
'''should build empty widget if channelsBlocState.channelsStream emits empty data''',
(tester) async {
const channelListCoreKey = Key('channelListCore');
const emptyWidgetKey = Key('emptyWidget');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
limit: pagination.limit,
);
final mockClient = MockClient();
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
const channels = <Channel>[];
when(() => mockClient.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: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
verify(() => mockClient.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: pagination,
)).called(1);
},
);
testWidgets(
'''should build list widget if channelsBlocState.channelsStream emits some data''',
(tester) async {
const channelListCoreKey = Key('channelListCore');
const listWidgetKey = Key('listWidget');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, __) => Container(key: listWidgetKey),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
limit: pagination.limit,
);
final mockClient = MockClient();
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
final channels = _generateChannels(mockClient);
when(() => mockClient.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: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
verify(() => mockClient.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: pagination,
)).called(1);
},
);
testWidgets(
'should build list widget with paginated data '
'on calling channelListCoreState.paginateData',
(tester) async {
const channelListCoreKey = Key('channelListCore');
const listWidgetKey = Key('listWidget');
final channelListCore = ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, channels) => Container(
key: listWidgetKey,
child: Text(
channels.map((e) => e.cid).join(','),
),
),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
limit: pagination.limit,
);
final mockClient = MockClient();
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
final channels = _generateChannels(mockClient);
when(() => mockClient.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: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: channelListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(find.text(channels.map((e) => e.cid).join(',')), findsOneWidget);
verify(() => mockClient.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: pagination,
)).called(1);
final channelListCoreState = tester.state<ChannelListCoreState>(
find.byKey(channelListCoreKey),
);
final offset = channels.length;
final paginatedChannels = _generateChannels(mockClient, offset: offset);
final updatedPagination = pagination.copyWith(offset: offset);
when(() => mockClient.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: updatedPagination,
)).thenAnswer((_) => Stream.value(paginatedChannels));
await channelListCoreState.paginateData();
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(
find.text([
...channels,
...paginatedChannels,
].map((e) => e.cid).join(',')),
findsOneWidget,
);
verify(() => mockClient.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: updatedPagination,
)).called(1);
},
);
testWidgets(
'should rebuild ChannelListCore with updated widget data '
'on calling setState()',
(tester) async {
StateSetter? _stateSetter;
var limit = pagination.limit;
const channelListCoreKey = Key('channelListCore');
const listWidgetKey = Key('listWidget');
ChannelListCore channelListCoreBuilder(int limit) => ChannelListCore(
key: channelListCoreKey,
listBuilder: (_, channels) => Container(
key: listWidgetKey,
child: Text(
channels.map((e) => e.cid).join(','),
),
),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) =>
const Offstage(),
limit: limit,
);
final mockClient = MockClient();
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
final channels = _generateChannels(mockClient);
when(() => mockClient.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: pagination,
)).thenAnswer((_) => Stream.value(channels));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: ChannelsBloc(
child: StatefulBuilder(builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding ChannelListCore
_stateSetter = stateSetter;
return channelListCoreBuilder(limit);
}),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(find.text(channels.map((e) => e.cid).join(',')), findsOneWidget);
verify(() => mockClient.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: pagination,
)).called(1);
// Rebuilding ChannelListCore with new pagination limit
_stateSetter?.call(() => limit = 6);
final updatedChannels = _generateChannels(mockClient, count: limit);
final updatedPagination = PaginationParams(limit: limit, offset: 0);
when(() => mockClient.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: updatedPagination,
)).thenAnswer((_) => Stream.value(updatedChannels));
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
expect(
find.text(updatedChannels.map((e) => e.cid).join(',')),
findsOneWidget,
);
verify(() => mockClient.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: updatedPagination,
)).called(1);
},
);
test('`widget.limit` should match `widget.pagination.limit`', () {
const pagination = PaginationParams(limit: 30);
final channelListCore = ChannelListCore(
listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
limit: pagination.limit,
);
expect(channelListCore.limit, pagination.limit);
});
}
@@ -1,948 +0,0 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'matchers/channel_matcher.dart';
import 'mocks.dart';
void main() {
setUpAll(() {
registerFallbackValue(const PaginationParams());
});
List<Channel> _generateChannels(
StreamChatClient client, {
int count = 3,
int offset = 0,
}) =>
List.generate(
count,
(index) {
index = index + offset;
return Channel(
client,
'testType$index',
'testId$index',
extraData: {'extra_data_key': 'extra_data_value_$index'},
);
},
);
testWidgets(
'''should throw if ChannelsBloc is used where StreamChat is not present in the widget tree''',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
const channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(key: childKey),
);
await tester.pumpWidget(channelsBloc);
expect(find.byKey(channelsBlocKey), findsNothing);
expect(find.byKey(childKey), findsNothing);
expect(tester.takeException(), isInstanceOf<AssertionError>());
},
);
testWidgets(
'should render ChannelsBloc if used with StreamChatCore as an ancestor',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
const channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
expect(find.byKey(channelsBlocKey), findsOneWidget);
expect(find.byKey(childKey), findsOneWidget);
},
);
testWidgets(
'channelsBlocState.queryChannels() should emit data through channelsStream',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Builder(
key: childKey,
builder: (context) => const Offstage(),
),
);
final mockClient = MockClient();
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final offlineChannels = _generateChannels(mockClient);
final onlineChannels = _generateChannels(mockClient, offset: 3);
when(() => mockClient.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(
(_) => Stream.fromIterable([offlineChannels, onlineChannels]),
);
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(offlineChannels),
isSameChannelListAs(onlineChannels),
]),
);
verify(() => mockClient.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);
},
);
testWidgets(
'channelsBlocState.channelsStream should emit error '
'if client.queryChannels() throws',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const childKey = Key('child');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Builder(
key: childKey,
builder: (context) => const Offstage(),
),
);
final mockClient = MockClient();
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
const error = 'Error! Error! Error!';
when(() => mockClient.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(error);
channelsBlocState.queryChannels();
await expectLater(
channelsBlocState.channelsStream,
emitsError(error),
);
verify(() => mockClient.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);
},
);
testWidgets(
'calling channelsBlocState.queryChannels() again with an offset '
'should emit new data through channelsStream and also emit loading state '
'through queryChannelsLoading',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
final mockClient = MockClient();
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(() => mockClient.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((_) => Stream.value(channels));
const pagination = PaginationParams(limit: 3);
channelsBlocState.queryChannels(
paginationParams: pagination,
);
await expectLater(
channelsBlocState.channelsStream,
emits(isSameChannelListAs(channels)),
);
verify(() => mockClient.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);
final offset = channels.length;
final paginationParams = pagination.copyWith(offset: offset);
final newChannels = _generateChannels(mockClient, offset: offset);
when(() => mockClient.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: paginationParams,
)).thenAnswer(
(_) => Stream.value(newChannels),
);
channelsBlocState.queryChannels(paginationParams: paginationParams);
await Future.wait([
expectLater(
channelsBlocState.queryChannelsLoading,
emitsInOrder([true, false]),
),
expectLater(
channelsBlocState.channelsStream,
emits(isSameChannelListAs(channels + newChannels)),
),
]);
verify(() => mockClient.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: paginationParams,
)).called(1);
},
);
testWidgets(
'calling channelsBlocState.queryChannels() again with an offset '
'should emit error through queryChannelsLoading if '
'client.queryChannels() throws',
(tester) async {
const channelsBlocKey = Key('channelsBloc');
const channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
final mockClient = MockClient();
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
const paginationParams = PaginationParams(
limit: 3,
);
when(() => mockClient.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: paginationParams,
)).thenAnswer((_) => Stream.value(channels));
channelsBlocState.queryChannels(
paginationParams: paginationParams,
);
await expectLater(
channelsBlocState.channelsStream,
emits(isSameChannelListAs(channels)),
);
verify(() => mockClient.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: paginationParams,
)).called(1);
const error = 'Error! Error! Error!';
when(() => mockClient.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: paginationParams,
)).thenThrow(error);
channelsBlocState.queryChannels(paginationParams: paginationParams);
await expectLater(
channelsBlocState.queryChannelsLoading,
emitsError(error),
);
verify(() => mockClient.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: paginationParams,
)).called(1);
},
);
group('event controller test', () {
late StreamController<Event> eventController;
setUp(() {
eventController = StreamController<Event>.broadcast();
});
testWidgets(
'channel should get hide when EventType.channelHidden event is received',
(tester) async {
final mockClient = MockClient();
const channelsBlocKey = Key('channelsBloc');
const channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
when(() => mockClient.on(
EventType.channelHidden,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(() => mockClient.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(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(() => mockClient.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);
final channelHiddenEvent = Event(
type: EventType.channelHidden,
cid: channels.first.cid,
);
eventController.add(channelHiddenEvent);
final newChannels = [...channels]
..removeWhere((it) => it.cid == channelHiddenEvent.cid);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(() => mockClient.on(EventType.channelHidden)).called(1);
},
);
testWidgets(
'channel should get removed when EventType.channelDeleted or '
'EventType.notificationRemovedFromChannel, event is received',
(tester) async {
final mockClient = MockClient();
const channelsBlocKey = Key('channelsBloc');
const channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
when(() => mockClient.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(() => mockClient.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(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(() => mockClient.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);
final channelDeletedOrNotificationRemovedEvent = Event(
type: EventType.channelDeleted,
channel: EventChannel(
cid: channels.first.cid!,
updatedAt: DateTime.now(),
config: ChannelConfig(),
createdAt: DateTime.now(),
memberCount: 1,
),
);
eventController.add(channelDeletedOrNotificationRemovedEvent);
final channelCid =
channelDeletedOrNotificationRemovedEvent.channel?.cid;
final newChannels = [...channels]
..removeWhere((it) => it.cid == channelCid);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(() => mockClient.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)).called(1);
},
);
testWidgets(
'event channel should be moved to top of the list if present when '
'EventType.messageNew event is received',
(tester) async {
final mockClient = MockClient();
const channelsBlocKey = Key('channelsBloc');
const channelsBloc = ChannelsBloc(
key: channelsBlocKey,
child: Offstage(),
);
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
when(() => mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
final channels = _generateChannels(mockClient);
when(() => mockClient.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(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(() => mockClient.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);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: channels.last.cid,
);
eventController.add(messageNewEvent);
final channelCid = messageNewEvent.cid;
final index = channels.indexWhere((it) => it.cid == channelCid);
final updatedChannel = channels[index];
final newChannels = [...channels]
..removeAt(index)
..insert(0, updatedChannel);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(() => mockClient.on(EventType.messageNew)).called(1);
},
);
testWidgets(
'event channel should be moved to top of the list if present inside '
'hiddenChannels list and shouldAddChannel is true when '
'EventType.messageNew event is received',
(tester) async {
final hiddenChannelEventController = StreamController<Event>();
addTearDown(hiddenChannelEventController.close);
final mockClient = MockClient();
final channels = _generateChannels(mockClient);
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
shouldAddChannel: (e) => channels.map((it) => it.cid).contains(e.cid),
child: const Offstage(),
);
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
when(() => mockClient.on(
EventType.channelHidden,
)).thenAnswer((_) => hiddenChannelEventController.stream);
when(() => mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: channels.last.cid,
);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
when(() => mockClient.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(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(() => mockClient.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);
final channelHiddenEvent = Event(
type: EventType.channelHidden,
cid: channels.last.cid,
);
// Hiding the channel before passing messageNew event
hiddenChannelEventController.add(channelHiddenEvent);
final channelsAfterHiddenEvent = [...channels]..removeLast();
eventController.add(messageNewEvent);
final channelCid = messageNewEvent.cid;
final index = channels.indexWhere((it) => it.cid == channelCid);
final newChannels = [...channels]
..removeAt(index)
..insert(0, channels[index]);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(channelsAfterHiddenEvent),
isSameChannelListAs(newChannels),
]),
);
verify(() => mockClient.on(EventType.channelHidden)).called(1);
verify(() => mockClient.on(EventType.messageNew)).called(1);
},
);
testWidgets(
'event channel should be moved to top of the list if present inside '
'channel state and shouldAddChannel is true when '
'EventType.messageNew event is received',
(tester) async {
final mockClient = MockClient();
final channels = _generateChannels(mockClient);
final stateChannels = {
for (var c in _generateChannels(mockClient, offset: 5)) c.cid!: c
};
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
shouldAddChannel: (_) => true,
child: const Offstage(),
);
when(() => mockClient.state.channels).thenReturn(stateChannels);
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
when(() => mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
when(() => mockClient.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(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(() => mockClient.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);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: stateChannels.keys.first,
);
eventController.add(messageNewEvent);
final newChannels = [...channels]
..insert(0, stateChannels[stateChannels.keys.first]!);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(() => mockClient.on(EventType.messageNew)).called(1);
},
);
testWidgets(
'channels should get sorted according to channelsComparator when '
'EventType.messageNew event is received',
(tester) async {
final mockClient = MockClient();
final channels = _generateChannels(mockClient);
int channelComparator(Channel a, Channel b) {
final aData = a.extraData['extra_data_key'].toString();
final bData = b.extraData['extra_data_key'].toString();
return bData.compareTo(aData);
}
const channelsBlocKey = Key('channelsBloc');
final channelsBloc = ChannelsBloc(
key: channelsBlocKey,
shouldAddChannel: (_) => true,
channelsComparator: channelComparator,
child: const Offstage(),
);
when(() => mockClient.on(any(), any(), any(), any()))
.thenAnswer((_) => const Stream.empty());
when(() => mockClient.on(
EventType.messageNew,
)).thenAnswer((_) => eventController.stream);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: channelsBloc,
),
);
final channelsBlocState = tester.state<ChannelsBlocState>(
find.byKey(channelsBlocKey),
);
when(() => mockClient.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(
(_) => Stream.value(channels),
);
await channelsBlocState.queryChannels();
verify(() => mockClient.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);
final messageNewEvent = Event(
type: EventType.messageNew,
cid: channels.first.cid,
);
eventController.add(messageNewEvent);
final newChannels = [...channels]..sort(channelComparator);
await expectLater(
channelsBlocState.channelsStream,
emitsInOrder([
isSameChannelListAs(channels),
isSameChannelListAs(newChannels),
]),
);
verify(() => mockClient.on(EventType.messageNew)).called(1);
},
);
tearDown(() {
eventController.close();
});
});
}
@@ -93,7 +93,7 @@ void main() {
final mockChannel = MockChannel();
when(() => mockChannel.initialized).thenAnswer((_) => Future.value(true));
when(() => mockChannel.state.unreadCount).thenReturn(0);
when(() => mockChannel.state.isUpToDate).thenReturn(true);
when(() => mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value([]));
@@ -129,6 +129,7 @@ void main() {
final mockChannel = MockChannel();
when(() => mockChannel.state.isUpToDate).thenReturn(true);
when(() => mockChannel.state.unreadCount).thenReturn(0);
when(() => mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value([]));
when(() => mockChannel.state.messages).thenReturn([]);
@@ -167,6 +168,7 @@ void main() {
final mockChannel = MockChannel();
when(() => mockChannel.state.isUpToDate).thenReturn(true);
when(() => mockChannel.state.unreadCount).thenReturn(0);
final messages = _generateMessages();
when(() => mockChannel.state.messages).thenReturn(messages);
when(() => mockChannel.state.messagesStream)
@@ -222,6 +224,7 @@ void main() {
when(() => mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.error(error));
when(() => mockChannel.state.messages).thenReturn([]);
when(() => mockChannel.state.unreadCount).thenReturn(0);
await tester.pumpWidget(
Directionality(
@@ -263,6 +266,7 @@ void main() {
when(() => mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value(messages));
when(() => mockChannel.state.messages).thenReturn(messages);
when(() => mockChannel.state.unreadCount).thenReturn(0);
await tester.pumpWidget(
Directionality(
@@ -312,6 +316,7 @@ void main() {
when(() => mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value(messages));
when(() => mockChannel.state.messages).thenReturn(messages);
when(() => mockChannel.state.unreadCount).thenReturn(0);
await tester.pumpWidget(
Directionality(
@@ -357,6 +362,7 @@ void main() {
when(() => mockChannel.state.messagesStream)
.thenAnswer((_) => Stream.value(messages));
when(() => mockChannel.state.messages).thenReturn(messages);
when(() => mockChannel.state.unreadCount).thenReturn(0);
await tester.pumpWidget(
Directionality(
@@ -406,6 +412,7 @@ void main() {
when(() => mockChannel.state.threads).thenReturn(threads);
when(() => mockChannel.state.threadsStream)
.thenAnswer((_) => Stream.value(threads));
when(() => mockChannel.state.unreadCount).thenReturn(0);
await tester.pumpWidget(
Directionality(
@@ -1,400 +0,0 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'matchers/get_message_response_matcher.dart';
import 'mocks.dart';
const testFilter = Filter.custom(key: 'test', value: 'testValue');
void main() {
List<GetMessageResponse> _generateMessages({
int count = 3,
int offset = 0,
}) =>
List.generate(
count,
(index) {
index = index + offset;
return GetMessageResponse()
..message = Message(
id: 'testId$index',
text: 'testTextData$index',
)
..channel = ChannelModel(
cid: 'testCid:id',
);
},
);
testWidgets(
'''messageSearchBlocState.search() should throw if used where StreamChat is not present in the widget tree''',
(tester) async {
const messageSearchBloc = MessageSearchBloc(
child: Offstage(),
);
await tester.pumpWidget(messageSearchBloc);
expect(tester.takeException(), isInstanceOf<AssertionError>());
},
);
testWidgets(
'messageSearchBlocState.search() should emit data through usersStream',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
const messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
final messageResponseList = _generateMessages();
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: any(named: 'paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
messageSearchBlocState.search(filter: testFilter);
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: any(named: 'paginationParams'),
)).called(1);
},
);
testWidgets(
'''messageSearchBlocState.messagesStream should emit error if client.search() throws''',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
const messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
const error = 'Error! Error! Error!';
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: any(named: 'paginationParams'),
)).thenThrow(error);
messageSearchBlocState.search(filter: testFilter);
await expectLater(
messageSearchBlocState.messagesStream,
emitsError(error),
);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: any(named: 'paginationParams'),
)).called(1);
},
);
testWidgets(
'''calling messageSearchBlocState.search() again with an offset should emit new data through messagesStream and also emit loading state through queryMessagesLoading''',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
const messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
const pagination = PaginationParams(limit: 25);
final messageResponseList = _generateMessages(count: 25);
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
messageSearchBlocState.search(pagination: pagination, filter: testFilter);
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: pagination,
)).called(1);
final offset = messageResponseList.length;
final paginatedMessageResponseList = _generateMessages(offset: offset);
final newPagination = pagination.copyWith(offset: offset);
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: newPagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()
..results = paginatedMessageResponseList
..next = null
..previous = null,
);
messageSearchBlocState.search(pagination: pagination, filter: testFilter);
await Future.wait([
expectLater(
messageSearchBlocState.queryMessagesLoading,
emitsInOrder([true, false]),
),
expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(
messageResponseList + paginatedMessageResponseList,
)),
),
]);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: pagination,
)).called(1);
},
);
testWidgets(
'''calling messageSearchBlocState.search() again with an offset should emit error through queryUsersLoading if client.search() throws''',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
const messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
const pagination = PaginationParams(limit: 25);
final messageResponseList = _generateMessages(count: 25);
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
messageSearchBlocState.search(pagination: pagination, filter: testFilter);
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: pagination,
)).called(1);
final offset = messageResponseList.length;
final newPagination = pagination.copyWith(offset: offset);
const error = 'Error! Error! Error!';
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: newPagination,
)).thenThrow(error);
messageSearchBlocState.search(
pagination: newPagination,
filter: testFilter,
);
await expectLater(
messageSearchBlocState.queryMessagesLoading,
emitsError(error),
);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: newPagination,
)).called(1);
},
);
testWidgets(
'''calling messageSearchBlocState.search() again with an offset should do nothing and return if pagination is completed''',
(tester) async {
const messageSearchBlocKey = Key('messageSearchBloc');
const childKey = Key('child');
const messageSearchBloc = MessageSearchBloc(
key: messageSearchBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: messageSearchBloc,
),
);
final messageSearchBlocState = tester.state<MessageSearchBlocState>(
find.byKey(messageSearchBlocKey),
);
const pagination = PaginationParams(limit: 25);
final messageResponseList = _generateMessages(count: 20);
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
messageSearchBlocState.search(pagination: pagination, filter: testFilter);
await expectLater(
messageSearchBlocState.messagesStream,
emits(isSameMessageResponseListAs(messageResponseList)),
);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: any(named: 'messageFilters'),
paginationParams: pagination,
)).called(1);
final offset = messageResponseList.length;
final newPagination = pagination.copyWith(offset: offset);
messageSearchBlocState.search(
filter: testFilter,
pagination: newPagination,
);
// should emit nothing.
await expectLater(
// skipping the initial data (behaviorSubject).
messageSearchBlocState.messagesStream.skip(1),
emitsInOrder([]),
);
},
);
}
@@ -1,571 +0,0 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter_core/src/message_search_list_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
const testFilter = Filter.custom(key: 'test', value: 'testValue');
const testMessageFilter = Filter.custom(key: 'test', value: 'testValue');
void main() {
List<GetMessageResponse> _generateMessages({
int count = 3,
int offset = 0,
}) =>
List.generate(
count,
(index) {
index = index + offset;
return GetMessageResponse()
..message = Message(
id: 'testId$index',
text: 'testTextData$index',
)
..channel = ChannelModel(
cid: 'test:Cid',
);
},
);
testWidgets(
'should throw if both `messageQuery` and `messageFilters` are provided',
(tester) async {
expect(
() => MessageSearchListCore(
childBuilder: (_) => const Offstage(),
loadingBuilder: (_) => const Offstage(),
emptyBuilder: (_) => const Offstage(),
errorBuilder: (_, __) => const Offstage(),
filters: testFilter,
messageFilters: testMessageFilter,
messageQuery: 'test',
),
throwsAssertionError,
);
},
);
testWidgets(
'should throw if both `messageQuery` and `messageFilters` are not provided',
(tester) async {
expect(
() => MessageSearchListCore(
childBuilder: (_) => const Offstage(),
loadingBuilder: (_) => const Offstage(),
emptyBuilder: (_) => const Offstage(),
errorBuilder: (_, __) => const Offstage(),
filters: testFilter,
),
throwsAssertionError,
);
},
);
testWidgets(
'should throw if MessageSearchListCore is used where MessageSearchBloc '
'is not present in the widget tree',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse>? messages) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object? error) => const Offstage(),
filters: testFilter,
messageFilters: testMessageFilter,
);
await tester.pumpWidget(messageSearchListCore);
expect(find.byKey(messageSearchListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<AssertionError>());
},
);
testWidgets(
'should render MessageSearchListCore if used with '
'MessageSearchListCore as an ancestor',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object? error) => const Offstage(),
filters: testFilter,
messageFilters: testMessageFilter,
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
expect(find.byKey(messageSearchListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign loadData and paginateData callback to '
'UserListController if passed',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
final controller = MessageSearchListController();
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
messageSearchListController: controller,
filters: testFilter,
messageFilters: testMessageFilter,
);
expect(controller.loadData, isNull);
expect(controller.paginateData, isNull);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(messageSearchListCoreKey), findsOneWidget);
expect(controller.loadData, isNotNull);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if messageSearchBloc.messagesStream emits error',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const errorWidgetKey = Key('errorWidget');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(
key: errorWidgetKey,
),
filters: testFilter,
messageFilters: testMessageFilter,
);
final mockClient = MockClient();
const error = 'Error! Error! Error!';
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: any(named: 'paginationParams'),
)).thenThrow(error);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: any(named: 'paginationParams'),
)).called(1);
},
);
testWidgets(
'should build empty widget if messageSearchBloc.messagesStream '
'emits empty data',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const emptyWidgetKey = Key('emptyWidget');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) =>
const Offstage(key: emptyWidgetKey),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
filters: testFilter,
messageFilters: testMessageFilter,
);
final mockClient = MockClient();
final messageResponseList = <GetMessageResponse>[];
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: any(named: 'paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: any(named: 'paginationParams'),
)).called(1);
},
);
testWidgets(
'should build child widget if usersBlocState.usersStream emits some data',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => const Offstage(
key: childWidgetKey,
),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
filters: testFilter,
messageFilters: testMessageFilter,
);
final mockClient = MockClient();
final messageResponseList = _generateMessages();
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: any(named: 'paginationParams'),
)).thenAnswer(
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: any(named: 'paginationParams'),
)).called(1);
},
);
testWidgets(
'should build child widget with paginated data '
'on calling channelListCoreState.paginateData',
(tester) async {
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
const pagination = PaginationParams(limit: 25);
final messageSearchListCore = MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Container(
key: childWidgetKey,
child: Text(
messages.map((e) => '${e.channel?.cid}-${e.message.id}').join(','),
),
),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
limit: pagination.limit,
filters: testFilter,
messageFilters: testMessageFilter,
);
final mockClient = MockClient();
final messageResponseList = _generateMessages(count: 25);
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: messageSearchListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text(
messageResponseList
.map((e) => '${e.channel?.cid}-${e.message.id}')
.join(','),
),
findsOneWidget,
);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: pagination,
)).called(1);
final messageSearchListCoreState =
tester.state<MessageSearchListCoreState>(
find.byKey(messageSearchListCoreKey),
);
final offset = messageResponseList.length;
final paginatedMessageResponseList = _generateMessages(offset: offset);
final updatedPagination = pagination.copyWith(offset: offset);
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: updatedPagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()
..results = paginatedMessageResponseList
..next = null
..previous = null,
);
await messageSearchListCoreState.paginateData();
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text([
...messageResponseList,
...paginatedMessageResponseList,
].map((e) => '${e.channel?.cid}-${e.message.id}').join(',')),
findsOneWidget,
);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: updatedPagination,
)).called(1);
},
);
testWidgets(
'should rebuild MessageSearchListCore with updated widget data '
'on calling setState()',
(tester) async {
const pagination = PaginationParams();
StateSetter? _stateSetter;
var limit = pagination.limit;
const messageSearchListCoreKey = Key('messageSearchListCore');
const childWidgetKey = Key('childWidget');
MessageSearchListCore messageSearchListCoreBuilder(int limit) =>
MessageSearchListCore(
key: messageSearchListCoreKey,
childBuilder: (List<GetMessageResponse> messages) => Container(
key: childWidgetKey,
child: Text(
messages
.map((e) => '${e.channel?.cid}-${e.message.id}')
.join(','),
),
),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) =>
const Offstage(),
limit: limit,
filters: testFilter,
messageFilters: testMessageFilter,
);
final mockClient = MockClient();
final messageResponseList = _generateMessages();
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: pagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()
..results = messageResponseList
..next = null
..previous = null,
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: MessageSearchBloc(
child: StatefulBuilder(builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding UserListCore
_stateSetter = stateSetter;
return messageSearchListCoreBuilder(limit);
}),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text(
messageResponseList
.map((e) => '${e.channel?.cid}-${e.message.id}')
.join(','),
),
findsOneWidget,
);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: pagination,
)).called(1);
// Rebuilding MessageSearchListCore with new pagination limit
_stateSetter?.call(() => limit = 6);
final updatedMessageResponseList = _generateMessages(count: limit);
final updatedPagination = PaginationParams(limit: limit);
when(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: updatedPagination,
)).thenAnswer(
(_) async => SearchMessagesResponse()
..results = updatedMessageResponseList
..next = null
..previous = null,
);
await tester.pumpAndSettle();
expect(find.byKey(childWidgetKey), findsOneWidget);
expect(
find.text(updatedMessageResponseList
.map((e) => '${e.channel?.cid}-${e.message.id}')
.join(',')),
findsOneWidget,
);
verify(() => mockClient.search(
testFilter,
query: any(named: 'query'),
sort: any(named: 'sort'),
messageFilters: testMessageFilter,
paginationParams: updatedPagination,
)).called(1);
},
);
test('`widget.limit` should match `widget.pagination.limit`', () {
const pagination = PaginationParams(limit: 30);
final messageSearchListCore = MessageSearchListCore(
childBuilder: (List<GetMessageResponse> messages) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object? error) => const Offstage(),
filters: testFilter,
messageFilters: testMessageFilter,
limit: pagination.limit,
);
expect(messageSearchListCore.limit, pagination.limit);
});
}
@@ -64,6 +64,7 @@ void main() {
const streamChannelKey = Key('streamChannel');
const childKey = Key('childKey');
when(() => mockChannel.initialized).thenAnswer((_) => Future.value(true));
when(() => mockChannel.state.unreadCount).thenReturn(0);
final streamChannel = StreamChannel(
key: streamChannelKey,
channel: mockChannel,
@@ -97,6 +98,7 @@ void main() {
);
when(() => mockChannel.initialized)
.thenAnswer((_) => Future.error(error));
when(() => mockChannel.state.unreadCount).thenReturn(0);
await tester.pumpWidget(
Directionality(
@@ -127,6 +129,7 @@ void main() {
);
when(() => mockChannel.initialized).thenAnswer((_) async => false);
when(() => mockChannel.state.unreadCount).thenReturn(0);
await tester.pumpWidget(
Directionality(
@@ -1,538 +0,0 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter_core/src/user_list_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'mocks.dart';
void main() {
const alphabets = 'abcdefghijklmnopqrstuvwxyz';
List<User> _generateUsers({
int count = 3,
int offset = 0,
}) =>
List.generate(
count,
(index) {
index = index + offset;
return User(
id: 'testId$index',
role: 'testRole$index',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
lastActive: DateTime.now(),
online: true,
extraData: {
'name': '${alphabets[index]}-testName',
},
);
},
);
testWidgets(
'should throw if UserListCore is used where UsersBloc is not present '
'in the widget tree',
(tester) async {
const userListCoreKey = Key('userListCore');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
);
await tester.pumpWidget(userListCore);
expect(find.byKey(userListCoreKey), findsNothing);
expect(tester.takeException(), isInstanceOf<AssertionError>());
},
);
testWidgets(
'should render UserListCore if used with UsersBloc as an ancestor',
(tester) async {
const userListCoreKey = Key('userListCore');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
expect(find.byKey(userListCoreKey), findsOneWidget);
},
);
testWidgets(
'should assign loadData and paginateData callback to '
'UserListController if passed',
(tester) async {
const userListCoreKey = Key('userListCore');
final controller = UserListController();
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
userListController: controller,
);
expect(controller.loadData, isNull);
expect(controller.paginateData, isNull);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
expect(find.byKey(userListCoreKey), findsOneWidget);
expect(controller.loadData, isNotNull);
expect(controller.paginateData, isNotNull);
},
);
testWidgets(
'should build error widget if usersBlocState.usersStream emits error',
(tester) async {
const userListCoreKey = Key('userListCore');
const errorWidgetKey = Key('errorWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) =>
Container(key: errorWidgetKey),
);
final mockClient = MockClient();
const error = 'Error! Error! Error!';
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).thenThrow(error);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(errorWidgetKey), findsOneWidget);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).called(1);
},
);
testWidgets(
'should build empty widget if usersBlocState.usersStream emits empty data',
(tester) async {
const userListCoreKey = Key('userListCore');
const emptyWidgetKey = Key('emptyWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
);
final mockClient = MockClient();
const users = <User>[];
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(emptyWidgetKey), findsOneWidget);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).called(1);
},
);
testWidgets(
'should build list widget if usersBlocState.usersStream emits some data',
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, __) => Container(key: listWidgetKey),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
);
final mockClient = MockClient();
final users = _generateUsers();
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).called(1);
},
);
testWidgets(
'should build list widget with grouped data if groupAlphabetically is true',
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
key: listWidgetKey,
child: ListView(
children: items
.map((e) => Container(
key: Key(e.key ?? ''),
child: e.when(
headerItem: Text.new,
userItem: (user) => Text(user.id),
),
))
.toList(growable: false),
),
),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
groupAlphabetically: true,
);
final mockClient = MockClient();
final users = _generateUsers();
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
for (final user in users) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).called(1);
},
);
testWidgets(
'should build list widget with paginated data '
'on calling channelListCoreState.paginateData',
(tester) async {
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
const pagination = PaginationParams(limit: 15);
final userListCore = UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
key: listWidgetKey,
child: ListView(
children: items
.map((e) => Container(
key: Key(e.key ?? ''),
child: e.when(
headerItem: Text.new,
userItem: (user) => Text(user.id),
),
))
.toList(growable: false),
),
),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
limit: pagination.limit,
groupAlphabetically: true,
);
final mockClient = MockClient();
final users = _generateUsers(count: 15);
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: UsersBloc(
child: userListCore,
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
for (final user in users) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).called(1);
final userListCoreState = tester.state<UserListCoreState>(
find.byKey(userListCoreKey),
);
final offset = users.length;
final paginatedUsers = _generateUsers(offset: offset);
final updatedPagination = pagination.copyWith(offset: offset);
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: updatedPagination,
))
.thenAnswer(
(_) async => QueryUsersResponse()..users = paginatedUsers);
await userListCoreState.paginateData();
await tester.pumpAndSettle();
for (final user in users + paginatedUsers) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: updatedPagination,
)).called(1);
},
);
testWidgets(
'should rebuild UserListCore with updated widget data '
'on calling setState()',
(tester) async {
const pagination = PaginationParams();
StateSetter? _stateSetter;
var limit = pagination.limit;
const userListCoreKey = Key('userListCore');
const listWidgetKey = Key('listWidget');
UserListCore userListCoreBuilder(int limit) => UserListCore(
key: userListCoreKey,
listBuilder: (_, items) => Container(
key: listWidgetKey,
child: ListView(
children: items
.map((e) => Container(
key: Key(e.key ?? ''),
child: e.when(
headerItem: Text.new,
userItem: (user) => Text(user.id),
),
))
.toList(growable: false),
),
),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) =>
const Offstage(),
limit: limit,
groupAlphabetically: true,
);
final mockClient = MockClient();
final users = _generateUsers();
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: StreamChatCore(
client: mockClient,
child: UsersBloc(
child: StatefulBuilder(builder: (context, stateSetter) {
// Assigning stateSetter for rebuilding UserListCore
_stateSetter = stateSetter;
return userListCoreBuilder(limit);
}),
),
),
),
);
await tester.pumpAndSettle();
expect(find.byKey(listWidgetKey), findsOneWidget);
for (final user in users) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).called(1);
// Rebuilding UserListCore with new pagination limit
_stateSetter?.call(() => limit = 6);
final updatedUsers = _generateUsers(count: limit);
final updatedPagination = PaginationParams(limit: limit);
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: updatedPagination,
))
.thenAnswer((_) async => QueryUsersResponse()..users = updatedUsers);
await tester.pumpAndSettle();
for (final user in updatedUsers) {
expect(find.byKey(Key('HEADER-${user.name[0]}')), findsOneWidget);
expect(find.byKey(Key('USER-${user.id}')), findsOneWidget);
}
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: updatedPagination,
)).called(1);
},
);
test('`widget.limit` should match `widget.pagination.limit`', () {
const limit = 20;
final userListCore = UserListCore(
listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(),
limit: limit,
);
expect(userListCore.limit, limit);
});
}
@@ -1,365 +0,0 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/users_bloc.dart';
import 'matchers/users_matcher.dart';
import 'mocks.dart';
void main() {
List<User> _generateUsers({
int count = 3,
int offset = 0,
}) =>
List.generate(
count,
(index) {
index = index + offset;
return User(
id: 'testId$index',
role: 'testRole$index',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
lastActive: DateTime.now(),
online: true,
extraData: const {'extra_data_key': 'extraDataValue'},
);
},
);
testWidgets(
'usersBlocState.queryUsers() should throw if used where '
'StreamChat is not present in the widget tree',
(tester) async {
const usersBloc = UsersBloc(
child: Offstage(),
);
await tester.pumpWidget(usersBloc);
expect(tester.takeException(), isInstanceOf<AssertionError>());
},
);
testWidgets(
'usersBlocState.queryUsers() should emit data through usersStream',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
const usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
final users = _generateUsers();
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).called(1);
},
);
testWidgets(
'usersBlocState.usersStream should emit error '
'if client.queryUsers() throws',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
const usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
const error = 'Error! Error! Error!';
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).thenThrow(error);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emitsError(error),
);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).called(1);
},
);
testWidgets(
'calling usersBlocState.queryUsers() again with an offset '
'should emit new data through usersStream and also emit loading state '
'through queryUsersLoading',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
const usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
const pagination = PaginationParams(limit: 25);
final users = _generateUsers(count: 25);
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: pagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers(pagination: pagination);
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: pagination,
)).called(1);
final offset = users.length;
final paginatedUsers = _generateUsers(offset: offset);
final newPagination = pagination.copyWith(offset: offset);
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: newPagination,
)).thenAnswer(
(_) async => QueryUsersResponse()..users = paginatedUsers,
);
usersBlocState.queryUsers(pagination: newPagination);
await Future.wait([
expectLater(
usersBlocState.queryUsersLoading,
emitsInOrder([true, false]),
),
expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users + paginatedUsers)),
),
]);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: newPagination,
)).called(1);
},
);
testWidgets(
'calling usersBlocState.queryUsers() again with an offset '
'should emit error through queryUsersLoading if '
'client.queryUsers() throws',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
const usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
const pagination = PaginationParams(limit: 25);
final users = _generateUsers(count: 25);
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: pagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers(pagination: pagination);
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: pagination,
)).called(1);
final offset = users.length;
final newPagination = pagination.copyWith(offset: offset);
const error = 'Error! Error! Error!';
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: newPagination,
)).thenThrow(error);
usersBlocState.queryUsers(pagination: newPagination);
await expectLater(
usersBlocState.queryUsersLoading,
emitsError(error),
);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: newPagination,
)).called(1);
},
);
testWidgets(
'''calling usersBlocState.queryUsers() again with an offset should do nothing and return if pagination is completed''',
(tester) async {
const usersBlocKey = Key('usersBloc');
const childKey = Key('child');
const usersBloc = UsersBloc(
key: usersBlocKey,
child: Offstage(key: childKey),
);
final mockClient = MockClient();
await tester.pumpWidget(
StreamChatCore(
client: mockClient,
child: usersBloc,
),
);
final usersBlocState = tester.state<UsersBlocState>(
find.byKey(usersBlocKey),
);
const pagination = PaginationParams(limit: 30);
final users = _generateUsers(count: 25);
when(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: pagination,
)).thenAnswer((_) async => QueryUsersResponse()..users = users);
usersBlocState.queryUsers();
await expectLater(
usersBlocState.usersStream,
emits(isSameUserListAs(users)),
);
verify(() => mockClient.queryUsers(
filter: any(named: 'filter'),
sort: any(named: 'sort'),
presence: any(named: 'presence'),
pagination: any(named: 'pagination'),
)).called(1);
final offset = users.length;
final newPagination = pagination.copyWith(offset: offset);
usersBlocState.queryUsers(pagination: newPagination);
// should emit nothing.
await expectLater(
// skipping the initial data (behaviorSubject).
usersBlocState.usersStream,
emitsInOrder([]),
);
},
);
}