* 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]>
2735 lines
83 KiB
Dart
2735 lines
83 KiB
Dart
import 'package:mocktail/mocktail.dart';
|
|
import 'package:stream_chat/src/client/retry_policy.dart';
|
|
import 'package:stream_chat/src/core/models/banned_user.dart';
|
|
import 'package:stream_chat/stream_chat.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
import '../fakes.dart';
|
|
import '../matchers.dart';
|
|
import '../mocks.dart';
|
|
|
|
void main() {
|
|
ChannelState _generateChannelState(
|
|
String channelId,
|
|
String channelType, {
|
|
bool mockChannelConfig = false,
|
|
}) {
|
|
ChannelConfig? config;
|
|
if (mockChannelConfig) {
|
|
config = MockChannelConfig();
|
|
when(() => config!.readEvents).thenReturn(true);
|
|
when(() => config!.typingEvents).thenReturn(true);
|
|
}
|
|
final channel = ChannelModel(
|
|
id: channelId,
|
|
type: channelType,
|
|
config: config,
|
|
);
|
|
final state = ChannelState(channel: channel);
|
|
return state;
|
|
}
|
|
|
|
Logger _createLogger(String name) {
|
|
final logger = Logger.detached(name)..level = Level.ALL;
|
|
logger.onRecord.listen(print);
|
|
return logger;
|
|
}
|
|
|
|
group('Non-Initialized Channel', () {
|
|
late final client = MockStreamChatClient();
|
|
const channelId = 'test-channel-id';
|
|
const channelType = 'test-channel-type';
|
|
late Channel channel;
|
|
|
|
setUpAll(() {
|
|
// detached loggers
|
|
when(() => client.detachedLogger(any())).thenAnswer((invocation) {
|
|
final name = invocation.positionalArguments.first;
|
|
return _createLogger(name);
|
|
});
|
|
|
|
// client logger
|
|
when(() => client.logger).thenReturn(_createLogger('mock-client-logger'));
|
|
});
|
|
|
|
setUp(() {
|
|
channel = Channel(client, channelType, channelId);
|
|
});
|
|
|
|
tearDown(() {
|
|
channel.dispose();
|
|
});
|
|
|
|
test('should be able to set `extraData`', () {
|
|
expect(channel.extraData.isEmpty, isTrue);
|
|
|
|
expect(
|
|
() => channel.extraData = {'name': 'test-channel-name'},
|
|
returnsNormally,
|
|
);
|
|
|
|
expect(channel.extraData.isEmpty, isFalse);
|
|
expect(channel.extraData.containsKey('name'), isTrue);
|
|
expect(channel.extraData['name'], 'test-channel-name');
|
|
});
|
|
|
|
test('should be able to get and set `image`', () {
|
|
expect(channel.extraData.isEmpty, isTrue);
|
|
|
|
const imageUrl = 'https://getstream.io/some-image';
|
|
channel.image = imageUrl;
|
|
|
|
expect(channel.image, imageUrl);
|
|
expect(channel.extraData['image'], imageUrl);
|
|
|
|
const newImage = 'https://getstream.io/new-image';
|
|
final newChannelInstance =
|
|
Channel(client, channelType, channelId, image: newImage);
|
|
|
|
expect(newChannelInstance.image, newImage);
|
|
expect(newChannelInstance.extraData['image'], newImage);
|
|
});
|
|
|
|
test('should be able to get and set `name`', () {
|
|
expect(channel.extraData.isEmpty, isTrue);
|
|
|
|
const name = 'Channel name';
|
|
channel.name = name;
|
|
|
|
expect(channel.name, name);
|
|
expect(channel.extraData['name'], name);
|
|
|
|
const newName = 'New channel name';
|
|
final newChannelInstance =
|
|
Channel(client, channelType, channelId, name: newName);
|
|
|
|
expect(newChannelInstance.name, newName);
|
|
expect(newChannelInstance.extraData['name'], newName);
|
|
});
|
|
});
|
|
|
|
group('Initialized Channel with Persistence', () {
|
|
late final client = MockStreamChatClientWithPersistence();
|
|
const channelId = 'test-channel-id';
|
|
const channelType = 'test-channel-type';
|
|
const channelCid = '$channelType:$channelId';
|
|
late Channel channel;
|
|
|
|
setUpAll(() {
|
|
// Fallback values
|
|
registerFallbackValue(FakeMessage());
|
|
registerFallbackValue(<Message>[]);
|
|
registerFallbackValue(FakeAttachmentFile());
|
|
|
|
// detached loggers
|
|
when(() => client.detachedLogger(any())).thenAnswer((invocation) {
|
|
final name = invocation.positionalArguments.first;
|
|
return _createLogger(name);
|
|
});
|
|
|
|
final retryPolicy = RetryPolicy(
|
|
shouldRetry: (_, __, ___) => false,
|
|
retryTimeout: (_, __, ___) => Duration.zero,
|
|
);
|
|
when(() => client.retryPolicy).thenReturn(retryPolicy);
|
|
|
|
final event = Event(type: 'event.local');
|
|
when(() => client.on(any(), any(), any(), any()))
|
|
.thenAnswer((_) => Stream.value(event));
|
|
|
|
// fake clientState
|
|
final clientState = FakeClientState();
|
|
when(() => client.state).thenReturn(clientState);
|
|
|
|
// mock persistence client
|
|
final channelThreads = <String, List<Message>>{};
|
|
when(() => client.chatPersistenceClient.getChannelThreads(channelCid))
|
|
.thenAnswer((_) async => channelThreads);
|
|
final channelState = _generateChannelState(channelId, channelType);
|
|
when(() => client.chatPersistenceClient.getChannelStateByCid(channelCid))
|
|
.thenAnswer((_) async => channelState);
|
|
when(() => client.chatPersistenceClient.updateMessages(channelCid, any()))
|
|
.thenAnswer((_) => Future.value());
|
|
|
|
// client logger
|
|
when(() => client.logger).thenReturn(_createLogger('mock-client-logger'));
|
|
});
|
|
|
|
// Setting up a initialized channel
|
|
setUp(() {
|
|
final channelState = _generateChannelState(channelId, channelType);
|
|
channel = Channel.fromState(client, channelState);
|
|
});
|
|
|
|
tearDown(() {
|
|
channel.dispose();
|
|
});
|
|
});
|
|
|
|
group('Initialized Channel', () {
|
|
late final client = MockStreamChatClient();
|
|
const channelId = 'test-channel-id';
|
|
const channelType = 'test-channel-type';
|
|
const channelCid = '$channelType:$channelId';
|
|
late Channel channel;
|
|
|
|
setUpAll(() {
|
|
// Fallback values
|
|
registerFallbackValue(FakeMessage());
|
|
registerFallbackValue(FakeAttachmentFile());
|
|
registerFallbackValue(FakeEvent());
|
|
|
|
// detached loggers
|
|
when(() => client.detachedLogger(any())).thenAnswer((invocation) {
|
|
final name = invocation.positionalArguments.first;
|
|
return _createLogger(name);
|
|
});
|
|
|
|
final retryPolicy = RetryPolicy(
|
|
shouldRetry: (_, __, ___) => false,
|
|
retryTimeout: (_, __, ___) => Duration.zero,
|
|
);
|
|
when(() => client.retryPolicy).thenReturn(retryPolicy);
|
|
|
|
final event = Event(type: 'event.local');
|
|
when(() => client.on(any(), any(), any(), any()))
|
|
.thenAnswer((_) => Stream.value(event));
|
|
|
|
// fake clientState
|
|
final clientState = FakeClientState();
|
|
when(() => client.state).thenReturn(clientState);
|
|
|
|
// client logger
|
|
when(() => client.logger).thenReturn(_createLogger('mock-client-logger'));
|
|
});
|
|
|
|
// Setting up a initialized channel
|
|
setUp(() {
|
|
final channelState = _generateChannelState(
|
|
channelId,
|
|
channelType,
|
|
mockChannelConfig: true,
|
|
);
|
|
channel = Channel.fromState(client, channelState);
|
|
});
|
|
|
|
tearDown(() {
|
|
channel.dispose();
|
|
});
|
|
|
|
test('should throw if trying to set `extraData`', () {
|
|
try {
|
|
channel.extraData = {'name': 'test-channel-name'};
|
|
} catch (e) {
|
|
expect(e, isA<StateError>());
|
|
}
|
|
});
|
|
|
|
test('should throw if trying to set `image`', () {
|
|
try {
|
|
channel.image = 'https://stream.io/some-image';
|
|
} catch (e) {
|
|
expect(e, isA<StateError>());
|
|
}
|
|
});
|
|
|
|
test('should throw if trying to set `name`', () {
|
|
try {
|
|
channel.name = 'New name';
|
|
} catch (e) {
|
|
expect(e, isA<StateError>());
|
|
}
|
|
});
|
|
|
|
group('`.sendMessage`', () {
|
|
test('should work fine', () async {
|
|
final message = Message(
|
|
id: 'test-message-id',
|
|
user: client.state.currentUser,
|
|
);
|
|
|
|
final sendMessageResponse = SendMessageResponse()
|
|
..message = message.copyWith(status: MessageSendingStatus.sent);
|
|
|
|
when(() => client.sendMessage(
|
|
any(that: isSameMessageAs(message)),
|
|
channelId,
|
|
channelType,
|
|
)).thenAnswer((_) async => sendMessageResponse);
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(status: MessageSendingStatus.sending),
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(status: MessageSendingStatus.sent),
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.sendMessage(message);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.message.id, message.id);
|
|
|
|
verify(() => client.sendMessage(
|
|
any(that: isSameMessageAs(message)),
|
|
channelId,
|
|
channelType,
|
|
)).called(1);
|
|
});
|
|
|
|
test('with attachments should work just fine', () async {
|
|
final attachments = List.generate(
|
|
3,
|
|
(index) => Attachment(
|
|
id: 'test-attachment-id-$index',
|
|
type: index.isEven ? 'image' : 'file',
|
|
file: AttachmentFile(size: index * 33, path: 'test-file-path'),
|
|
),
|
|
);
|
|
|
|
final message = Message(
|
|
id: 'test-message-id',
|
|
attachments: attachments,
|
|
);
|
|
|
|
final sendImageResponse = SendImageResponse()..file = 'test-image-url';
|
|
final sendFileResponse = SendFileResponse()..file = 'test-file-url';
|
|
|
|
when(() => client.sendImage(
|
|
any(),
|
|
channelId,
|
|
channelType,
|
|
onSendProgress: any(named: 'onSendProgress'),
|
|
cancelToken: any(named: 'cancelToken'),
|
|
extraData: any(named: 'extraData'),
|
|
)).thenAnswer((_) async => sendImageResponse);
|
|
|
|
when(() => client.sendFile(
|
|
any(),
|
|
channelId,
|
|
channelType,
|
|
onSendProgress: any(named: 'onSendProgress'),
|
|
cancelToken: any(named: 'cancelToken'),
|
|
extraData: any(named: 'extraData'),
|
|
)).thenAnswer((_) async => sendFileResponse);
|
|
|
|
when(() => client.sendMessage(
|
|
any(that: isSameMessageAs(message)),
|
|
channelId,
|
|
channelType,
|
|
)).thenAnswer((_) async => SendMessageResponse()
|
|
..message = message.copyWith(
|
|
attachments: attachments
|
|
.map((it) =>
|
|
it.copyWith(uploadState: const UploadState.success()))
|
|
.toList(growable: false),
|
|
status: MessageSendingStatus.sent,
|
|
));
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder(
|
|
[
|
|
// preparing attachments to upload
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sending,
|
|
attachments: [
|
|
...attachments.map((it) => it.copyWith(
|
|
uploadState: const UploadState.preparing()))
|
|
],
|
|
),
|
|
matchSendingStatus: true,
|
|
matchAttachments: true,
|
|
matchAttachmentsUploadState: true,
|
|
),
|
|
],
|
|
// 0th attachment is successfully uploaded
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sending,
|
|
attachments: [...attachments]..[0] =
|
|
attachments[0].copyWith(
|
|
uploadState: const UploadState.success(),
|
|
),
|
|
),
|
|
matchSendingStatus: true,
|
|
matchAttachments: true,
|
|
matchAttachmentsUploadState: true,
|
|
),
|
|
],
|
|
// 0th and 1st attachment is successfully uploaded
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sending,
|
|
attachments: [...attachments]
|
|
..[0] = attachments[0].copyWith(
|
|
uploadState: const UploadState.success(),
|
|
)
|
|
..[1] = attachments[1].copyWith(
|
|
uploadState: const UploadState.success(),
|
|
),
|
|
),
|
|
matchSendingStatus: true,
|
|
matchAttachments: true,
|
|
matchAttachmentsUploadState: true,
|
|
),
|
|
],
|
|
// all the attachments are successfully uploaded
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sending,
|
|
attachments: [
|
|
...attachments.map((it) =>
|
|
it.copyWith(uploadState: const UploadState.success()))
|
|
],
|
|
),
|
|
matchSendingStatus: true,
|
|
matchAttachments: true,
|
|
matchAttachmentsUploadState: true,
|
|
),
|
|
],
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
attachments: [
|
|
...attachments.map((it) =>
|
|
it.copyWith(uploadState: const UploadState.success()))
|
|
],
|
|
),
|
|
matchSendingStatus: true,
|
|
matchAttachments: true,
|
|
matchAttachmentsUploadState: true,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
|
|
final res = await channel.sendMessage(message);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.message.id, message.id);
|
|
expect(res.message.attachments.length, message.attachments.length);
|
|
expect(
|
|
res.message.attachments.every(
|
|
(it) => it.uploadState == const UploadState.success(),
|
|
),
|
|
isTrue,
|
|
);
|
|
|
|
verify(() => client.sendImage(
|
|
any(),
|
|
channelId,
|
|
channelType,
|
|
onSendProgress: any(named: 'onSendProgress'),
|
|
cancelToken: any(named: 'cancelToken'),
|
|
extraData: any(named: 'extraData'),
|
|
)).called(2);
|
|
|
|
verify(() => client.sendFile(
|
|
any(),
|
|
channelId,
|
|
channelType,
|
|
onSendProgress: any(named: 'onSendProgress'),
|
|
cancelToken: any(named: 'cancelToken'),
|
|
extraData: any(named: 'extraData'),
|
|
)).called(1);
|
|
|
|
verify(() => client.sendMessage(
|
|
any(that: isSameMessageAs(message)),
|
|
channelId,
|
|
channelType,
|
|
)).called(1);
|
|
});
|
|
});
|
|
|
|
group('`.updateMessage`', () {
|
|
test('should work fine', () async {
|
|
final message = Message(
|
|
id: 'test-message-id',
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
final updateMessageResponse = UpdateMessageResponse()
|
|
..message = message;
|
|
|
|
when(() => client.updateMessage(any(that: isSameMessageAs(message))))
|
|
.thenAnswer((_) async => updateMessageResponse);
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(status: MessageSendingStatus.updating),
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(status: MessageSendingStatus.sent),
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.updateMessage(message);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.message.id, message.id);
|
|
|
|
verify(() => client.updateMessage(
|
|
any(that: isSameMessageAs(message)),
|
|
)).called(1);
|
|
});
|
|
|
|
test('with attachments should work just fine', () async {
|
|
final attachments = List.generate(
|
|
3,
|
|
(index) => Attachment(
|
|
id: 'test-attachment-id-$index',
|
|
type: index.isEven ? 'image' : 'file',
|
|
file: AttachmentFile(size: index * 33, path: 'test-file-path'),
|
|
),
|
|
);
|
|
|
|
final message = Message(
|
|
id: 'test-message-id',
|
|
attachments: attachments,
|
|
);
|
|
|
|
final sendImageResponse = SendImageResponse()..file = 'test-image-url';
|
|
final sendFileResponse = SendFileResponse()..file = 'test-file-url';
|
|
|
|
when(() => client.sendImage(
|
|
any(),
|
|
channelId,
|
|
channelType,
|
|
onSendProgress: any(named: 'onSendProgress'),
|
|
cancelToken: any(named: 'cancelToken'),
|
|
extraData: any(named: 'extraData'),
|
|
)).thenAnswer((_) async => sendImageResponse);
|
|
|
|
when(() => client.sendFile(
|
|
any(),
|
|
channelId,
|
|
channelType,
|
|
onSendProgress: any(named: 'onSendProgress'),
|
|
cancelToken: any(named: 'cancelToken'),
|
|
extraData: any(named: 'extraData'),
|
|
)).thenAnswer((_) async => sendFileResponse);
|
|
|
|
when(() => client.updateMessage(
|
|
any(that: isSameMessageAs(message)),
|
|
)).thenAnswer((_) async => UpdateMessageResponse()
|
|
..message = message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
attachments: attachments
|
|
.map((it) =>
|
|
it.copyWith(uploadState: const UploadState.success()))
|
|
.toList(growable: false),
|
|
));
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder(
|
|
[
|
|
// preparing attachments to upload
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.updating,
|
|
attachments: [
|
|
...attachments.map((it) => it.copyWith(
|
|
uploadState: const UploadState.preparing()))
|
|
],
|
|
),
|
|
matchSendingStatus: true,
|
|
matchAttachments: true,
|
|
matchAttachmentsUploadState: true,
|
|
),
|
|
],
|
|
// 0th attachment is successfully uploaded
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.updating,
|
|
attachments: [...attachments]..[0] =
|
|
attachments[0].copyWith(
|
|
uploadState: const UploadState.success(),
|
|
),
|
|
),
|
|
matchSendingStatus: true,
|
|
matchAttachments: true,
|
|
matchAttachmentsUploadState: true,
|
|
),
|
|
],
|
|
// 0th and 1st attachment is successfully uploaded
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.updating,
|
|
attachments: [...attachments]
|
|
..[0] = attachments[0].copyWith(
|
|
uploadState: const UploadState.success(),
|
|
)
|
|
..[1] = attachments[1].copyWith(
|
|
uploadState: const UploadState.success(),
|
|
),
|
|
),
|
|
matchSendingStatus: true,
|
|
matchAttachments: true,
|
|
matchAttachmentsUploadState: true,
|
|
),
|
|
],
|
|
// all the attachments are successfully uploaded
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.updating,
|
|
attachments: [
|
|
...attachments.map((it) =>
|
|
it.copyWith(uploadState: const UploadState.success()))
|
|
],
|
|
),
|
|
matchSendingStatus: true,
|
|
matchAttachments: true,
|
|
matchAttachmentsUploadState: true,
|
|
),
|
|
],
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
attachments: [
|
|
...attachments.map((it) =>
|
|
it.copyWith(uploadState: const UploadState.success()))
|
|
],
|
|
),
|
|
matchSendingStatus: true,
|
|
matchAttachments: true,
|
|
matchAttachmentsUploadState: true,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
|
|
final res = await channel.updateMessage(message);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.message.id, message.id);
|
|
expect(res.message.attachments.length, message.attachments.length);
|
|
expect(
|
|
res.message.attachments.every(
|
|
(it) => it.uploadState == const UploadState.success(),
|
|
),
|
|
isTrue,
|
|
);
|
|
|
|
verify(() => client.sendImage(
|
|
any(),
|
|
channelId,
|
|
channelType,
|
|
onSendProgress: any(named: 'onSendProgress'),
|
|
cancelToken: any(named: 'cancelToken'),
|
|
extraData: any(named: 'extraData'),
|
|
)).called(2);
|
|
|
|
verify(() => client.sendFile(
|
|
any(),
|
|
channelId,
|
|
channelType,
|
|
onSendProgress: any(named: 'onSendProgress'),
|
|
cancelToken: any(named: 'cancelToken'),
|
|
extraData: any(named: 'extraData'),
|
|
)).called(1);
|
|
|
|
verify(() => client.updateMessage(
|
|
any(that: isSameMessageAs(message)),
|
|
)).called(1);
|
|
});
|
|
});
|
|
|
|
test('`.partialUpdateMessage`', () async {
|
|
final message = Message(id: 'test-message-id');
|
|
|
|
const set = {'text': 'Update Message text'};
|
|
const unset = ['pinExpires'];
|
|
|
|
final updateMessageResponse = UpdateMessageResponse()
|
|
..message = message.copyWith(text: set['text'], pinExpires: null);
|
|
|
|
when(
|
|
() => client.partialUpdateMessage(message.id, set: set, unset: unset),
|
|
).thenAnswer((_) async => updateMessageResponse);
|
|
|
|
channel.state?.messagesStream.skip(1).listen(print);
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
updateMessageResponse.message.copyWith(
|
|
status: MessageSendingStatus.sending,
|
|
),
|
|
matchText: true,
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.partialUpdateMessage(
|
|
message,
|
|
set: set,
|
|
unset: unset,
|
|
);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.message.id, message.id);
|
|
expect(res.message.id, message.id);
|
|
expect(res.message.text, set['text']);
|
|
expect(res.message.pinExpires, isNull);
|
|
|
|
verify(
|
|
() => client.partialUpdateMessage(message.id, set: set, unset: unset),
|
|
).called(1);
|
|
});
|
|
|
|
group('`.deleteMessage`', () {
|
|
test('should work fine', () async {
|
|
const messageId = 'test-message-id';
|
|
final message = Message(
|
|
id: messageId,
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
when(() => client.deleteMessage(messageId))
|
|
.thenAnswer((_) async => EmptyResponse());
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(status: MessageSendingStatus.deleting),
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(status: MessageSendingStatus.sent),
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.deleteMessage(message);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.deleteMessage(messageId)).called(1);
|
|
});
|
|
|
|
test(
|
|
'''should directly update the state with message as deleted if the state is sending or failed''',
|
|
() async {
|
|
const messageId = 'test-message-id';
|
|
final message = Message(
|
|
id: messageId,
|
|
);
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(status: MessageSendingStatus.sent),
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.deleteMessage(message);
|
|
|
|
expect(res, isNotNull);
|
|
},
|
|
);
|
|
});
|
|
|
|
group('`.pinMessage`', () {
|
|
test('should work fine without passing timeoutOrExpirationDate',
|
|
() async {
|
|
final message = Message(id: 'test-message-id');
|
|
|
|
when(() => client.partialUpdateMessage(
|
|
message.id,
|
|
set: any(named: 'set'),
|
|
unset: any(named: 'unset'),
|
|
)).thenAnswer((_) async => UpdateMessageResponse()
|
|
..message = message.copyWith(
|
|
pinned: true,
|
|
pinExpires: null,
|
|
status: MessageSendingStatus.sent,
|
|
));
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(status: MessageSendingStatus.sent),
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.pinMessage(message);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.message.pinned, isTrue);
|
|
expect(res.message.pinExpires, isNull);
|
|
|
|
verify(() => client.partialUpdateMessage(
|
|
message.id,
|
|
set: any(named: 'set'),
|
|
unset: any(named: 'unset'),
|
|
)).called(1);
|
|
});
|
|
|
|
test(
|
|
'should work fine if passed timeoutOrExpirationDate as num(seconds)',
|
|
() async {
|
|
final message = Message(id: 'test-message-id');
|
|
const timeoutOrExpirationDate = 300; // 300 seconds
|
|
|
|
when(() => client.partialUpdateMessage(
|
|
message.id,
|
|
set: any(named: 'set'),
|
|
unset: any(named: 'unset'),
|
|
)).thenAnswer((_) async => UpdateMessageResponse()
|
|
..message = message.copyWith(
|
|
pinned: true,
|
|
pinExpires: DateTime.now().add(
|
|
const Duration(seconds: timeoutOrExpirationDate),
|
|
),
|
|
status: MessageSendingStatus.sent,
|
|
));
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(status: MessageSendingStatus.sent),
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.pinMessage(
|
|
message,
|
|
timeoutOrExpirationDate: timeoutOrExpirationDate,
|
|
);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.message.pinned, isTrue);
|
|
expect(res.message.pinExpires, isNotNull);
|
|
|
|
verify(() => client.partialUpdateMessage(
|
|
message.id,
|
|
set: any(named: 'set'),
|
|
unset: any(named: 'unset'),
|
|
)).called(1);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'should work fine if passed timeoutOrExpirationDate as DateTime',
|
|
() async {
|
|
final message = Message(id: 'test-message-id');
|
|
final timeoutOrExpirationDate =
|
|
DateTime.now().add(const Duration(days: 3)); // 3 days
|
|
|
|
when(() => client.partialUpdateMessage(
|
|
message.id,
|
|
set: any(named: 'set'),
|
|
unset: any(named: 'unset'),
|
|
)).thenAnswer((_) async => UpdateMessageResponse()
|
|
..message = message.copyWith(
|
|
pinned: true,
|
|
pinExpires: timeoutOrExpirationDate,
|
|
status: MessageSendingStatus.sent,
|
|
));
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(status: MessageSendingStatus.sent),
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.pinMessage(
|
|
message,
|
|
timeoutOrExpirationDate: timeoutOrExpirationDate,
|
|
);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.message.pinned, isTrue);
|
|
expect(res.message.pinExpires, isNotNull);
|
|
expect(res.message.pinExpires, timeoutOrExpirationDate.toUtc());
|
|
|
|
verify(() => client.partialUpdateMessage(
|
|
message.id,
|
|
set: any(named: 'set'),
|
|
unset: any(named: 'unset'),
|
|
)).called(1);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'should throw if invalid timeoutOrExpirationDate is passed',
|
|
() async {
|
|
final message = Message(id: 'test-message-id');
|
|
const timeoutOrExpirationDate = 'invalid-value';
|
|
|
|
try {
|
|
await channel.pinMessage(
|
|
message,
|
|
timeoutOrExpirationDate: timeoutOrExpirationDate,
|
|
);
|
|
} catch (e) {
|
|
expect(e, isA<ArgumentError>());
|
|
}
|
|
},
|
|
);
|
|
});
|
|
|
|
test('`.unpinMessage`', () async {
|
|
final message = Message(id: 'test-message-id', pinned: true);
|
|
|
|
when(() => client.partialUpdateMessage(
|
|
message.id,
|
|
set: {'pinned': false},
|
|
)).thenAnswer((_) async => UpdateMessageResponse()
|
|
..message = message.copyWith(
|
|
pinned: false,
|
|
status: MessageSendingStatus.sent,
|
|
));
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(status: MessageSendingStatus.sent),
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.unpinMessage(message);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.message.pinned, isFalse);
|
|
|
|
verify(() => client.partialUpdateMessage(
|
|
message.id,
|
|
set: {'pinned': false},
|
|
)).called(1);
|
|
});
|
|
|
|
group('`.search`', () {
|
|
final filter = Filter.in_('cid', const [channelCid]);
|
|
|
|
test('should work fine with `query`', () async {
|
|
const query = 'test-search-query';
|
|
const sort = [SortOption('test-sort-field')];
|
|
const pagination = PaginationParams();
|
|
|
|
final results = List.generate(3, (index) => GetMessageResponse());
|
|
|
|
when(() => client.search(
|
|
filter,
|
|
query: query,
|
|
sort: any(named: 'sort'),
|
|
paginationParams: any(named: 'paginationParams'),
|
|
)).thenAnswer(
|
|
(_) async => SearchMessagesResponse()..results = results,
|
|
);
|
|
|
|
final res = await channel.search(
|
|
query: query,
|
|
sort: sort,
|
|
paginationParams: pagination,
|
|
);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.results.length, results.length);
|
|
|
|
verify(() => client.search(
|
|
filter,
|
|
query: query,
|
|
sort: any(named: 'sort'),
|
|
paginationParams: any(named: 'paginationParams'),
|
|
)).called(1);
|
|
});
|
|
|
|
test('should work fine with `messageFilters`', () async {
|
|
final messageFilters = Filter.query('key', 'text');
|
|
const sort = [SortOption('test-sort-field')];
|
|
const pagination = PaginationParams();
|
|
|
|
final results = List.generate(3, (index) => GetMessageResponse());
|
|
|
|
when(() => client.search(
|
|
filter,
|
|
messageFilters: messageFilters,
|
|
sort: any(named: 'sort'),
|
|
paginationParams: any(named: 'paginationParams'),
|
|
)).thenAnswer(
|
|
(_) async => SearchMessagesResponse()..results = results,
|
|
);
|
|
|
|
final res = await channel.search(
|
|
sort: sort,
|
|
paginationParams: pagination,
|
|
messageFilters: messageFilters,
|
|
);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.results.length, results.length);
|
|
|
|
verify(() => client.search(
|
|
filter,
|
|
messageFilters: messageFilters,
|
|
sort: any(named: 'sort'),
|
|
paginationParams: any(named: 'paginationParams'),
|
|
)).called(1);
|
|
});
|
|
});
|
|
|
|
test('`.deleteFile`', () async {
|
|
const url = 'test-file-url';
|
|
|
|
when(() => client.deleteFile(url, channelId, channelType,
|
|
cancelToken: any(named: 'cancelToken')))
|
|
.thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.deleteFile(url);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.deleteFile(url, channelId, channelType,
|
|
cancelToken: any(named: 'cancelToken'))).called(1);
|
|
});
|
|
|
|
test('`.deleteImage`', () async {
|
|
const url = 'test-image-url';
|
|
|
|
when(() => client.deleteImage(url, channelId, channelType,
|
|
cancelToken: any(named: 'cancelToken')))
|
|
.thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.deleteImage(url);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.deleteImage(url, channelId, channelType,
|
|
cancelToken: any(named: 'cancelToken'))).called(1);
|
|
});
|
|
|
|
test('`.sendEvent`', () async {
|
|
final event = Event(type: 'event.local');
|
|
|
|
when(() => client.sendEvent(channelId, channelType, event))
|
|
.thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.sendEvent(event);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.sendEvent(channelId, channelType, event)).called(1);
|
|
});
|
|
|
|
group('`.sendReaction`', () {
|
|
test('should work fine', () async {
|
|
const type = 'test-reaction-type';
|
|
final message = Message(
|
|
id: 'test-message-id',
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
final reaction = Reaction(type: type, messageId: message.id);
|
|
|
|
when(() => client.sendReaction(message.id, type)).thenAnswer(
|
|
(_) async => SendReactionResponse()
|
|
..message = message
|
|
..reaction = reaction,
|
|
);
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
reactionCounts: {type: 1},
|
|
reactionScores: {type: 1},
|
|
latestReactions: [reaction],
|
|
ownReactions: [reaction],
|
|
),
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.sendReaction(message, type);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.reaction.type, type);
|
|
expect(res.reaction.messageId, message.id);
|
|
|
|
verify(() => client.sendReaction(message.id, type)).called(1);
|
|
});
|
|
|
|
test('should work fine with score passed explicitly', () async {
|
|
const type = 'test-reaction-type';
|
|
final message = Message(
|
|
id: 'test-message-id',
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
const score = 5;
|
|
final reaction = Reaction(
|
|
type: type,
|
|
messageId: message.id,
|
|
score: score,
|
|
);
|
|
|
|
when(() => client.sendReaction(
|
|
message.id,
|
|
type,
|
|
score: score,
|
|
)).thenAnswer(
|
|
(_) async => SendReactionResponse()
|
|
..message = message
|
|
..reaction = reaction,
|
|
);
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
reactionCounts: {type: 1},
|
|
reactionScores: {type: score},
|
|
latestReactions: [reaction],
|
|
ownReactions: [reaction],
|
|
),
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.sendReaction(
|
|
message,
|
|
type,
|
|
score: score,
|
|
);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.reaction.type, type);
|
|
expect(res.reaction.messageId, message.id);
|
|
expect(res.reaction.score, score);
|
|
|
|
verify(() => client.sendReaction(
|
|
message.id,
|
|
type,
|
|
score: score,
|
|
)).called(1);
|
|
});
|
|
|
|
test('should work fine with score passed explicitly and in extraData',
|
|
() async {
|
|
const type = 'test-reaction-type';
|
|
final message = Message(
|
|
id: 'test-message-id',
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
const score = 5;
|
|
const extraDataScore = 3;
|
|
const extraData = {
|
|
'score': extraDataScore,
|
|
};
|
|
final reaction = Reaction(
|
|
type: type,
|
|
messageId: message.id,
|
|
score: extraDataScore,
|
|
);
|
|
|
|
when(() => client.sendReaction(
|
|
message.id,
|
|
type,
|
|
score: score,
|
|
extraData: extraData,
|
|
)).thenAnswer(
|
|
(_) async => SendReactionResponse()
|
|
..message = message
|
|
..reaction = reaction,
|
|
);
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
reactionCounts: {type: 1},
|
|
reactionScores: {type: extraDataScore},
|
|
latestReactions: [reaction],
|
|
ownReactions: [reaction],
|
|
),
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.sendReaction(
|
|
message,
|
|
type,
|
|
score: score,
|
|
extraData: extraData,
|
|
);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.reaction.type, type);
|
|
expect(res.reaction.messageId, message.id);
|
|
expect(
|
|
res.reaction.score,
|
|
extraDataScore,
|
|
);
|
|
|
|
verify(() => client.sendReaction(
|
|
message.id,
|
|
type,
|
|
score: score,
|
|
extraData: extraData,
|
|
)).called(1);
|
|
});
|
|
|
|
test(
|
|
'should restore previous message if `client.sendReaction` throws',
|
|
() async {
|
|
const type = 'test-reaction-type';
|
|
final message = Message(
|
|
id: 'test-message-id',
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
final reaction = Reaction(type: type, messageId: message.id);
|
|
|
|
when(() => client.sendReaction(message.id, type))
|
|
.thenThrow(StreamChatNetworkError(ChatErrorCode.inputError));
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
reactionCounts: {type: 1},
|
|
reactionScores: {type: 1},
|
|
latestReactions: [reaction],
|
|
ownReactions: [reaction],
|
|
),
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
[
|
|
isSameMessageAs(
|
|
message,
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
try {
|
|
await channel.sendReaction(message, type);
|
|
} catch (e) {
|
|
expect(e, isA<StreamChatNetworkError>());
|
|
}
|
|
|
|
verify(() => client.sendReaction(message.id, type)).called(1);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'''should override previous reaction if present and `enforceUnique` is true''',
|
|
() async {
|
|
const userId = 'test-user-id';
|
|
const messageId = 'test-message-id';
|
|
const prevType = 'test-reaction-type';
|
|
final prevReaction = Reaction(
|
|
type: prevType,
|
|
messageId: messageId,
|
|
userId: userId,
|
|
);
|
|
final message = Message(
|
|
id: messageId,
|
|
ownReactions: [prevReaction],
|
|
latestReactions: [prevReaction],
|
|
reactionScores: const {prevType: 1},
|
|
reactionCounts: const {prevType: 1},
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
const type = 'test-reaction-type-2';
|
|
final newReaction = Reaction(
|
|
type: type,
|
|
messageId: messageId,
|
|
userId: userId,
|
|
);
|
|
final newMessage = message.copyWith(
|
|
ownReactions: [newReaction],
|
|
latestReactions: [newReaction],
|
|
);
|
|
|
|
const enforceUnique = true;
|
|
|
|
when(() => client.sendReaction(
|
|
messageId,
|
|
type,
|
|
enforceUnique: enforceUnique,
|
|
)).thenAnswer(
|
|
(_) async => SendReactionResponse()
|
|
..message = newMessage
|
|
..reaction = newReaction,
|
|
);
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
newMessage,
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.sendReaction(
|
|
message,
|
|
type,
|
|
enforceUnique: enforceUnique,
|
|
);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.reaction.type, type);
|
|
expect(res.reaction.messageId, messageId);
|
|
|
|
verify(() => client.sendReaction(
|
|
messageId,
|
|
type,
|
|
enforceUnique: enforceUnique,
|
|
)).called(1);
|
|
},
|
|
);
|
|
});
|
|
|
|
group('`.sendReaction in thread`', () {
|
|
test('should work fine', () async {
|
|
const type = 'test-reaction-type';
|
|
final message = Message(
|
|
id: 'test-message-id',
|
|
parentId: 'test-parent-id', // is thread message
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
final reaction = Reaction(type: type, messageId: message.id);
|
|
|
|
when(() => client.sendReaction(message.id, type)).thenAnswer(
|
|
(_) async => SendReactionResponse()
|
|
..message = message
|
|
..reaction = reaction,
|
|
);
|
|
|
|
expectLater(
|
|
channel.state?.threadsStream
|
|
// skipping first seed message list -> [] messages
|
|
.skip(1)
|
|
.map((event) => event['test-parent-id']),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
reactionCounts: {type: 1},
|
|
reactionScores: {type: 1},
|
|
latestReactions: [reaction],
|
|
ownReactions: [reaction],
|
|
),
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
matchParentId: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.sendReaction(message, type);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.reaction.type, type);
|
|
expect(res.reaction.messageId, message.id);
|
|
|
|
verify(() => client.sendReaction(message.id, type)).called(1);
|
|
});
|
|
|
|
test(
|
|
'''should restore previous thread message if `client.sendReaction` throws''',
|
|
() async {
|
|
const type = 'test-reaction-type';
|
|
final message = Message(
|
|
id: 'test-message-id',
|
|
parentId: 'test-parent-id', // is thread message
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
final reaction = Reaction(type: type, messageId: message.id);
|
|
|
|
when(() => client.sendReaction(message.id, type))
|
|
.thenThrow(StreamChatNetworkError(ChatErrorCode.inputError));
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.threadsStream
|
|
.skip(1)
|
|
.map((event) => event['test-parent-id']),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
reactionCounts: {type: 1},
|
|
reactionScores: {type: 1},
|
|
latestReactions: [reaction],
|
|
ownReactions: [reaction],
|
|
),
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
matchParentId: true,
|
|
),
|
|
],
|
|
[
|
|
isSameMessageAs(
|
|
message,
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
matchParentId: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
try {
|
|
await channel.sendReaction(message, type);
|
|
} catch (e) {
|
|
expect(e, isA<StreamChatNetworkError>());
|
|
}
|
|
|
|
verify(() => client.sendReaction(message.id, type)).called(1);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'''should override previous thread reaction if present and `enforceUnique` is true''',
|
|
() async {
|
|
const userId = 'test-user-id';
|
|
const messageId = 'test-message-id';
|
|
const parentId = 'test-parent-id';
|
|
const prevType = 'test-reaction-type';
|
|
final prevReaction = Reaction(
|
|
type: prevType,
|
|
messageId: messageId,
|
|
userId: userId,
|
|
);
|
|
final message = Message(
|
|
id: messageId,
|
|
parentId: parentId,
|
|
ownReactions: [prevReaction],
|
|
latestReactions: [prevReaction],
|
|
reactionScores: const {prevType: 1},
|
|
reactionCounts: const {prevType: 1},
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
const type = 'test-reaction-type-2';
|
|
final newReaction = Reaction(
|
|
type: type,
|
|
messageId: messageId,
|
|
userId: userId,
|
|
);
|
|
final newMessage = message.copyWith(
|
|
ownReactions: [newReaction],
|
|
latestReactions: [newReaction],
|
|
);
|
|
|
|
const enforceUnique = true;
|
|
|
|
when(() => client.sendReaction(
|
|
messageId,
|
|
type,
|
|
enforceUnique: enforceUnique,
|
|
)).thenAnswer(
|
|
(_) async => SendReactionResponse()
|
|
..message = newMessage
|
|
..reaction = newReaction,
|
|
);
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.threadsStream
|
|
.skip(1)
|
|
.map((event) => event['test-parent-id']),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
newMessage.copyWith(status: MessageSendingStatus.sent),
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
matchParentId: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.sendReaction(
|
|
message,
|
|
type,
|
|
enforceUnique: enforceUnique,
|
|
);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.reaction.type, type);
|
|
expect(res.reaction.messageId, messageId);
|
|
|
|
verify(() => client.sendReaction(
|
|
messageId,
|
|
type,
|
|
enforceUnique: enforceUnique,
|
|
)).called(1);
|
|
},
|
|
);
|
|
});
|
|
|
|
group('`.deleteReaction`', () {
|
|
test('should work fine', () async {
|
|
const userId = 'test-user-id';
|
|
const messageId = 'test-message-id';
|
|
const type = 'test-reaction-type';
|
|
final reaction = Reaction(
|
|
type: type,
|
|
messageId: messageId,
|
|
userId: userId,
|
|
);
|
|
final message = Message(
|
|
id: messageId,
|
|
ownReactions: [reaction],
|
|
latestReactions: [reaction],
|
|
reactionScores: const {type: 1},
|
|
reactionCounts: const {type: 1},
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
when(() => client.deleteReaction(messageId, type))
|
|
.thenAnswer((_) async => EmptyResponse());
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
latestReactions: [],
|
|
ownReactions: [],
|
|
),
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.deleteReaction(message, reaction);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.deleteReaction(messageId, type)).called(1);
|
|
});
|
|
|
|
test(
|
|
'should restore prev message state if `client.deleteReaction` throws',
|
|
() async {
|
|
const userId = 'test-user-id';
|
|
const messageId = 'test-message-id';
|
|
const type = 'test-reaction-type';
|
|
final reaction = Reaction(
|
|
type: type,
|
|
messageId: messageId,
|
|
userId: userId,
|
|
);
|
|
final message = Message(
|
|
id: messageId,
|
|
ownReactions: [reaction],
|
|
latestReactions: [reaction],
|
|
reactionScores: const {type: 1},
|
|
reactionCounts: const {type: 1},
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
when(() => client.deleteReaction(messageId, type))
|
|
.thenThrow(StreamChatNetworkError(ChatErrorCode.inputError));
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
latestReactions: [],
|
|
ownReactions: [],
|
|
),
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
[
|
|
isSameMessageAs(
|
|
message,
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
try {
|
|
await channel.deleteReaction(message, reaction);
|
|
} catch (e) {
|
|
expect(e, isA<StreamChatNetworkError>());
|
|
}
|
|
|
|
verify(() => client.deleteReaction(messageId, type)).called(1);
|
|
},
|
|
);
|
|
});
|
|
|
|
group('`.deleteReaction in thread`', () {
|
|
test('should work fine', () async {
|
|
const userId = 'test-user-id';
|
|
const messageId = 'test-message-id';
|
|
const parentId = 'test-parent-id';
|
|
const type = 'test-reaction-type';
|
|
final reaction = Reaction(
|
|
type: type,
|
|
messageId: messageId,
|
|
userId: userId,
|
|
);
|
|
final message = Message(
|
|
id: messageId,
|
|
parentId: parentId,
|
|
// is thread
|
|
ownReactions: [reaction],
|
|
latestReactions: [reaction],
|
|
reactionScores: const {type: 1},
|
|
reactionCounts: const {type: 1},
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
when(() => client.deleteReaction(messageId, type))
|
|
.thenAnswer((_) async => EmptyResponse());
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.threadsStream
|
|
.skip(1)
|
|
.map((event) => event['test-parent-id']),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
latestReactions: [],
|
|
ownReactions: [],
|
|
),
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
matchParentId: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.deleteReaction(message, reaction);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.deleteReaction(messageId, type)).called(1);
|
|
});
|
|
|
|
test(
|
|
'should restore prev message state if `client.deleteReaction` throws',
|
|
() async {
|
|
const userId = 'test-user-id';
|
|
const messageId = 'test-message-id';
|
|
const parentId = 'test-parent-id';
|
|
const type = 'test-reaction-type';
|
|
final reaction = Reaction(
|
|
type: type,
|
|
messageId: messageId,
|
|
userId: userId,
|
|
);
|
|
final message = Message(
|
|
id: messageId,
|
|
parentId: parentId,
|
|
ownReactions: [reaction],
|
|
latestReactions: [reaction],
|
|
reactionScores: const {type: 1},
|
|
reactionCounts: const {type: 1},
|
|
status: MessageSendingStatus.sent,
|
|
);
|
|
|
|
when(() => client.deleteReaction(messageId, type))
|
|
.thenThrow(StreamChatNetworkError(ChatErrorCode.inputError));
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.threadsStream
|
|
.skip(1)
|
|
.map((event) => event['test-parent-id']),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message.copyWith(
|
|
status: MessageSendingStatus.sent,
|
|
latestReactions: [],
|
|
ownReactions: [],
|
|
),
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
matchParentId: true,
|
|
),
|
|
],
|
|
[
|
|
isSameMessageAs(
|
|
message,
|
|
matchReactions: true,
|
|
matchSendingStatus: true,
|
|
matchParentId: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
try {
|
|
await channel.deleteReaction(message, reaction);
|
|
} catch (e) {
|
|
expect(e, isA<StreamChatNetworkError>());
|
|
}
|
|
|
|
verify(() => client.deleteReaction(messageId, type)).called(1);
|
|
},
|
|
);
|
|
});
|
|
|
|
test('`.update`', () async {
|
|
const channelData = {
|
|
'name': 'Stream Team',
|
|
'profile_image': 'test-profile-image',
|
|
};
|
|
final updateMessage = Message(
|
|
id: 'test-message-id',
|
|
text: 'updated channel',
|
|
);
|
|
|
|
final channelModel = ChannelModel(
|
|
cid: channelCid,
|
|
extraData: channelData,
|
|
);
|
|
|
|
when(() => client.updateChannel(channelId, channelType, channelData,
|
|
message: any(named: 'message'))).thenAnswer(
|
|
(_) async => UpdateChannelResponse()
|
|
..channel = channelModel
|
|
..message = updateMessage,
|
|
);
|
|
|
|
final res = await channel.update(
|
|
channelData,
|
|
updateMessage: updateMessage,
|
|
);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.channel.cid, channelModel.cid);
|
|
expect(res.channel.extraData, channelData);
|
|
expect(res.message?.id, updateMessage.id);
|
|
|
|
verify(() => client.updateChannel(channelId, channelType, channelData,
|
|
message: any(named: 'message'))).called(1);
|
|
});
|
|
|
|
test('`.updateImage`', () async {
|
|
const image = 'https://getstream.io/new-image';
|
|
|
|
final channelModel = ChannelModel(
|
|
cid: channelCid,
|
|
extraData: {'image': image},
|
|
);
|
|
|
|
when(() => client.updateChannelPartial(
|
|
channelId,
|
|
channelType,
|
|
set: {'image': image},
|
|
)).thenAnswer(
|
|
(_) async => PartialUpdateChannelResponse()..channel = channelModel,
|
|
);
|
|
|
|
final res = await channel.updateImage(image);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.channel.extraData['image'], image);
|
|
|
|
verify(() => client.updateChannelPartial(
|
|
channelId,
|
|
channelType,
|
|
set: {'image': image},
|
|
)).called(1);
|
|
});
|
|
|
|
test('`.updateName`', () async {
|
|
const name = 'Name';
|
|
|
|
final channelModel = ChannelModel(
|
|
cid: channelCid,
|
|
extraData: {'name': name},
|
|
);
|
|
|
|
when(() => client.updateChannelPartial(
|
|
channelId,
|
|
channelType,
|
|
set: {'name': name},
|
|
)).thenAnswer(
|
|
(_) async => PartialUpdateChannelResponse()..channel = channelModel,
|
|
);
|
|
|
|
final res = await channel.updateName(name);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.channel.extraData['name'], name);
|
|
|
|
verify(() => client.updateChannelPartial(
|
|
channelId,
|
|
channelType,
|
|
set: {'name': name},
|
|
)).called(1);
|
|
});
|
|
|
|
test('`.updatePartial`', () async {
|
|
const set = {
|
|
'name': 'Stream Team',
|
|
'profile_image': 'test-profile-image',
|
|
};
|
|
|
|
const unset = ['tag', 'last_name'];
|
|
|
|
final channelModel = ChannelModel(
|
|
cid: channelCid,
|
|
extraData: {
|
|
'coolness': 999,
|
|
...set,
|
|
},
|
|
);
|
|
|
|
when(() => client.updateChannelPartial(
|
|
channelId,
|
|
channelType,
|
|
set: set,
|
|
unset: unset,
|
|
)).thenAnswer(
|
|
(_) async => PartialUpdateChannelResponse()..channel = channelModel,
|
|
);
|
|
|
|
final res = await channel.updatePartial(set: set, unset: unset);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.channel.cid, channelModel.cid);
|
|
expect(
|
|
res.channel.extraData,
|
|
{'coolness': 999, ...set},
|
|
);
|
|
|
|
verify(() => client.updateChannelPartial(
|
|
channelId,
|
|
channelType,
|
|
set: set,
|
|
unset: unset,
|
|
)).called(1);
|
|
});
|
|
|
|
test('`.delete`', () async {
|
|
when(() => client.deleteChannel(channelId, channelType))
|
|
.thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.delete();
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.deleteChannel(channelId, channelType)).called(1);
|
|
});
|
|
|
|
test('`.truncate`', () async {
|
|
when(() => client.truncateChannel(channelId, channelType))
|
|
.thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.truncate();
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.truncateChannel(channelId, channelType)).called(1);
|
|
});
|
|
|
|
test('`.acceptInvite`', () async {
|
|
final message = Message(id: 'test-message-id', text: 'Invite Accepted');
|
|
|
|
final channelModel = ChannelModel(cid: channelCid);
|
|
|
|
when(() => client.acceptChannelInvite(channelId, channelType,
|
|
message: any(named: 'message'))).thenAnswer(
|
|
(_) async => AcceptInviteResponse()
|
|
..channel = channelModel
|
|
..message = message,
|
|
);
|
|
|
|
final res = await channel.acceptInvite(message);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.channel.cid, channelModel.cid);
|
|
expect(res.message?.id, message.id);
|
|
|
|
verify(() => client.acceptChannelInvite(channelId, channelType,
|
|
message: any(named: 'message'))).called(1);
|
|
});
|
|
|
|
test('`.rejectInvite`', () async {
|
|
final message = Message(id: 'test-message-id', text: 'Invite Rejected');
|
|
|
|
final channelModel = ChannelModel(cid: channelCid);
|
|
|
|
when(() => client.rejectChannelInvite(channelId, channelType,
|
|
message: any(named: 'message'))).thenAnswer(
|
|
(_) async => RejectInviteResponse()
|
|
..channel = channelModel
|
|
..message = message,
|
|
);
|
|
|
|
final res = await channel.rejectInvite(message);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.channel.cid, channelModel.cid);
|
|
expect(res.message?.id, message.id);
|
|
|
|
verify(() => client.rejectChannelInvite(channelId, channelType,
|
|
message: any(named: 'message'))).called(1);
|
|
});
|
|
|
|
test('`.addMembers`', () async {
|
|
final members = List.generate(
|
|
3,
|
|
(index) => Member(userId: 'test-member-id-$index'),
|
|
);
|
|
final memberIds = members
|
|
.map((it) => it.userId)
|
|
.whereType<String>()
|
|
.toList(growable: false);
|
|
final message = Message(id: 'test-message-id', text: 'Members Added');
|
|
|
|
final channelModel = ChannelModel(cid: channelCid);
|
|
|
|
when(() => client.addChannelMembers(channelId, channelType, memberIds,
|
|
message: any(named: 'message'))).thenAnswer(
|
|
(_) async => AddMembersResponse()
|
|
..channel = channelModel
|
|
..members = members
|
|
..message = message,
|
|
);
|
|
|
|
final res = await channel.addMembers(memberIds, message: message);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.channel.cid, channelModel.cid);
|
|
expect(res.members.length, members.length);
|
|
expect(res.message?.id, message.id);
|
|
|
|
verify(() => client.addChannelMembers(channelId, channelType, memberIds,
|
|
message: any(named: 'message'))).called(1);
|
|
});
|
|
|
|
test('`.inviteMembers`', () async {
|
|
final members = List.generate(
|
|
3,
|
|
(index) => Member(userId: 'test-member-id-$index'),
|
|
);
|
|
final memberIds = members
|
|
.map((it) => it.userId)
|
|
.whereType<String>()
|
|
.toList(growable: false);
|
|
final message = Message(id: 'test-message-id', text: 'Members Invited');
|
|
|
|
final channelModel = ChannelModel(cid: channelCid);
|
|
|
|
when(() => client.inviteChannelMembers(channelId, channelType, memberIds,
|
|
message: any(named: 'message'))).thenAnswer(
|
|
(_) async => InviteMembersResponse()
|
|
..channel = channelModel
|
|
..members = members
|
|
..message = message,
|
|
);
|
|
|
|
final res = await channel.inviteMembers(memberIds, message: message);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.channel.cid, channelModel.cid);
|
|
expect(res.members.length, members.length);
|
|
expect(res.message?.id, message.id);
|
|
|
|
verify(() => client.inviteChannelMembers(
|
|
channelId, channelType, memberIds,
|
|
message: any(named: 'message'))).called(1);
|
|
});
|
|
|
|
test('`.removeMembers`', () async {
|
|
final members = List.generate(
|
|
3,
|
|
(index) => Member(userId: 'test-member-id-$index'),
|
|
);
|
|
final memberIds = members
|
|
.map((it) => it.userId)
|
|
.whereType<String>()
|
|
.toList(growable: false);
|
|
final message = Message(id: 'test-message-id', text: 'Members Removed');
|
|
|
|
final channelModel = ChannelModel(cid: channelCid);
|
|
|
|
when(() => client.removeChannelMembers(channelId, channelType, memberIds,
|
|
message: any(named: 'message'))).thenAnswer(
|
|
(_) async => RemoveMembersResponse()
|
|
..channel = channelModel
|
|
..members = members
|
|
..message = message,
|
|
);
|
|
|
|
final res = await channel.removeMembers(memberIds, message: message);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.channel.cid, channelModel.cid);
|
|
expect(res.members.length, members.length);
|
|
expect(res.message?.id, message.id);
|
|
|
|
verify(() => client.removeChannelMembers(
|
|
channelId, channelType, memberIds,
|
|
message: any(named: 'message'))).called(1);
|
|
});
|
|
|
|
group('`.sendAction`', () {
|
|
test('should work fine', () async {
|
|
final message = Message(id: 'test-message-id', text: 'Action Sent');
|
|
const formData = {'key': 'value'};
|
|
|
|
when(
|
|
() => client.sendAction(channelId, channelType, message.id, formData),
|
|
).thenAnswer((_) async => SendActionResponse());
|
|
|
|
final res = await channel.sendAction(message, formData);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(
|
|
() => client.sendAction(channelId, channelType, message.id, formData),
|
|
).called(1);
|
|
});
|
|
|
|
test('should emit received message if not null', () async {
|
|
final message = Message(id: 'test-message-id', text: 'Action Sent');
|
|
const formData = {'key': 'value'};
|
|
|
|
when(
|
|
() => client.sendAction(channelId, channelType, message.id, formData),
|
|
).thenAnswer((_) async => SendActionResponse()..message = message);
|
|
|
|
expectLater(
|
|
// skipping first seed message list -> [] messages
|
|
channel.state?.messagesStream.skip(1),
|
|
emitsInOrder([
|
|
[
|
|
isSameMessageAs(
|
|
message,
|
|
matchSendingStatus: true,
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
final res = await channel.sendAction(message, formData);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.message?.id, message.id);
|
|
|
|
verify(
|
|
() => client.sendAction(channelId, channelType, message.id, formData),
|
|
).called(1);
|
|
});
|
|
});
|
|
|
|
test('`.markRead`', () async {
|
|
const messageId = 'test-message-id';
|
|
|
|
when(() => client.markChannelRead(channelId, channelType,
|
|
messageId: messageId)).thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.markRead(messageId: messageId);
|
|
|
|
expect(res, isNotNull);
|
|
expect(client.state.totalUnreadCount, 0);
|
|
|
|
verify(() => client.markChannelRead(channelId, channelType,
|
|
messageId: messageId)).called(1);
|
|
});
|
|
|
|
group('`.watch`', () {
|
|
test('should work fine', () async {
|
|
when(() => client.queryChannel(
|
|
channelType,
|
|
channelId: channelId,
|
|
watch: true,
|
|
channelData: any(named: 'channelData'),
|
|
messagesPagination: any(named: 'messagesPagination'),
|
|
membersPagination: any(named: 'membersPagination'),
|
|
watchersPagination: any(named: 'watchersPagination'),
|
|
)).thenAnswer(
|
|
(_) async => _generateChannelState(channelId, channelType),
|
|
);
|
|
|
|
final res = await channel.watch();
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.channel, isNotNull);
|
|
expect(res.channel?.cid, channelCid);
|
|
|
|
verify(() => client.queryChannel(
|
|
channelType,
|
|
channelId: channelId,
|
|
watch: true,
|
|
channelData: any(named: 'channelData'),
|
|
messagesPagination: any(named: 'messagesPagination'),
|
|
membersPagination: any(named: 'membersPagination'),
|
|
watchersPagination: any(named: 'watchersPagination'),
|
|
)).called(1);
|
|
});
|
|
|
|
test('should rethrow if `.query` throws', () async {
|
|
when(() => client.queryChannel(
|
|
channelType,
|
|
channelId: channelId,
|
|
watch: true,
|
|
channelData: any(named: 'channelData'),
|
|
messagesPagination: any(named: 'messagesPagination'),
|
|
membersPagination: any(named: 'membersPagination'),
|
|
watchersPagination: any(named: 'watchersPagination'),
|
|
)).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError));
|
|
|
|
try {
|
|
await channel.watch();
|
|
} catch (e) {
|
|
expect(e, isA<StreamChatNetworkError>());
|
|
}
|
|
|
|
verify(() => client.queryChannel(
|
|
channelType,
|
|
channelId: channelId,
|
|
watch: true,
|
|
channelData: any(named: 'channelData'),
|
|
messagesPagination: any(named: 'messagesPagination'),
|
|
membersPagination: any(named: 'membersPagination'),
|
|
watchersPagination: any(named: 'watchersPagination'),
|
|
)).called(1);
|
|
});
|
|
});
|
|
|
|
test('`.stopWatching`', () async {
|
|
when(() => client.stopChannelWatching(channelId, channelType))
|
|
.thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.stopWatching();
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.stopChannelWatching(channelId, channelType))
|
|
.called(1);
|
|
});
|
|
|
|
test('`.getReplies`', () async {
|
|
const parentId = 'test-parent-id';
|
|
|
|
final messages = List.generate(
|
|
3,
|
|
(index) => Message(
|
|
id: 'test-message-id-$index',
|
|
parentId: parentId,
|
|
),
|
|
);
|
|
|
|
when(() => client.getReplies(parentId)).thenAnswer(
|
|
(_) async => QueryRepliesResponse()..messages = messages,
|
|
);
|
|
|
|
final res = await channel.getReplies(parentId);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.messages.length, messages.length);
|
|
expect(res.messages.every((it) => it.parentId == parentId), isTrue);
|
|
|
|
verify(() => client.getReplies(parentId)).called(1);
|
|
});
|
|
|
|
test('`.getReactions`', () async {
|
|
const messageId = 'test-message-id';
|
|
|
|
final reactions = List.generate(
|
|
3,
|
|
(index) => Reaction(
|
|
type: 'test-reaction-type-$index',
|
|
messageId: messageId,
|
|
),
|
|
);
|
|
|
|
when(() => client.getReactions(messageId)).thenAnswer(
|
|
(_) async => QueryReactionsResponse()..reactions = reactions,
|
|
);
|
|
|
|
final res = await channel.getReactions(messageId);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.reactions.length, reactions.length);
|
|
expect(res.reactions.every((it) => it.messageId == messageId), isTrue);
|
|
|
|
verify(() => client.getReactions(messageId)).called(1);
|
|
});
|
|
|
|
test('`.getMessagesById`', () async {
|
|
final messages = List.generate(
|
|
3,
|
|
(index) => Message(id: 'test-message-id-$index'),
|
|
);
|
|
|
|
final messageIds = messages.map((it) => it.id).toList(growable: false);
|
|
|
|
when(() => client.getMessagesById(channelId, channelType, messageIds))
|
|
.thenAnswer(
|
|
(_) async => GetMessagesByIdResponse()..messages = messages,
|
|
);
|
|
|
|
final res = await channel.getMessagesById(messageIds);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.messages.length, messageIds.length);
|
|
|
|
verify(
|
|
() => client.getMessagesById(channelId, channelType, messageIds),
|
|
).called(1);
|
|
});
|
|
|
|
test('`.translateMessage`', () async {
|
|
const messageId = 'test-message-id';
|
|
const language = 'hi'; // Hindi
|
|
const translatedMessageText = 'नमस्ते';
|
|
|
|
final translatedMessage = Message(
|
|
i18n: const {
|
|
language: translatedMessageText,
|
|
},
|
|
);
|
|
|
|
when(() => client.translateMessage(messageId, language)).thenAnswer(
|
|
(_) async => TranslateMessageResponse()..message = translatedMessage,
|
|
);
|
|
|
|
final res = await channel.translateMessage(messageId, language);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.message.i18n, translatedMessage.i18n);
|
|
|
|
verify(() => client.translateMessage(messageId, language)).called(1);
|
|
});
|
|
|
|
group('`.query`', () {
|
|
test('should work fine', () async {
|
|
final channelState = _generateChannelState(channelId, channelType);
|
|
|
|
when(
|
|
() => client.queryChannel(
|
|
channelType,
|
|
channelId: channelId,
|
|
channelData: any(named: 'channelData'),
|
|
messagesPagination: any(named: 'messagesPagination'),
|
|
membersPagination: any(named: 'membersPagination'),
|
|
watchersPagination: any(named: 'watchersPagination'),
|
|
),
|
|
).thenAnswer((_) async => channelState);
|
|
|
|
final res = await channel.query();
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(
|
|
() => client.queryChannel(
|
|
channelType,
|
|
channelId: channelId,
|
|
channelData: any(named: 'channelData'),
|
|
messagesPagination: any(named: 'messagesPagination'),
|
|
membersPagination: any(named: 'membersPagination'),
|
|
watchersPagination: any(named: 'watchersPagination'),
|
|
),
|
|
).called(1);
|
|
});
|
|
|
|
test('should rethrow if `client.queryChannel` throws', () async {
|
|
when(
|
|
() => client.queryChannel(
|
|
channelType,
|
|
channelId: channelId,
|
|
channelData: any(named: 'channelData'),
|
|
messagesPagination: any(named: 'messagesPagination'),
|
|
membersPagination: any(named: 'membersPagination'),
|
|
watchersPagination: any(named: 'watchersPagination'),
|
|
),
|
|
).thenThrow(StreamChatNetworkError(ChatErrorCode.inputError));
|
|
|
|
try {
|
|
await channel.query();
|
|
} catch (e) {
|
|
expect(e, isA<StreamChatNetworkError>());
|
|
}
|
|
|
|
verify(
|
|
() => client.queryChannel(
|
|
channelType,
|
|
channelId: channelId,
|
|
channelData: any(named: 'channelData'),
|
|
messagesPagination: any(named: 'messagesPagination'),
|
|
membersPagination: any(named: 'membersPagination'),
|
|
watchersPagination: any(named: 'watchersPagination'),
|
|
),
|
|
).called(1);
|
|
});
|
|
});
|
|
|
|
test('`.queryMembers`', () async {
|
|
final filter = Filter.in_('cid', const [channelCid]);
|
|
|
|
final members = List.generate(
|
|
3,
|
|
(index) => Member(userId: 'test-user-id-$index'),
|
|
);
|
|
|
|
when(() => client.queryMembers(
|
|
channelType,
|
|
channelId: channelId,
|
|
filter: filter,
|
|
members: any(named: 'members'),
|
|
sort: any(named: 'sort'),
|
|
pagination: any(named: 'pagination'),
|
|
)).thenAnswer((_) async => QueryMembersResponse()..members = members);
|
|
|
|
final res = await channel.queryMembers(filter: filter);
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.members.length, members.length);
|
|
|
|
verify(() => client.queryMembers(
|
|
channelType,
|
|
channelId: channelId,
|
|
filter: filter,
|
|
members: any(named: 'members'),
|
|
sort: any(named: 'sort'),
|
|
pagination: any(named: 'pagination'),
|
|
)).called(1);
|
|
});
|
|
|
|
test('`.queryBannedUsers`', () async {
|
|
final filter = Filter.equal('channel_cid', channelCid);
|
|
|
|
final bans = List.generate(
|
|
3,
|
|
(index) => BannedUser(
|
|
user: User(id: 'test-user-id-$index'),
|
|
bannedBy: User(id: 'test-user-id-${index + 1}'),
|
|
),
|
|
);
|
|
|
|
when(() => client.queryBannedUsers(
|
|
filter: filter,
|
|
sort: any(named: 'sort'),
|
|
pagination: any(named: 'pagination'),
|
|
)).thenAnswer((_) async => QueryBannedUsersResponse()..bans = bans);
|
|
|
|
final res = await channel.queryBannedUsers();
|
|
|
|
expect(res, isNotNull);
|
|
expect(res.bans.length, bans.length);
|
|
|
|
verify(() => client.queryBannedUsers(
|
|
filter: filter,
|
|
sort: any(named: 'sort'),
|
|
pagination: any(named: 'pagination'),
|
|
)).called(1);
|
|
});
|
|
|
|
test('`.mute`', () async {
|
|
when(() => client.muteChannel(
|
|
channelCid,
|
|
expiration: any(named: 'expiration'),
|
|
)).thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.mute();
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.muteChannel(
|
|
channelCid,
|
|
expiration: any(named: 'expiration'),
|
|
)).called(1);
|
|
});
|
|
|
|
test('`.unmute`', () async {
|
|
when(
|
|
() => client.unmuteChannel(channelCid),
|
|
).thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.unmute();
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(
|
|
() => client.unmuteChannel(channelCid),
|
|
).called(1);
|
|
});
|
|
|
|
test('`.enableSlowMode`', () async {
|
|
const cooldown = 10;
|
|
|
|
final channelModel = ChannelModel(
|
|
cid: channelCid,
|
|
cooldown: cooldown,
|
|
);
|
|
|
|
when(() => client.enableSlowdown(
|
|
channelId,
|
|
channelType,
|
|
cooldown,
|
|
)).thenAnswer((_) async => PartialUpdateChannelResponse()
|
|
..channel = channelModel);
|
|
|
|
final res = await channel.enableSlowMode(cooldownInterval: 10);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.enableSlowdown(
|
|
channelId,
|
|
channelType,
|
|
cooldown,
|
|
)).called(1);
|
|
});
|
|
|
|
test('`.disableSlowMode`', () async {
|
|
final channelModel = ChannelModel(
|
|
cid: channelCid,
|
|
);
|
|
|
|
when(() => client.disableSlowdown(
|
|
channelId,
|
|
channelType,
|
|
)).thenAnswer((_) async => PartialUpdateChannelResponse()
|
|
..channel = channelModel);
|
|
|
|
final res = await channel.disableSlowMode();
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.disableSlowdown(channelId, channelType)).called(1);
|
|
});
|
|
|
|
test('`.banUser`', () async {
|
|
const userId = 'test-user-id';
|
|
const options = {'key': 'value'};
|
|
|
|
when(() => client.banUser(
|
|
userId,
|
|
{'type': channelType, 'id': channelId, ...options},
|
|
)).thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.banMember(userId, options);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.banUser(
|
|
userId,
|
|
{'type': channelType, 'id': channelId, ...options},
|
|
)).called(1);
|
|
});
|
|
|
|
test('`.unbanUser`', () async {
|
|
const userId = 'test-user-id';
|
|
|
|
when(() => client.unbanUser(userId, any()))
|
|
.thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.unbanMember(userId);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.unbanUser(userId, any())).called(1);
|
|
});
|
|
|
|
test('`.shadowBan`', () async {
|
|
const userId = 'test-user-id';
|
|
const options = {'key': 'value'};
|
|
|
|
when(() => client.shadowBan(
|
|
userId,
|
|
{'type': channelType, 'id': channelId, ...options},
|
|
)).thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.shadowBan(userId, options);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.shadowBan(
|
|
userId,
|
|
{'type': channelType, 'id': channelId, ...options},
|
|
)).called(1);
|
|
});
|
|
|
|
test('`.removeShadowBan`', () async {
|
|
const userId = 'test-user-id';
|
|
|
|
when(() => client.removeShadowBan(userId, any()))
|
|
.thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.removeShadowBan(userId);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.removeShadowBan(userId, any())).called(1);
|
|
});
|
|
|
|
test('`.hide`', () async {
|
|
const clearHistory = true;
|
|
|
|
when(() => client.hideChannel(
|
|
channelId,
|
|
channelType,
|
|
clearHistory: clearHistory,
|
|
)).thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.hide(clearHistory: clearHistory);
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.hideChannel(
|
|
channelId,
|
|
channelType,
|
|
clearHistory: clearHistory,
|
|
)).called(1);
|
|
});
|
|
|
|
test('`.show`', () async {
|
|
when(() => client.showChannel(channelId, channelType))
|
|
.thenAnswer((_) async => EmptyResponse());
|
|
|
|
final res = await channel.show();
|
|
|
|
expect(res, isNotNull);
|
|
|
|
verify(() => client.showChannel(channelId, channelType)).called(1);
|
|
});
|
|
|
|
test('`.on`', () async {
|
|
const eventType = 'test.event';
|
|
final event = Event(type: eventType, cid: channelCid);
|
|
|
|
when(() => client.on(eventType, any(), any(), any()))
|
|
.thenAnswer((_) => Stream.value(event));
|
|
|
|
expectLater(channel.on(eventType), emitsInOrder([event]));
|
|
|
|
verify(() => client.on(eventType, any(), any(), any())).called(1);
|
|
});
|
|
|
|
group(
|
|
'`.keyStroke`',
|
|
() {
|
|
test('should return if `config.typingEvents` is false', () async {
|
|
when(() => channel.config?.typingEvents).thenReturn(false);
|
|
|
|
final typingEvent = Event(type: EventType.typingStart);
|
|
|
|
await channel.keyStroke();
|
|
|
|
verifyNever(() => client.sendEvent(
|
|
channelId,
|
|
channelType,
|
|
any(that: isSameEventAs(typingEvent)),
|
|
));
|
|
});
|
|
|
|
test(
|
|
'''should send `typingStart` event if there is not already a typingEvent or the difference between the two is > 3 seconds''',
|
|
() async {
|
|
final startTypingEvent = Event(type: EventType.typingStart);
|
|
final stopTypingEvent = Event(type: EventType.typingStop);
|
|
|
|
when(() => channel.config?.typingEvents).thenReturn(true);
|
|
|
|
when(() => client.sendEvent(
|
|
channelId,
|
|
channelType,
|
|
any(that: isSameEventAs(startTypingEvent)),
|
|
)).thenAnswer((_) async => EmptyResponse());
|
|
when(() => client.sendEvent(
|
|
channelId,
|
|
channelType,
|
|
any(that: isSameEventAs(stopTypingEvent)),
|
|
)).thenAnswer((_) async => EmptyResponse());
|
|
|
|
await channel.keyStroke();
|
|
|
|
verify(() => client.sendEvent(
|
|
channelId,
|
|
channelType,
|
|
any(that: isSameEventAs(startTypingEvent)),
|
|
)).called(1);
|
|
verify(() => client.sendEvent(
|
|
channelId,
|
|
channelType,
|
|
any(that: isSameEventAs(stopTypingEvent)),
|
|
)).called(1);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
group('`.stopTyping`', () {
|
|
test('should return if `config.typingEvents` is false', () async {
|
|
when(() => channel.config?.typingEvents).thenReturn(false);
|
|
|
|
final typingStopEvent = Event(type: EventType.typingStop);
|
|
|
|
await channel.stopTyping();
|
|
|
|
verifyNever(() => client.sendEvent(
|
|
channelId,
|
|
channelType,
|
|
any(that: isSameEventAs(typingStopEvent)),
|
|
));
|
|
});
|
|
|
|
test('should send `typingStop` successfully', () async {
|
|
final typingStopEvent = Event(type: EventType.typingStop);
|
|
|
|
when(() => channel.config?.typingEvents).thenReturn(true);
|
|
|
|
when(() => client.sendEvent(
|
|
channelId,
|
|
channelType,
|
|
any(that: isSameEventAs(typingStopEvent)),
|
|
)).thenAnswer((_) async => EmptyResponse());
|
|
|
|
await channel.stopTyping();
|
|
|
|
verify(() => client.sendEvent(
|
|
channelId,
|
|
channelType,
|
|
any(that: isSameEventAs(typingStopEvent)),
|
|
)).called(1);
|
|
});
|
|
});
|
|
});
|
|
}
|