V5 (#1222)
* 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:
co-authored by
Sahil Kumar
GroovinChip
Gordon Hayes
Sacha Arbonel
parent
3e1d6e91c7
commit
4f2542291c
@@ -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 you’ll 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 you’ll 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 you’ll 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([]),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user