V5 (#1222)
* chore(flutter): change BoxFit of VLC player * chore(flutter): remove unused imports * chore(flutter sample): tweak breakpoints, ignore some lints * chore(flutter): minor code readability improvements * chore(flutter): extract out platform_widget_builder to its own "package" * fix(flutter): fix a theme bug with PlatformDialog * chore(persistence): test plugin on macOS * Generated the macos runner for the example * Re-generated the android runner because it was using the v1 embedding * chore(persistence): test plugin on web * chore(persistence): add Windows runner to example * chore(persistence): add linux runner to example * feat(flutter): branded context menus * chore(flutter): fix bug where lines would be shown in between menu items * chore(flutter): import/export fixes * chore(flutter): move file * chore(flutter): /src/channel Also refactors several /channel widgets to use class widgets instead of methods. Some methods, however, have been left as those cases do actually produce cleaner code. chore(flutter): fix bad directory placement chore(flutter): fix bad directory placement * chore(flutter): more directory organization * chore(flutter): navigator improvements * chore(flutter): void async improvements * chore(flutter): fix imports/exports * docs(flutter): documentation updates for /attachment * docs(flutter): doc updates For: * /avatars * /bottom_sheets * /channel * feat(flutter): selectable message text on web & desktop * Squashed commit of the following: commit 50b249edb6b2ee2d744e4e00e97f81979026aab4 Author: GroovinChip <[email protected]> Date: Wed Mar 16 14:55:05 2022 -0400 chore(flutter): wrapping up docs commit 7d289ae45e20c0109bcdb4496f7b693e2adc4ef9 Author: GroovinChip <[email protected]> Date: Wed Mar 16 14:42:09 2022 -0400 chore(flutter): more docs & typedefs commit 6b36f6f3864f15b52e441d58ee7e62577ff8f843 Author: GroovinChip <[email protected]> Date: Wed Mar 16 14:22:34 2022 -0400 chore(flutter): more doc fixes commit 1a9a6de3c836e9ee5d6373033f1d9a7da830bddc Merge: 59188f5d 88abf99f Author: GroovinChip <[email protected]> Date: Wed Mar 16 13:17:52 2022 -0400 Merge branch 'desk_web_dev' into utils commit 59188f5d9c1dd68edd531086cdfd9d42faf93fc8 Author: GroovinChip <[email protected]> Date: Wed Mar 16 12:48:16 2022 -0400 chore(flutter): add todo commit 9b4a986ca5757f284677eb5335637d11ccb1d642 Author: GroovinChip <[email protected]> Date: Wed Mar 16 12:43:12 2022 -0400 chore(flutter): more docs and typedefs updates commit 8786d2dd82b94f673b36371fb2b33a1ebcc9840a Author: GroovinChip <[email protected]> Date: Wed Mar 16 12:14:14 2022 -0400 chore(flutter): more typedef and doc template updates commit 9e906beb0e7a054c13a32c8c8c5b0e2a9e27bb21 Author: GroovinChip <[email protected]> Date: Wed Mar 16 10:46:04 2022 -0400 chore(flutter): create /uitils and move files Fixes subsequent imports/exports * chore(flutter): update file_selector plugin * chore(flutter): include utils/typedefs.dart in library export * Squashed commit of the following: commit 501228afa614b0d740858dec1ebedd3f671e2c96 Author: GroovinChip <[email protected]> Date: Thu Mar 17 16:18:12 2022 -0400 feat(flutter): ensure fullscreen media feature works on ALL platforms commit cd0aab92bc7d90af03258829a5aae66125578af5 Author: GroovinChip <[email protected]> Date: Thu Mar 17 14:26:48 2022 -0400 feat(flutter): working conditional import for initializing VLC * fix(flutter): remove stray import causing web to break * chore(flutter): ensure context menus can show on web in sample * feat(flutter) FullScreenMedia context menus for Web * feat(flutter): branded context menus * chore(flutter): no success dialog on file download (web) * chore(flutter): remove platform UI packages * chore(flutter): properly show/hide delete message menu item Also deletes deprecated PlatformDialog * feat(flutter): 'view info' channel preview context menu * feat(flutter): more menu actions for ChannelPreview Also new ConfirmationDialog and add missing docs * feat(flutter/localizations): translations related to channel mutes * chore(flutter): remove prints and fix super.didChangeDependencies order * chore(flutter): rename some things in ChannelBottomSheet * chore(flutter): more sensible name in TypingIndicator * chore(flutter): remove unused code in MessageWidget * feat(flutter): ChannelInfoDialog * chore(flutter): organize files more Also refactors out some internal MessageListView widgets * fix(flutter): fix render overflow issue * chore(flutter): move MediaListView * chore(flutter): remove unused imports * fix(flutter): fix weird share menu placement on macos * chore(flutter): clean up fullscreenmedia conditional imports * chore(flutter): adjust package exports * chore(flutter): update dart_vlc * chore(flutter): clean up platform checks * chore(flutter): run flutter format on lib * chore(flutter): minor sample cleanup * chore(flutter): ensure vlc doesn't get initialized in non-desktop testing environments * test(flutter): reformat existing tests * fix(flutter): pass onThreadReplyTap down to ThreadReplyButton * First round of new tests commit a90bed427521123fdaa4c5e276744a224fac2b68 Author: GroovinChip <[email protected]> Date: Thu Apr 7 12:34:19 2022 -0400 test(flutter): Username test commit d7bc179c20eae2713c9248191518bf1d955104a9 Author: GroovinChip <[email protected]> Date: Thu Apr 7 12:30:47 2022 -0400 chore(flutter): remove unused imports commit 09c9517de2917dc5ccd7de019b50a7875386991c Author: GroovinChip <[email protected]> Date: Thu Apr 7 12:29:58 2022 -0400 test(flutter): ThreadSeparator test commit 9422d1129211aaaac9a51ca4d29fe94bde06593c Author: GroovinChip <[email protected]> Date: Thu Apr 7 12:19:43 2022 -0400 chore(flutter): rename LoadingIndicator parameter commit 63af943a1a16e78a1e0377d76176420bd7e1fbc7 Author: GroovinChip <[email protected]> Date: Thu Apr 7 11:47:46 2022 -0400 test(flutter): FloatingDateDivider test commit d8e67cf6fca2b76ac091a9047104c0910d5fdfef Author: GroovinChip <[email protected]> Date: Tue Mar 29 15:19:39 2022 -0400 test(flutter): InputAttachment tests commit d2b7d5dd3304c8be6f74bf5b0281d7a95dd027ec Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:41:40 2022 -0400 test(flutter): DmCheckbox tests commit 01567f235a2f00880bb530b232489e3790159b25 Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:16:08 2022 -0400 chore(flutter): remove unused imports commit c9625858e840df6082506ae9374f1d2710053cbd Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:15:44 2022 -0400 test(flutter): CountdownButton tests commit d770474bb11ee8b36a3b0e18db31695fd812a6b3 Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:11:37 2022 -0400 test(flutter): CommandButton tests commit 95e63706d1dd971373d39d471894a38bb2a634f2 Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:07:43 2022 -0400 test(flutter): ClearInputItem tests commit 4156586945c91408cb7943b6c57c2ae560a2857c Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:03:59 2022 -0400 test(flutter): AttachmentButton tests commit bba042806326999aabf338636c6e33900de86160 Author: GroovinChip <[email protected]> Date: Tue Mar 29 13:00:42 2022 -0400 test(flutter): golden test for SendButton commit e2cd0f00b9eeb8d81399f6316edb298234de12dc Author: GroovinChip <[email protected]> Date: Tue Mar 29 12:56:52 2022 -0400 chore(flutter): deal with lints commit b4e045f9cbd54c4d42c3f30acfe72d5f8ccf9d82 Author: GroovinChip <[email protected]> Date: Tue Mar 29 12:56:00 2022 -0400 test(flutter): SendButton test commit 4e3d5db0bddb97e508fcdda2e7272fe31e70808f Author: GroovinChip <[email protected]> Date: Tue Mar 29 12:38:21 2022 -0400 test(flutter): KeyboardShortcutRunner tests commit 0dfd7a0bd6fd6bc76902e87f62ac29da0b01bac6 Author: GroovinChip <[email protected]> Date: Tue Mar 29 12:17:30 2022 -0400 test(flutter): UploadProgressIndicator commit ec50c8aea716bdc19e4308e367c8331211c61ea3 Author: GroovinChip <[email protected]> Date: Tue Mar 29 11:47:53 2022 -0400 test(flutter): SendingIndicator tests commit e87bd1450fb5bfb713a867c7e831968f76428848 Author: GroovinChip <[email protected]> Date: Tue Mar 29 11:32:35 2022 -0400 test(flutter): more gallery tests commit 93d46ca649277fe6d289a70f0bdd23584fd0ab94 Author: GroovinChip <[email protected]> Date: Tue Mar 29 11:17:29 2022 -0400 test(flutter): remove bad mock commit c7b03a39f0f3ca7a334dfb908f3473b5e4deab38 Author: GroovinChip <[email protected]> Date: Tue Mar 29 10:45:45 2022 -0400 chore(flutter): minor refactor commit 33aeb7b5ed398287cb930f3b4702f47a82fce2c6 Author: GroovinChip <[email protected]> Date: Tue Mar 29 09:49:07 2022 -0400 test(flutter): MessageDialog tests commit 220b3ae591e135d9606254666e04d4771945d0ae Author: GroovinChip <[email protected]> Date: Tue Mar 29 09:42:51 2022 -0400 test(flutter): DeleteMessageDialog tests commit db508478bb96749971f563de87b6c2a49e1e4e57 Author: GroovinChip <[email protected]> Date: Mon Mar 28 14:46:41 2022 -0400 test(flutter): ConfirmationDialog tests commit e8172dad6bfe18f40b65554ef2c6c16d7437cb56 Author: GroovinChip <[email protected]> Date: Mon Mar 28 14:35:50 2022 -0400 test(flutter): ChannelInfoDialog test commit 04c21458a601af3c3f4a5fc5ce9e8b61493c208c Author: GroovinChip <[email protected]> Date: Mon Mar 28 13:57:02 2022 -0400 test(flutter): DownloadMenuItem tests commit afee1dace543e2fef8543f9b035f684b2ee00f3b Author: GroovinChip <[email protected]> Date: Mon Mar 28 13:53:21 2022 -0400 test(flutter): StreamChatContextMenuItem tests commit 4616b1a973d0cf5224b5a71914c9908339c8462a Author: GroovinChip <[email protected]> Date: Mon Mar 28 13:02:07 2022 -0400 test(flutter): golden test for ErrorAlertSheet commit 22fb8241ae333f57ade8e8550f87695929836b2b Author: GroovinChip <[email protected]> Date: Mon Mar 28 12:59:01 2022 -0400 test(flutter): golden test for EditMessageSheet commit 0be79964827f344ba66e99cb9e5bd3d627aed01d Author: GroovinChip <[email protected]> Date: Mon Mar 28 12:56:08 2022 -0400 test(flutter): golden test for AttachmentModalSheet commit 0ae842a98143478885bc211c788353c8bca57b40 Author: GroovinChip <[email protected]> Date: Mon Mar 28 12:53:26 2022 -0400 test(flutter): GroupAvatar tests commit 697d294c6c7e1bba1d9089a53d5c60d8609ea64e Author: GroovinChip <[email protected]> Date: Mon Mar 28 12:30:25 2022 -0400 test(flutter): UserAvatar tests commit c03ba4124068c83d3a49c4db00ca18a659801c23 Author: GroovinChip <[email protected]> Date: Mon Mar 28 11:04:55 2022 -0400 test(flutter): ErrorAlertSheet test commit 3414f28530b0e22614b83af20361f21dd4967fae Author: GroovinChip <[email protected]> Date: Mon Mar 28 10:45:37 2022 -0400 test(flutter): EditMessageSheet test commit b2962b641dfbf14c11d3da7f80a4be27f9980d90 Author: GroovinChip <[email protected]> Date: Mon Mar 28 10:37:18 2022 -0400 test(flutter): AttachmentModalSheet test test(flutter): AttachmentModalSheet test chore(flutter): update exports commit 7d7e278692c65dea84e7ef3b2d3c1311ebb28408 Author: GroovinChip <[email protected]> Date: Mon Mar 28 09:52:29 2022 -0400 test(flutter): more organization commit 4479b649835acc62d8a0553f9505a0af8077abfc Author: GroovinChip <[email protected]> Date: Mon Mar 28 09:33:06 2022 -0400 test(flutter): tweak AttachmentHandler tests commit b4a41106567e3c08105ce8dc272b1ebf40b3b0bb Author: GroovinChip <[email protected]> Date: Mon Mar 28 09:24:00 2022 -0400 chore(flutter): streamline attachment handling * MobileAttachmentHandler for streamlining related code for mobile * Break out DesktopAttachmentHandler into its own file * Update minSdk version in Android runner commit bfb06d31b79cd941d8b611c7e89e2d7728a394cb Merge: a44b3b39 15c83d03 Author: GroovinChip <[email protected]> Date: Fri Mar 25 13:00:27 2022 -0400 Merge branch 'desk_web_dev' into tests commit a44b3b390adc25a00d1597bb1460973e8555fa9e Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:59:14 2022 -0400 chore(flutter): start cleaning up test dir commit de104a3491d942d12f72dd66152b62c6e8b10a5b Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:56:48 2022 -0400 test(flutter): VideoAttachment test commit 4748a1c081a3272f7fdbdeb69c501344e221e01e Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:52:30 2022 -0400 test(flutter): UrlAttachment test commit 54408ef70f07c60adf8350553fdf3173e5edd7c7 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:48:24 2022 -0400 test(flutter): ImageGroup test commit 811cff069e490c6bd03be0c0ac2f5ec86fb990e9 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:46:04 2022 -0400 test(flutter): ImageAttachment test commit a72677976c6524e76793432b0520e99cac4e7a62 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:34:06 2022 -0400 test(flutter): GiphyAttachment test commit 32ddac022e2c149726b4b6607f41850d556055b8 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:28:01 2022 -0400 test(flutter): FileAttachment test commit 6534958cab775be1fb9bda56e67caa4746aa1502 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:14:25 2022 -0400 test(flutter): AttachmentError test commit 718a83c10c4e883ce01c9c002ced24de671d30b9 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:06:43 2022 -0400 test(flutter): AttachmentUploadStateBuilder test commit a05108c460e36b2e425e4ec06f8bf408b0c20e97 Author: GroovinChip <[email protected]> Date: Fri Mar 25 11:44:34 2022 -0400 chore(flutter): minor cleanup commit 6e73038ac3714a2bd96f8c587af7f743bf2b2023 Author: GroovinChip <[email protected]> Date: Fri Mar 25 11:43:23 2022 -0400 test(flutter): AttachmentTitle test commit c6ee52ec951877feef31dc1427eec3bfc2bd8fcb Author: GroovinChip <[email protected]> Date: Fri Mar 25 11:43:11 2022 -0400 test(flutter): minor cleanup commit 09845cd845aa8c546043f4ae04805a8e6f53f61c Author: GroovinChip <[email protected]> Date: Fri Mar 25 11:42:52 2022 -0400 chore(flutter): improve device segmentation & vlc init check commit 172e6b25a064b5147b7641f29ebff3c920156f70 Author: GroovinChip <[email protected]> Date: Fri Mar 25 10:37:43 2022 -0400 test(flutter): attachment_handler tests commit 5f5a91c70cd7c069f39518f1f50bb74ab484c7e9 Author: GroovinChip <[email protected]> Date: Wed Mar 23 13:55:27 2022 -0400 test(flutter): test PlatformWidgetBuilder & DesktopWidgetBuilder commit 5a457b7557cb4127b74ee022389a24e5e9150601 Author: GroovinChip <[email protected]> Date: Wed Mar 23 13:55:05 2022 -0400 chore(flutter): make base platform widgets testable Also fix environment check in stream_chat.dart commit 88fb16018103c581236aeebd978e705b223b2243 Author: GroovinChip <[email protected]> Date: Wed Mar 23 11:59:19 2022 -0400 chore(flutter): add readme to conditional_parent_widget library commit e9307f8a6a5639d59358c245f033f9d9b209fe20 Author: GroovinChip <[email protected]> Date: Wed Mar 23 11:54:54 2022 -0400 test(flutter): ConditionalParentBuilder tests * chore(flutter): add thumblr dependency * chore(flutter): fix typos * chore(flutter): properly order super calls * feat(flutter) video thumbnails on desktop * chore(flutter): fix incorrect file name * chore(flutter): fix docs for generateVideoThumbnail * chore(flutter): remove incorrect comment * chore(flutter): run flutter format * chore(flutter): ignore lint * chore(flutter): flutter format * chore(flutter): fix tests * fix(flutter): fix MessageListView bug caused by rebase * chore(flutter): migrate desktop stuff to v4/stream_message_input.dart * chore(flutter): migrate over desktop changes for message replies * chore(flutter): fix message replies on desktop * chore(flutter): address lints * chore(flutter): update dart_vlc * chore(flutter): melos clean & update photo_view * fix(ui): port #1118 * fix(ui): use attachmentButtonBuilder * fix(ui): fix file attachment * format * fix(ui): fix message bottom row * chore(flutter sample): use modern window look * chore(flutter): pass userAvatarBuilder through to MessageWidgetContent Also pass onUserAvatarTap & userAvatarBuilder through to UserAvatarTransform * chore: fix MessageWidget menu actions * fix(flutter): fix "enter" sending empty messages in StreamMessageInput * fix(flutter): fix Giphys not loading in quoted messages * chore(flutter): update thumblr * feat(flutter): keyboard navigation for gallery items * fix(ui): fix double date divider * fix(ui): add scroll to bottom back * fix(ui): use mouse region * fix(ui): remove backbutton in desktop example * fix(ui): rename full screen media widget * feat(flutter): elevation prop for gallery header * chore(flutter): update dart_vlc * fix(flutter): fix "show in chat" showing when it shouldn't * fix(flutter): fix "show in chat" for mobile * docs(flutter): minor doc update * chore(flutter): add onMessageSwiped in example app * chore(flutter): pass usernameBuilder through to BottomRow * fix(flutter): hacky fix for chonky link previews * fix(flutter): fix chonky text messages * feat(flutter): press "esc" to close gallery Also refactors KeyboardShortcutRunner API * chore(flutter): fix innaccurate name * fix(ui): fix attachment + long text sizing * add key to channelpage * fix(ui): better example * fix format * fix example * chore(flutter): revert modern window look * improvement(flutter): improve `GalleryNavigationItem` * Make buttons more visible * Spacing from the sides of the screen * fix video thumbnail * fix split view * fix(ui): fix giphy in thread * feat(flutter): desktop message reactions commit 4b880ecb6d4dcbf315a7d41d863174817a9c530a Author: Reuben Turner <[email protected]> Date: Thu Jun 9 12:25:21 2022 -0400 fix(flutter): fixes for desktop reactions * Fix reactions for reverse messages * Ensure reactions near the edges of MessageListView show properly commit f511513f8dbb272397e81d08bad33ee60108d738 Author: Salvatore Giordano <[email protected]> Date: Thu Jun 9 16:43:03 2022 +0200 fix(ui): fix DesktopReactionBuilder and Portal commit c4885648330552f5212c5ba6b771c0f07023f6ae Merge: 2b710a2b 72b34c21 Author: Reuben Turner <[email protected]> Date: Wed Jun 8 14:12:44 2022 -0400 Merge branch 'desk_web_dev' into desktop_reactions commit 2b710a2bb610326123d5b7f75c26caa705114c9b Author: Reuben Turner <[email protected]> Date: Mon Jun 6 12:19:12 2022 -0400 chore(flutter): add debug props commit e24dabc1de12da8dce697b6d4e1a3617e9835549 Author: Reuben Turner <[email protected]> Date: Mon Jun 6 12:00:03 2022 -0400 feat(flutter): first pass at desktop reactions popup on mouse hover * fix(flutter): potential fix for #1199 * fix(flutter): fix non-reversed reactions placement * refactor(flutter): extract bottom reaction, stacked reaction with debug props * fix(flutter): fixes for desktop reactions * Fix bottom row being rendered above reaction buttons * Fix padding issues related to avatars * chore(flutter): add back lost changes from 2b1fb6a Also removed commented code * chore(flutter): stacked reactions update * Show "You" if userId matches the reaction username (per design) * Ensure long names don't throw off alignment * chore(flutter): format * fix(ui): fix mime type generation * chore(flutter): tweak desktop video * chore(flutter): add some missing things from develop * merge develop * fix merge * fix merge * format * fix tests * increate test action timeout * remove test * refactor(ui,core): remove deprecated code and cleanup * run builder * update coverage threasholds * refactor(ui): remove arrow function from countdown button * Update packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart * Update packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart * update comment * fix(ui): fix attachment + link solution * fix(ui): fix attachment + link solution * chore(flutter) update changelog and version * chore(localizations): update changelog and versions * chore(llc,core,ui,persistence,localizations): update changelog and pubspec * chore(llc,core,ui,persistence,localizations): update changelog and pubspec * fix example * fix deps * fix(ui): fix enter sending the message while in slowmode * fix(ui): fix reaction list * fix web * fix(ui): fix image download when using assetUrl * fix(ui): fix typo in changelog * fix(ui): fix gallery navigation * fix(ui): fix giphy sending size * fix(ui): add keyboard shorcuts to gallery view * fix(ui): fix gallery footer * fix(core): allow scroll notification bubbling in lazy load scroll view * fix(ui): fix lazy load scroll view * fix(ui): fix loading to unread position * fix(ui): fix last message preview * chore(ui): update changelog * fix tests * fix(llc): fix connecting while connecting and disconneting (#1237) * fix(llc): fix connecting while connecting and disconneting * chore(llc): update changelog * chore(llc): add test * chore(llc): improve test * fix(llc): _resetRequestFlags regardless of the connection status * feat(llc): add support for AttachmentFileUploaderProvider. (#1246) * feat(llc): add support for `AttachmentFileUploaderProvider`. Signed-off-by: xsahil03x <[email protected]> * chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> * refactor(llc): Remove deprecated attachmentFileUploader field. Signed-off-by: xsahil03x <[email protected]> * port no localization * fix(ui): fix jiffy locale initialization (#1255) * fix(ui): fix jiffy locale initialization * chore(ui): update changelog * fix(ui): cleanup * chore(llc,core,ui,persistence,localizations): align changelog * chore(llc,core,ui,persistence,localizations): add links to 4.x changelogs * fix(ui): fix date divider not showing (#1259) * fix(ui): fix date divider not showing * chore(ui): update changelog * chore(ui): minor changes Signed-off-by: xsahil03x <[email protected]> * fix(ui): fix separator Signed-off-by: xsahil03x <[email protected]> Co-authored-by: Sahil Kumar <[email protected]> * chore(ui): add contextmenu requirements for web (#1261) * fix unread separator * feat(ui): `StreamChatConfiguration` class (#1128) * feat: StreamChatConfiguration class See #1125 for more details * chore(flutter): update changelog * fix analysis * cleanup * feat(ui): add enforceUniqueReactions to `StreamChatConfiguration` widget * add tests * refactor(ui): rename StreamChatConfigurationProvider and StreamChatConfiguration * refactor(ui): rename StreamChat.config to streamChatConfigData * refactor(ui): make enforceUniqueReactions non nullable * chore(ui): fix analysis errors * refactor(ui): make config fields non nullable * chore(ui): format Co-authored-by: Salvatore Giordano <[email protected]> * feat(ui, core): add StreamAutocomplete (#1263) * feat(ui, core): add stream_autocomplete.dart Signed-off-by: xsahil03x <[email protected]> * refactor(core, ui): refactor message input controller Signed-off-by: xsahil03x <[email protected]> * fix(ui): minor fixes Signed-off-by: xsahil03x <[email protected]> * feat(ui): add StreamAutocompleteOptions widget Signed-off-by: xsahil03x <[email protected]> * Update packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart * fix(ui): cleanup * chore(core, ui): cleanup Signed-off-by: xsahil03x <[email protected]> * chore(ui): ignore no empty block * chore(ui): more cleanup Signed-off-by: xsahil03x <[email protected]> * fix(ui): fix command button for bigger screens, other fixes. Signed-off-by: xsahil03x <[email protected]> Co-authored-by: Salvatore Giordano <[email protected]> * fix(ui): increase default file limit to 100MB (#1279) * fix(ui): increase default file limit to 100MB * Update packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart * fix(ui): export kDefaultMaxAttachmentSize and use it in `StreamAttachmentPicker` default * feat(ui): show dialog after clicking on the camera button and permission is denied (#1262) * feat(ui): show dialog after clicking on the camera button and permission is denied * chore(ui): update changelog * Remove permission_handler plugin and refactor * cleanup * cleanup * add tests * fix(ui): reset the permission state on resume * cleanup * fix concurrent calls * fix(ui): cleanup * fix(ui): add try catch * fix(llc, ui): copy file to tempdir before uploading a file and do not serialize bytes (#1285) * fix(llc, ui): copy file to tempdir before uploading a file and do not serialize bytes * fix(llc): do not save failed messages with attachments on web * fix(llc): replace every with any * chore(llc): add test * fix analysis * fix analysis * chore(llc,core,ui,persistence): align changelog with v4 * fix(ui): do not linkify system message mentions * chore(llc,core,ui,persistence,localizations): update changelog and pubspec (#1289) * chore(ui): update deps * chore(llc,core,ui,persistence,localizations): update changelog and pubspec * chore(ui): publish beta.2 * feat(core,ui): add StreamMemberListView, StreamMemberGridView, and StreamMemberListController (#1291) * feat(core,ui): add StreamMemberListView, StreamMemberGridView, and StreamMemberListController * chore(core,ui): update changelog * fix(ui): fix exports * chore(ui): add tests * chore(ui): add StreamMemberGridTile typedef * chore(ui): minor changes Signed-off-by: xsahil03x <[email protected]> Co-authored-by: Sahil Kumar <[email protected]> * fix(ui): do not send empty message * refactor(ui,localization): removed emoji picker and connected (#1292) * refactor(ui,localization): removed emoji picker and connected * chore(ui,localizations): update changelog * chore(repo): update ui coverage threashold * doc(ui): add autocomplete_triggers guide * chore(ui): add tests for isOnlyEmoji * chore(ui): add tests for isOnlyEmoji * feat(ui): add `StreamAutocompleteTrigger.triggerOnlyAfterSpace` * fix(llc): Fix `Channel.removeMessage` not able to remove thread message. (#1294) Signed-off-by: xsahil03x <[email protected]> * chore(ui): flutter format Signed-off-by: xsahil03x <[email protected]> * fix(ui): fix `stopTyping` unhandled exceptions when network is off or spotty. (#1296) * feat(llc, ui): Introduce `keyStrokeHandler` to properly handle keyStrokes. Signed-off-by: xsahil03x <[email protected]> * chore(ui): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> * test(llc): add key_stroke_handler_test.dart Signed-off-by: xsahil03x <[email protected]> * chore(ui): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> * fix(core): only handle the channel list event if the state is success. Signed-off-by: xsahil03x <[email protected]> * feat(llc): add `hide_history` flag in `channel.addMembers`. Signed-off-by: xsahil03x <[email protected]> * refactor(llc): Convert positional argument to optional arguments Signed-off-by: xsahil03x <[email protected]> * test(llc): update tests Signed-off-by: xsahil03x <[email protected]> * perf: improve image resizing * fix(core): use copyWith while setting controller items. Signed-off-by: xsahil03x <[email protected]> * feat(ui): Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params to `StreamMessageWidget` and `StreamAttachmentPicker` to customize the appearance of image thumbnails. Signed-off-by: xsahil03x <[email protected]> * fix analysis * Release v4.5.0 (#1308) * chore(llc, ui, core): bump version to v4.5.0 Signed-off-by: xsahil03x <[email protected]> * chore(ui, core): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> Signed-off-by: xsahil03x <[email protected]> * third party video integration * fix llc * refactor: clean up and add docs * chore(llc): update changelog * chore(llc): add tests * chore(llc): add more tests * fix analysis * chore(llc): add more tests * chore(llc): add more tests * fix(llc): only start listening to event once connected. Signed-off-by: xsahil03x <[email protected]> * chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> * chore(llc): move files to correct folders. Signed-off-by: xsahil03x <[email protected]> * fix(ui): Fix message text hiding because of a flutter bug (#1327) * fix(ui): remove clip * chore(ui): update changelog * cleanup * fix(ui): hitting "enter" on the android keyboard sends the message instead of going to a new line. Signed-off-by: xsahil03x <[email protected]> * feat(ui): add support for additional text field params in message input. Signed-off-by: xsahil03x <[email protected]> * fix analysis * remove print * chore(llc,core,ui): update pubspec and changelogs (#1333) * fix(ui): show placeholder for web video thumbs * fix file download on web * fix reply and show buttons * remove tests * treat svg as files * fix(ui): change default image resize to clip * solve some todos * polish v5 fixing analysis issues * feat(ui): stream attachment picker bottom sheet (#1336) * refactor(ui): improve stream_attachment_handler.dart, add stream_attachment_picker_bottom_sheet.dart Signed-off-by: xsahil03x <[email protected]> * feat(ui): add copyWith in command_button.dart Signed-off-by: xsahil03x <[email protected]> * chore(ui): remove permission_manager.dart Signed-off-by: xsahil03x <[email protected]> * fix(ui): some fixes Signed-off-by: xsahil03x <[email protected]> * fix(ui): more fixes Signed-off-by: xsahil03x <[email protected]> * chore(ui): fix lints, add doc comments Signed-off-by: xsahil03x <[email protected]> * chore(ci): update flutter version Signed-off-by: xsahil03x <[email protected]> * chore(ui): more fixes Signed-off-by: xsahil03x <[email protected]> * chore(ui): more fixes Signed-off-by: xsahil03x <[email protected]> * chore(ui): more fixes Signed-off-by: xsahil03x <[email protected]> * chore(ci): update melos and flutter version Signed-off-by: xsahil03x <[email protected]> Signed-off-by: xsahil03x <[email protected]> Co-authored-by: Salvatore Giordano <[email protected]> * remove todo * update changelog * update changelogs * revert * update coverage threasholds Signed-off-by: xsahil03x <[email protected]> Co-authored-by: GroovinChip <[email protected]> Co-authored-by: Sahil Kumar <[email protected]> Co-authored-by: Gordon Hayes <[email protected]> Co-authored-by: Sacha Arbonel <[email protected]>
This commit is contained in:
co-authored by
Sahil Kumar
GroovinChip
Gordon Hayes
Sacha Arbonel
parent
3e1d6e91c7
commit
4f2542291c
@@ -0,0 +1,642 @@
|
||||
// ignore_for_file: no-empty-block
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
export 'stream_command_autocomplete_options.dart';
|
||||
export 'stream_mention_autocomplete_options.dart';
|
||||
|
||||
/// {@macro stream_chat_flutter.StreamMessageInputController}
|
||||
typedef StreamMessageEditingController = StreamMessageInputController;
|
||||
|
||||
/// Positions the [AutocompleteTrigger] options around the [TextField] or
|
||||
/// [TextFormField] that triggered the autocomplete.
|
||||
enum OptionsAlignment {
|
||||
/// The options are displayed below the field.
|
||||
below,
|
||||
|
||||
/// The options are displayed above the field.
|
||||
///
|
||||
/// This is the default.
|
||||
above;
|
||||
|
||||
Anchor _toAnchor() {
|
||||
switch (this) {
|
||||
case OptionsAlignment.below:
|
||||
return const Aligned(
|
||||
widthFactor: 1,
|
||||
follower: Alignment.topCenter,
|
||||
target: Alignment.bottomCenter,
|
||||
);
|
||||
case OptionsAlignment.above:
|
||||
return const Aligned(
|
||||
widthFactor: 1,
|
||||
follower: Alignment.bottomCenter,
|
||||
target: Alignment.topCenter,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of the Autocomplete callback which returns the widget that
|
||||
/// contains the input [TextField] or [TextFormField].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [StreamAutocomplete.fieldViewBuilder], which is of this type.
|
||||
typedef StreamAutocompleteFieldViewBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
StreamMessageEditingController messageEditingController,
|
||||
FocusNode focusNode,
|
||||
);
|
||||
|
||||
/// The type of the [StreamAutocompleteTrigger] callback which returns a
|
||||
/// [Widget] that displays the specified [options].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [StreamAutocompleteTrigger.optionsViewBuilder], which is of this type.
|
||||
typedef StreamAutocompleteOptionsViewBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
StreamAutocompleteQuery autocompleteQuery,
|
||||
StreamMessageEditingController messageEditingController,
|
||||
);
|
||||
|
||||
/// The query to determine the autocomplete options.
|
||||
class StreamAutocompleteQuery {
|
||||
/// Creates a [StreamAutocompleteQuery] with the specified [query] and
|
||||
/// [selection].
|
||||
const StreamAutocompleteQuery({
|
||||
required this.query,
|
||||
required this.selection,
|
||||
});
|
||||
|
||||
/// The query string.
|
||||
final String query;
|
||||
|
||||
/// The selection in the text field.
|
||||
final TextSelection selection;
|
||||
}
|
||||
|
||||
class _StreamAutocompleteInvokedTriggerWithQuery {
|
||||
const _StreamAutocompleteInvokedTriggerWithQuery(this.trigger, this.query);
|
||||
|
||||
final StreamAutocompleteTrigger trigger;
|
||||
final StreamAutocompleteQuery query;
|
||||
}
|
||||
|
||||
/// The class responsible for triggering autocomplete suggestions and
|
||||
/// displaying the options.
|
||||
class StreamAutocompleteTrigger {
|
||||
/// Creates a [StreamAutocompleteTrigger] which can be used to trigger
|
||||
/// autocomplete suggestions.
|
||||
const StreamAutocompleteTrigger({
|
||||
required this.trigger,
|
||||
required this.optionsViewBuilder,
|
||||
this.triggerOnlyAfterSpace = false,
|
||||
this.triggerOnlyAtStart = false,
|
||||
this.minimumRequiredCharacters = 0,
|
||||
});
|
||||
|
||||
/// The trigger character.
|
||||
///
|
||||
/// eg. '@', '#', ':'
|
||||
final String trigger;
|
||||
|
||||
/// Whether the [trigger] should only be recognised at the start of the input.
|
||||
final bool triggerOnlyAtStart;
|
||||
|
||||
/// Whether the [trigger] should only be recognised after a space.
|
||||
final bool triggerOnlyAfterSpace;
|
||||
|
||||
/// The minimum required characters for the [trigger] to start recognising
|
||||
/// a autocomplete options.
|
||||
final int minimumRequiredCharacters;
|
||||
|
||||
/// Builds the widget responsible for querying and displaying the
|
||||
/// autocomplete options.
|
||||
///
|
||||
/// See also:
|
||||
/// * [StreamAutocompleteOptions], which helps in displaying the options.
|
||||
final StreamAutocompleteOptionsViewBuilder optionsViewBuilder;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is StreamAutocompleteTrigger &&
|
||||
runtimeType == other.runtimeType &&
|
||||
trigger == other.trigger &&
|
||||
triggerOnlyAtStart == other.triggerOnlyAtStart &&
|
||||
triggerOnlyAfterSpace == other.triggerOnlyAfterSpace &&
|
||||
minimumRequiredCharacters == other.minimumRequiredCharacters;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
trigger.hashCode ^
|
||||
triggerOnlyAtStart.hashCode ^
|
||||
triggerOnlyAfterSpace.hashCode ^
|
||||
minimumRequiredCharacters.hashCode;
|
||||
|
||||
/// Checks if the user is invoking the recognising [trigger] and returns
|
||||
/// the autocomplete query if so.
|
||||
StreamAutocompleteQuery? invokingTrigger(
|
||||
Message message,
|
||||
TextEditingValue textEditingValue,
|
||||
) {
|
||||
final text = textEditingValue.text;
|
||||
final cursorPosition = textEditingValue.selection.baseOffset;
|
||||
|
||||
// Find the first [trigger] location before the input cursor.
|
||||
final firstTriggerIndexBeforeCursor =
|
||||
text.substring(0, cursorPosition).lastIndexOf(trigger);
|
||||
|
||||
// If the [trigger] is not found before the cursor, then it's not a trigger.
|
||||
if (firstTriggerIndexBeforeCursor == -1) return null;
|
||||
|
||||
// If the [trigger] is found before the cursor, but the [trigger] is only
|
||||
// recognised at the start of the input, then it's not a trigger.
|
||||
if (triggerOnlyAtStart && firstTriggerIndexBeforeCursor != 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only show typing suggestions after a space, or at the start of the input
|
||||
// valid examples: "@user", "Hello @user"
|
||||
// invalid examples: "Hello@user"
|
||||
final textBeforeTrigger = text.substring(0, firstTriggerIndexBeforeCursor);
|
||||
if (triggerOnlyAfterSpace &&
|
||||
textBeforeTrigger.isNotEmpty &&
|
||||
!textBeforeTrigger.endsWith(' ')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The suggestion range. Protect against invalid ranges.
|
||||
final suggestionStart = firstTriggerIndexBeforeCursor + trigger.length;
|
||||
final suggestionEnd = cursorPosition;
|
||||
if (suggestionStart > suggestionEnd) return null;
|
||||
|
||||
// Fetch the suggestion text. The suggestions can't have spaces.
|
||||
// valid example: "@luke_skywa..."
|
||||
// invalid example: "@luke skywa..."
|
||||
final suggestionText = text.substring(suggestionStart, suggestionEnd);
|
||||
if (suggestionText.contains(' ')) return null;
|
||||
|
||||
// A minimum number of characters can be provided to only show
|
||||
// suggestions after the customer has input enough characters.
|
||||
if (suggestionText.length < minimumRequiredCharacters) return null;
|
||||
|
||||
return StreamAutocompleteQuery(
|
||||
query: suggestionText,
|
||||
selection: TextSelection(
|
||||
baseOffset: suggestionStart,
|
||||
extentOffset: suggestionEnd,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that provides a text field with autocomplete functionality.
|
||||
class StreamAutocomplete extends StatefulWidget {
|
||||
/// Create an instance of StreamAutocomplete.
|
||||
///
|
||||
/// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must
|
||||
/// not be null.
|
||||
const StreamAutocomplete({
|
||||
super.key,
|
||||
this.focusNode,
|
||||
this.messageEditingController,
|
||||
required this.autocompleteTriggers,
|
||||
this.fieldViewBuilder = _defaultFieldViewBuilder,
|
||||
this.optionsAlignment = OptionsAlignment.above,
|
||||
this.debounceDuration = const Duration(milliseconds: 300),
|
||||
}) : assert((focusNode == null) == (messageEditingController == null), '');
|
||||
|
||||
/// The triggers that trigger autocomplete.
|
||||
final Iterable<StreamAutocompleteTrigger> autocompleteTriggers;
|
||||
|
||||
/// Builds the field whose input is used to get the options.
|
||||
///
|
||||
/// Pass the provided [StreamMessageEditingController] to the field built
|
||||
/// here so that StreamAutocomplete can listen for changes.
|
||||
final StreamAutocompleteFieldViewBuilder fieldViewBuilder;
|
||||
|
||||
/// The [FocusNode] that is used for the text field.
|
||||
///
|
||||
/// The main purpose of this parameter is to allow the use of a separate text
|
||||
/// field located in another part of the widget tree instead of the text
|
||||
/// field built by [fieldViewBuilder]. For example, it may be desirable to
|
||||
/// place the text field in the AppBar and the options below in the main body.
|
||||
///
|
||||
/// When following this pattern, [fieldViewBuilder] can return
|
||||
/// `SizedBox.shrink()` so that nothing is drawn where the text field would
|
||||
/// normally be. A separate text field can be created elsewhere, and a
|
||||
/// FocusNode and StreamMessageEditingController can be passed both to that
|
||||
/// text field and to StreamAutocomplete.
|
||||
///
|
||||
/// If this parameter is not null, then [messageEditingController] must also
|
||||
/// be not null.
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// The [StreamMessageEditingController] that is used for the text field.
|
||||
///
|
||||
/// If this parameter is not null, then [focusNode] must also be not null.
|
||||
final StreamMessageEditingController? messageEditingController;
|
||||
|
||||
/// The alignment of the options.
|
||||
///
|
||||
/// The default value is [OptionsAlignment.above].
|
||||
final OptionsAlignment optionsAlignment;
|
||||
|
||||
/// The duration of the debounce period for the
|
||||
/// [StreamMessageEditingController].
|
||||
///
|
||||
/// The default value is [300ms].
|
||||
final Duration debounceDuration;
|
||||
|
||||
static Widget _defaultFieldViewBuilder(
|
||||
BuildContext context,
|
||||
StreamMessageEditingController messageEditingController,
|
||||
FocusNode focusNode,
|
||||
) {
|
||||
return _StreamAutocompleteField(
|
||||
focusNode: focusNode,
|
||||
messageEditingController: messageEditingController,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the nearest [StreamAutocomplete] ancestor of the given context.
|
||||
static _StreamAutocompleteState of(BuildContext context) {
|
||||
final state = context.findAncestorStateOfType<_StreamAutocompleteState>();
|
||||
assert(state != null, 'StreamAutocomplete not found in the widget tree');
|
||||
return state!;
|
||||
}
|
||||
|
||||
@override
|
||||
_StreamAutocompleteState createState() => _StreamAutocompleteState();
|
||||
}
|
||||
|
||||
class _StreamAutocompleteState extends State<StreamAutocomplete> {
|
||||
late StreamMessageEditingController _messageEditingController;
|
||||
late FocusNode _focusNode;
|
||||
|
||||
StreamAutocompleteQuery? _currentQuery;
|
||||
StreamAutocompleteTrigger? _currentTrigger;
|
||||
|
||||
bool _hideOptions = false;
|
||||
String _lastFieldText = '';
|
||||
|
||||
// True if the state indicates that the options should be visible.
|
||||
bool get _shouldShowOptions {
|
||||
return !_hideOptions &&
|
||||
_focusNode.hasFocus &&
|
||||
_currentQuery != null &&
|
||||
_currentTrigger != null;
|
||||
}
|
||||
|
||||
/// Accepts and replaces the current query with the given [option] and closes
|
||||
/// the suggested options.
|
||||
///
|
||||
/// Optionally, pass [keepTrigger] false to remove the trigger from the text.
|
||||
void acceptAutocompleteOption(
|
||||
String option, {
|
||||
bool keepTrigger = true,
|
||||
}) {
|
||||
if (option.isEmpty) return;
|
||||
|
||||
final query = _currentQuery;
|
||||
final trigger = _currentTrigger;
|
||||
if (query == null || trigger == null) return;
|
||||
|
||||
final querySelection = query.selection;
|
||||
final text = _messageEditingController.text;
|
||||
|
||||
var start = querySelection.baseOffset;
|
||||
if (!keepTrigger) start -= 1;
|
||||
|
||||
final end = querySelection.extentOffset;
|
||||
|
||||
final alreadyContainsSpace = text.substring(end).startsWith(' ');
|
||||
// Having extra space helps dismissing the auto-completion view.
|
||||
// ignore: parameter_assignments
|
||||
if (!alreadyContainsSpace) option += ' ';
|
||||
|
||||
var selectionOffset = start + option.length;
|
||||
// In case the extra space is already there, we need to move the cursor
|
||||
// after the space.
|
||||
if (alreadyContainsSpace) selectionOffset += 1;
|
||||
|
||||
final newText = text.replaceRange(start, end, option);
|
||||
final newSelection = TextSelection.collapsed(offset: selectionOffset);
|
||||
|
||||
_messageEditingController.textEditingValue = TextEditingValue(
|
||||
text: newText,
|
||||
selection: newSelection,
|
||||
);
|
||||
|
||||
return closeSuggestions();
|
||||
}
|
||||
|
||||
/// Closes the suggestions and resets the current query.
|
||||
void closeSuggestions() {
|
||||
final prev = _currentQuery;
|
||||
if (prev == null) return;
|
||||
|
||||
_currentQuery = null;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
/// Starts showing the suggestions for the given [query].
|
||||
void showSuggestions(
|
||||
StreamAutocompleteQuery query,
|
||||
StreamAutocompleteTrigger trigger,
|
||||
) {
|
||||
final prevQuery = _currentQuery;
|
||||
final prevTrigger = _currentTrigger;
|
||||
if (prevQuery == query && prevTrigger == trigger) return;
|
||||
|
||||
_currentQuery = query;
|
||||
_currentTrigger = trigger;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
// Checks if there is any invoked autocomplete trigger and returns the first
|
||||
// one along with the query that matches the current input.
|
||||
_StreamAutocompleteInvokedTriggerWithQuery? _getInvokedTriggerWithQuery(
|
||||
Message messageValue,
|
||||
TextEditingValue textEditingValue,
|
||||
) {
|
||||
final autocompleteTriggers = widget.autocompleteTriggers.toSet();
|
||||
for (final trigger in autocompleteTriggers) {
|
||||
final query = trigger.invokingTrigger(messageValue, textEditingValue);
|
||||
if (query != null) {
|
||||
return _StreamAutocompleteInvokedTriggerWithQuery(trigger, query);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Called when _textEditingController changes.
|
||||
late final _onChangedField = debounce(
|
||||
() {
|
||||
final messageValue = _messageEditingController.message;
|
||||
final textEditingValue = _messageEditingController.textEditingValue;
|
||||
|
||||
// If the content has not changed, then there is nothing to do.
|
||||
if (textEditingValue.text == _lastFieldText) return;
|
||||
|
||||
// Make sure the options are no longer hidden if the content of the
|
||||
// field changes.
|
||||
_hideOptions = false;
|
||||
_lastFieldText = textEditingValue.text;
|
||||
|
||||
// If the text field is empty, then there is no need to do anything.
|
||||
if (textEditingValue.text.isEmpty) return closeSuggestions();
|
||||
|
||||
// If the text field is not empty, then we need to check if the
|
||||
// text field contains a trigger.
|
||||
final _triggerWithQuery = _getInvokedTriggerWithQuery(
|
||||
messageValue,
|
||||
textEditingValue,
|
||||
);
|
||||
|
||||
// If the text field does not contain a trigger, then there is no need
|
||||
// to do anything.
|
||||
if (_triggerWithQuery == null) return closeSuggestions();
|
||||
|
||||
// If the text field contains a trigger, then we need to open the
|
||||
// portal.
|
||||
final trigger = _triggerWithQuery.trigger;
|
||||
final query = _triggerWithQuery.query;
|
||||
return showSuggestions(query, trigger);
|
||||
},
|
||||
widget.debounceDuration,
|
||||
);
|
||||
|
||||
// Called when the field's FocusNode changes.
|
||||
void _onChangedFocus() {
|
||||
// Options should no longer be hidden when the field is re-focused.
|
||||
_hideOptions = !_focusNode.hasFocus;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
// Handle a potential change in textEditingController by properly disposing of
|
||||
// the old one and setting up the new one, if needed.
|
||||
void _updateTextEditingController(
|
||||
StreamMessageEditingController? old,
|
||||
StreamMessageEditingController? current,
|
||||
) {
|
||||
if ((old == null && current == null) || old == current) {
|
||||
return;
|
||||
}
|
||||
if (old == null) {
|
||||
_messageEditingController
|
||||
..removeListener(_onChangedField)
|
||||
..dispose();
|
||||
_messageEditingController = current!;
|
||||
} else if (current == null) {
|
||||
_messageEditingController.removeListener(_onChangedField);
|
||||
_messageEditingController = StreamMessageEditingController();
|
||||
} else {
|
||||
_messageEditingController.removeListener(_onChangedField);
|
||||
_messageEditingController = current;
|
||||
}
|
||||
_messageEditingController.addListener(_onChangedField);
|
||||
}
|
||||
|
||||
// Handle a potential change in focusNode by properly disposing of the old one
|
||||
// and setting up the new one, if needed.
|
||||
void _updateFocusNode(FocusNode? old, FocusNode? current) {
|
||||
if ((old == null && current == null) || old == current) {
|
||||
return;
|
||||
}
|
||||
if (old == null) {
|
||||
_focusNode
|
||||
..removeListener(_onChangedFocus)
|
||||
..dispose();
|
||||
_focusNode = current!;
|
||||
} else if (current == null) {
|
||||
_focusNode.removeListener(_onChangedFocus);
|
||||
_focusNode = FocusNode();
|
||||
} else {
|
||||
_focusNode.removeListener(_onChangedFocus);
|
||||
_focusNode = current;
|
||||
}
|
||||
_focusNode.addListener(_onChangedFocus);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_messageEditingController =
|
||||
widget.messageEditingController ?? StreamMessageEditingController();
|
||||
_messageEditingController.addListener(_onChangedField);
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
_focusNode.addListener(_onChangedFocus);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(StreamAutocomplete oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_updateTextEditingController(
|
||||
oldWidget.messageEditingController,
|
||||
widget.messageEditingController,
|
||||
);
|
||||
_updateFocusNode(oldWidget.focusNode, widget.focusNode);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_messageEditingController.removeListener(_onChangedField);
|
||||
if (widget.messageEditingController == null) {
|
||||
_messageEditingController.dispose();
|
||||
}
|
||||
_focusNode.removeListener(_onChangedFocus);
|
||||
if (widget.focusNode == null) {
|
||||
_focusNode.dispose();
|
||||
}
|
||||
_onChangedField.cancel();
|
||||
closeSuggestions();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Adding additional builder so that [.of] works.
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
final anchor = widget.optionsAlignment._toAnchor();
|
||||
final shouldShowOptions = _shouldShowOptions;
|
||||
final optionViewBuilder = shouldShowOptions
|
||||
? _currentTrigger!.optionsViewBuilder(
|
||||
context,
|
||||
_currentQuery!,
|
||||
_messageEditingController,
|
||||
)
|
||||
: null;
|
||||
|
||||
return PortalTarget(
|
||||
anchor: anchor,
|
||||
visible: shouldShowOptions,
|
||||
portalFollower: optionViewBuilder,
|
||||
child: widget.fieldViewBuilder(
|
||||
context,
|
||||
_messageEditingController,
|
||||
_focusNode,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The default Material-style Autocomplete text field.
|
||||
class _StreamAutocompleteField extends StatelessWidget {
|
||||
const _StreamAutocompleteField({
|
||||
required this.focusNode,
|
||||
required this.messageEditingController,
|
||||
});
|
||||
|
||||
final FocusNode focusNode;
|
||||
|
||||
final StreamMessageEditingController messageEditingController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamMessageTextField(
|
||||
controller: messageEditingController,
|
||||
focusNode: focusNode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const _kDefaultStreamAutocompleteOptionsShape = RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
);
|
||||
|
||||
/// A helper widget used to show the options of a [StreamAutocomplete].
|
||||
class StreamAutocompleteOptions<T extends Object> extends StatelessWidget {
|
||||
/// Creates a [StreamAutocompleteOptions] widget.
|
||||
const StreamAutocompleteOptions({
|
||||
super.key,
|
||||
this.color,
|
||||
this.elevation = 2,
|
||||
this.margin = const EdgeInsets.all(8),
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
required this.options,
|
||||
this.maxHeight,
|
||||
required this.optionBuilder,
|
||||
this.headerBuilder,
|
||||
this.shape = _kDefaultStreamAutocompleteOptionsShape,
|
||||
});
|
||||
|
||||
/// The background color of the options card.
|
||||
///
|
||||
/// Defaults to [StreamColorTheme.barsBg].
|
||||
final Color? color;
|
||||
|
||||
/// The elevation of the options card.
|
||||
///
|
||||
/// The default value is 2.
|
||||
final double elevation;
|
||||
|
||||
/// The margin of the options card.
|
||||
///
|
||||
/// The default value is [EdgeInsets.all(8)].
|
||||
final EdgeInsetsGeometry margin;
|
||||
|
||||
/// The clip behavior of the options card.
|
||||
///
|
||||
/// The default value is [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// The shape of the options card.
|
||||
final ShapeBorder shape;
|
||||
|
||||
/// The options to display.
|
||||
final Iterable<T> options;
|
||||
|
||||
/// The maximum height of the options card.
|
||||
///
|
||||
/// Defaults to half the height of the screen.
|
||||
final double? maxHeight;
|
||||
|
||||
/// The builder for the options.
|
||||
final Widget Function(BuildContext context, T option) optionBuilder;
|
||||
|
||||
/// The builder for the header of the options.
|
||||
final WidgetBuilder? headerBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final height = MediaQuery.of(context).size.height;
|
||||
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
||||
return Card(
|
||||
margin: margin,
|
||||
elevation: elevation,
|
||||
color: color ?? colorTheme.barsBg,
|
||||
shape: shape,
|
||||
clipBehavior: clipBehavior,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (headerBuilder != null) ...[
|
||||
headerBuilder!(context),
|
||||
const Divider(height: 0),
|
||||
],
|
||||
LimitedBox(
|
||||
maxHeight: maxHeight ?? height * 0.5,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (context, index) {
|
||||
final option = options.elementAt(index);
|
||||
return optionBuilder(context, option);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/autocomplete/stream_autocomplete.dart';
|
||||
import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// {@template commands_overlay}
|
||||
/// Overlay for displaying commands that can be used
|
||||
/// to interact with the channel.
|
||||
/// {@endtemplate}
|
||||
class StreamCommandAutocompleteOptions extends StatelessWidget {
|
||||
/// Constructor for creating a [StreamCommandAutocompleteOptions]
|
||||
const StreamCommandAutocompleteOptions({
|
||||
required this.query,
|
||||
required this.channel,
|
||||
this.onCommandSelected,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Query for searching commands.
|
||||
final String query;
|
||||
|
||||
/// The channel to search for users.
|
||||
final Channel channel;
|
||||
|
||||
/// Callback called when a command is selected.
|
||||
final ValueSetter<Command>? onCommandSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final commands = channel.config?.commands.where((it) {
|
||||
final normalizedQuery = query.toUpperCase();
|
||||
final normalizedName = it.name.toUpperCase();
|
||||
return normalizedName.contains(normalizedQuery);
|
||||
});
|
||||
|
||||
if (commands == null || commands.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
final streamChatTheme = StreamChatTheme.of(context);
|
||||
final colorTheme = streamChatTheme.colorTheme;
|
||||
final textTheme = streamChatTheme.textTheme;
|
||||
|
||||
return StreamAutocompleteOptions<Command>(
|
||||
options: commands,
|
||||
headerBuilder: (context) {
|
||||
return ListTile(
|
||||
dense: true,
|
||||
horizontalTitleGap: 0,
|
||||
leading: StreamSvgIcon.lightning(
|
||||
color: colorTheme.accentPrimary,
|
||||
size: 28,
|
||||
),
|
||||
title: Text(
|
||||
context.translations.instantCommandsLabel,
|
||||
style: TextStyle(
|
||||
color: colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
optionBuilder: (context, command) {
|
||||
return ListTile(
|
||||
dense: true,
|
||||
horizontalTitleGap: 0,
|
||||
leading: _CommandIcon(command: command),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
command.name.capitalize(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'/${command.name} ${command.args}',
|
||||
style: textTheme.body.copyWith(
|
||||
color: colorTheme.textLowEmphasis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: onCommandSelected == null
|
||||
? null
|
||||
: () => onCommandSelected!(command),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CommandIcon extends StatelessWidget {
|
||||
const _CommandIcon({required this.command});
|
||||
|
||||
final Command command;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _streamChatTheme = StreamChatTheme.of(context);
|
||||
switch (command.name) {
|
||||
case 'giphy':
|
||||
return CircleAvatar(
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.giphyIcon(
|
||||
size: 24,
|
||||
),
|
||||
);
|
||||
case 'ban':
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.iconUserDelete(
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
case 'flag':
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.flag(
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
case 'imgur':
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: ClipOval(
|
||||
child: StreamSvgIcon.imgur(
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
);
|
||||
case 'mute':
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.mute(
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
case 'unban':
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.userAdd(
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
case 'unmute':
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.volumeUp(
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
default:
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.lightning(
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/autocomplete/stream_autocomplete.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/user/user_mention_tile.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/typedefs.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// {@template user_mentions_overlay}
|
||||
/// Overlay for displaying users that can be mentioned.
|
||||
/// {@endtemplate}
|
||||
class StreamMentionAutocompleteOptions extends StatefulWidget {
|
||||
/// Constructor for creating a [StreamMentionAutocompleteOptions].
|
||||
StreamMentionAutocompleteOptions({
|
||||
super.key,
|
||||
required this.query,
|
||||
required this.channel,
|
||||
this.client,
|
||||
this.limit = 10,
|
||||
this.mentionAllAppUsers = false,
|
||||
this.mentionsTileBuilder,
|
||||
this.onMentionUserTap,
|
||||
}) : assert(
|
||||
channel.state != null,
|
||||
'Channel ${channel.cid} is not yet initialized',
|
||||
),
|
||||
assert(
|
||||
!mentionAllAppUsers || (mentionAllAppUsers && client != null),
|
||||
'StreamChatClient is required in order to use mentionAllAppUsers',
|
||||
);
|
||||
|
||||
/// Query for searching users.
|
||||
final String query;
|
||||
|
||||
/// Limit applied on user search results.
|
||||
final int limit;
|
||||
|
||||
/// The channel to search for users.
|
||||
final Channel channel;
|
||||
|
||||
/// The client to search for users in case [mentionAllAppUsers] is True.
|
||||
final StreamChatClient? client;
|
||||
|
||||
/// When enabled mentions search users across the entire app.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool mentionAllAppUsers;
|
||||
|
||||
/// Customize the tile for the mentions overlay.
|
||||
final UserMentionTileBuilder? mentionsTileBuilder;
|
||||
|
||||
/// Callback called when a user is selected.
|
||||
final ValueSetter<User>? onMentionUserTap;
|
||||
|
||||
@override
|
||||
_StreamMentionAutocompleteOptionsState createState() =>
|
||||
_StreamMentionAutocompleteOptionsState();
|
||||
}
|
||||
|
||||
class _StreamMentionAutocompleteOptionsState
|
||||
extends State<StreamMentionAutocompleteOptions> {
|
||||
late Future<List<User>> userMentionsFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
userMentionsFuture = queryMentions(widget.query);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant StreamMentionAutocompleteOptions oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.channel != oldWidget.channel ||
|
||||
widget.query != oldWidget.query ||
|
||||
widget.mentionAllAppUsers != oldWidget.mentionAllAppUsers ||
|
||||
widget.limit != oldWidget.limit) {
|
||||
userMentionsFuture = queryMentions(widget.query);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<User>>(
|
||||
future: userMentionsFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return const SizedBox.shrink();
|
||||
if (!snapshot.hasData) return const SizedBox.shrink();
|
||||
final users = snapshot.data!;
|
||||
|
||||
return StreamAutocompleteOptions<User>(
|
||||
options: users,
|
||||
optionBuilder: (context, user) {
|
||||
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
||||
return Material(
|
||||
color: colorTheme.barsBg,
|
||||
child: InkWell(
|
||||
onTap: widget.onMentionUserTap == null
|
||||
? null
|
||||
: () => widget.onMentionUserTap!(user),
|
||||
child: widget.mentionsTileBuilder?.call(context, user) ??
|
||||
StreamUserMentionTile(user),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<User> get membersAndWatchers {
|
||||
final state = widget.channel.state!;
|
||||
return {
|
||||
...state.watchers,
|
||||
...state.members.map((it) => it.user),
|
||||
}.whereType<User>().toList(growable: false);
|
||||
}
|
||||
|
||||
Future<List<User>> queryMentions(String query) async {
|
||||
if (widget.mentionAllAppUsers) {
|
||||
return _queryUsers(query);
|
||||
}
|
||||
|
||||
var channelState = widget.channel.state;
|
||||
|
||||
channelState = channelState!;
|
||||
final members = channelState.members;
|
||||
|
||||
// By default, we return maximum 100 members via queryChannels api call.
|
||||
// Thus it is safe to assume, that if number of members in channel.state
|
||||
// is < 100, then all the members are already available on client side
|
||||
// and we don't need to make any api call to queryMembers endpoint.
|
||||
if (members.length < 100) {
|
||||
final matchingUsers = membersAndWatchers.search(query);
|
||||
return matchingUsers.toList(growable: false);
|
||||
}
|
||||
|
||||
final result = await _queryMembers(query);
|
||||
return result
|
||||
.map((it) => it.user)
|
||||
.whereType<User>()
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
Future<List<Member>> _queryMembers(String query) async {
|
||||
final response = await widget.channel.queryMembers(
|
||||
pagination: PaginationParams(limit: widget.limit),
|
||||
filter: query.isEmpty
|
||||
? const Filter.empty()
|
||||
: Filter.autoComplete('name', query),
|
||||
);
|
||||
return response.members;
|
||||
}
|
||||
|
||||
Future<List<User>> _queryUsers(String query) async {
|
||||
assert(
|
||||
widget.client != null,
|
||||
'StreamChatClient is required in order to query all app users',
|
||||
);
|
||||
final response = await widget.client!.queryUsers(
|
||||
pagination: PaginationParams(limit: widget.limit),
|
||||
filter: query.isEmpty
|
||||
? const Filter.empty()
|
||||
: Filter.or([
|
||||
Filter.autoComplete('id', query),
|
||||
Filter.autoComplete('name', query),
|
||||
]),
|
||||
sort: [const SortOption('id', direction: SortOption.ASC)],
|
||||
);
|
||||
return response.users;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user