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,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template attachmentButton}
|
||||
/// A button for adding attachments to a chat on mobile.
|
||||
/// {@endtemplate}
|
||||
class AttachmentButton extends StatelessWidget {
|
||||
/// {@macro attachmentButton}
|
||||
const AttachmentButton({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
/// The color of the button.
|
||||
final Color color;
|
||||
|
||||
/// The callback to perform when the button is tapped or clicked.
|
||||
final VoidCallback onPressed;
|
||||
|
||||
/// Returns a copy of this object with the given fields updated.
|
||||
AttachmentButton copyWith({
|
||||
Key? key,
|
||||
Color? color,
|
||||
VoidCallback? onPressed,
|
||||
}) {
|
||||
return AttachmentButton(
|
||||
key: key ?? this.key,
|
||||
color: color ?? this.color,
|
||||
onPressed: onPressed ?? this.onPressed,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: StreamSvgIcon.attach(
|
||||
color: color,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
splashRadius: 24,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export 'stream_file_picker.dart';
|
||||
export 'stream_gallery_picker.dart';
|
||||
export 'stream_image_picker.dart';
|
||||
export 'stream_video_picker.dart';
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler.dart';
|
||||
import 'package:stream_chat_flutter/src/message_input/attachment_picker/stream_attachment_picker.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/utils.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// Widget used to pick files from the device
|
||||
class StreamFilePicker extends StatelessWidget {
|
||||
/// Creates a [StreamFilePicker] widget.
|
||||
const StreamFilePicker({
|
||||
super.key,
|
||||
required this.onFilePicked,
|
||||
this.dialogTitle,
|
||||
this.initialDirectory,
|
||||
this.type = FileType.any,
|
||||
this.allowedExtensions,
|
||||
this.onFileLoading,
|
||||
this.allowCompression = true,
|
||||
this.withData = false,
|
||||
this.withReadStream = false,
|
||||
this.lockParentWindow = false,
|
||||
});
|
||||
|
||||
/// Callback called when a file is picked.
|
||||
final ValueSetter<Attachment?> onFilePicked;
|
||||
|
||||
/// Title of the file picker dialog.
|
||||
final String? dialogTitle;
|
||||
|
||||
/// Initial directory of the file picker dialog.
|
||||
final String? initialDirectory;
|
||||
|
||||
/// Type of the file to pick.
|
||||
final FileType type;
|
||||
|
||||
/// Allowed extensions of the file to pick.
|
||||
final List<String>? allowedExtensions;
|
||||
|
||||
/// Callback called when the file picker is loading a file.
|
||||
final Function(FilePickerStatus)? onFileLoading;
|
||||
|
||||
/// Whether to allow compression of the file.
|
||||
final bool allowCompression;
|
||||
|
||||
/// Whether to include the file data in the [Attachment].
|
||||
final bool withData;
|
||||
|
||||
/// Whether to include the file read stream in the [Attachment].
|
||||
final bool withReadStream;
|
||||
|
||||
/// Whether to lock the parent window when the file picker is open.
|
||||
final bool lockParentWindow;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
return OptionDrawer(
|
||||
child: EndOfFrameCallbackWidget(
|
||||
child: StreamSvgIcon.files(
|
||||
size: 240,
|
||||
color: theme.colorTheme.disabled,
|
||||
),
|
||||
onEndOfFrame: (_) async {
|
||||
final pickedFile = await runInPermissionRequestLock(() {
|
||||
return StreamAttachmentHandler.instance.pickFile(
|
||||
dialogTitle: dialogTitle,
|
||||
initialDirectory: initialDirectory,
|
||||
type: type,
|
||||
allowedExtensions: allowedExtensions,
|
||||
onFileLoading: onFileLoading,
|
||||
allowCompression: allowCompression,
|
||||
withData: withData,
|
||||
withReadStream: withReadStream,
|
||||
lockParentWindow: lockParentWindow,
|
||||
);
|
||||
});
|
||||
|
||||
onFilePicked.call(pickedFile);
|
||||
},
|
||||
errorBuilder: (context, error, stacktrace) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
StreamSvgIcon.files(
|
||||
size: 240,
|
||||
color: theme.colorTheme.disabled,
|
||||
),
|
||||
Text(
|
||||
context.translations.enablePhotoAndVideoAccessMessage,
|
||||
style: theme.textTheme.body.copyWith(
|
||||
color: theme.colorTheme.textLowEmphasis,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: PhotoManager.openSetting,
|
||||
child: Text(
|
||||
context.translations.allowGalleryAccessMessage,
|
||||
style: theme.textTheme.bodyBold.copyWith(
|
||||
color: theme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:stream_chat_flutter/src/message_input/attachment_picker/stream_attachment_picker.dart';
|
||||
import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/scroll_view/photo_gallery/stream_photo_gallery.dart';
|
||||
import 'package:stream_chat_flutter/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/utils.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// Max image resolution which can be resized by the CDN.
|
||||
// Taken from https://getstream.io/chat/docs/flutter-dart/file_uploads/?language=dart#image-resizing
|
||||
const maxCDNImageResolution = 16800000;
|
||||
|
||||
/// Widget used to pick media from the device gallery.
|
||||
class StreamGalleryPicker extends StatefulWidget {
|
||||
/// Creates a [StreamGalleryPicker] widget.
|
||||
const StreamGalleryPicker({
|
||||
super.key,
|
||||
this.limit = 50,
|
||||
required this.selectedMediaItems,
|
||||
required this.onMediaItemSelected,
|
||||
this.mediaThumbnailSize = const ThumbnailSize(400, 400),
|
||||
this.mediaThumbnailFormat = ThumbnailFormat.jpeg,
|
||||
this.mediaThumbnailQuality = 100,
|
||||
this.mediaThumbnailScale = 1,
|
||||
});
|
||||
|
||||
/// Maximum number of media items that can be selected.
|
||||
final int limit;
|
||||
|
||||
/// List of selected media items.
|
||||
final Iterable<String> selectedMediaItems;
|
||||
|
||||
/// Callback called when an media item is selected.
|
||||
final ValueSetter<AssetEntity> onMediaItemSelected;
|
||||
|
||||
/// Size of the attachment thumbnails.
|
||||
///
|
||||
/// Defaults to (400, 400).
|
||||
final ThumbnailSize mediaThumbnailSize;
|
||||
|
||||
/// Format of the attachment thumbnails.
|
||||
///
|
||||
/// Defaults to [ThumbnailFormat.jpeg].
|
||||
final ThumbnailFormat mediaThumbnailFormat;
|
||||
|
||||
/// The quality value for the attachment thumbnails.
|
||||
///
|
||||
/// Valid from 1 to 100.
|
||||
/// Defaults to 100.
|
||||
final int mediaThumbnailQuality;
|
||||
|
||||
/// The scale to apply on the [attachmentThumbnailSize].
|
||||
final double mediaThumbnailScale;
|
||||
|
||||
@override
|
||||
State<StreamGalleryPicker> createState() => _StreamGalleryPickerState();
|
||||
}
|
||||
|
||||
class _StreamGalleryPickerState extends State<StreamGalleryPicker> {
|
||||
Future<PermissionState>? requestPermission;
|
||||
late StreamPhotoGalleryController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = StreamPhotoGalleryController(limit: widget.limit);
|
||||
requestPermission = runInPermissionRequestLock(
|
||||
PhotoManager.requestPermissionExtend,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(StreamGalleryPicker oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.limit != oldWidget.limit) {
|
||||
_controller.dispose();
|
||||
_controller = StreamPhotoGalleryController(limit: widget.limit);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<PermissionState>(
|
||||
future: requestPermission,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) return const SizedBox.shrink();
|
||||
|
||||
final theme = StreamChatTheme.of(context);
|
||||
final textTheme = theme.textTheme;
|
||||
final colorTheme = theme.colorTheme;
|
||||
|
||||
// Available on both Android and iOS.
|
||||
final isAuthorized = snapshot.data == PermissionState.authorized;
|
||||
// Only available on iOS.
|
||||
final isLimited = snapshot.data == PermissionState.limited;
|
||||
|
||||
final isPermissionGranted = isAuthorized || isLimited;
|
||||
|
||||
return OptionDrawer(
|
||||
actions: [
|
||||
if (isLimited)
|
||||
IconButton(
|
||||
color: colorTheme.accentPrimary,
|
||||
icon: const Icon(Icons.add_circle_outline_rounded),
|
||||
onPressed: () async {
|
||||
await PhotoManager.presentLimited();
|
||||
_controller.doInitialLoad();
|
||||
},
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (!isPermissionGranted) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
StreamSvgIcon.pictures(
|
||||
size: 240,
|
||||
color: colorTheme.disabled,
|
||||
),
|
||||
Text(
|
||||
context.translations.enablePhotoAndVideoAccessMessage,
|
||||
style: textTheme.body.copyWith(
|
||||
color: colorTheme.textLowEmphasis,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: PhotoManager.openSetting,
|
||||
child: Text(
|
||||
context.translations.allowGalleryAccessMessage,
|
||||
style: textTheme.bodyBold.copyWith(
|
||||
color: colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return StreamPhotoGallery(
|
||||
shrinkWrap: true,
|
||||
controller: _controller,
|
||||
onMediaTap: widget.onMediaItemSelected,
|
||||
loadMoreTriggerIndex: 10,
|
||||
padding: const EdgeInsets.all(2),
|
||||
thumbnailSize: widget.mediaThumbnailSize,
|
||||
thumbnailFormat: widget.mediaThumbnailFormat,
|
||||
thumbnailQuality: widget.mediaThumbnailQuality,
|
||||
thumbnailScale: widget.mediaThumbnailScale,
|
||||
itemBuilder: (context, mediaItems, index, defaultWidget) {
|
||||
final media = mediaItems[index];
|
||||
return defaultWidget.copyWith(
|
||||
selected: widget.selectedMediaItems.contains(media.id),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
extension StreamImagePickerX on StreamAttachmentPickerController {
|
||||
///
|
||||
Future<void> addAssetAttachment(AssetEntity asset) async {
|
||||
final mediaFile = await asset.originFile;
|
||||
|
||||
if (mediaFile == null) return;
|
||||
|
||||
var cachedFile = mediaFile;
|
||||
|
||||
final type = asset.type;
|
||||
if (type == AssetType.image) {
|
||||
// Resize image if it's resolution is greater than the
|
||||
// [maxCDNImageResolution].
|
||||
final imageResolution = asset.width * asset.height;
|
||||
if (imageResolution > maxCDNImageResolution) {
|
||||
final aspect = imageResolution / maxCDNImageResolution;
|
||||
final updatedSize = asset.size / (math.sqrt(aspect));
|
||||
final resizedImage = await asset.thumbnailDataWithSize(
|
||||
ThumbnailSize(
|
||||
updatedSize.width.floor(),
|
||||
updatedSize.height.floor(),
|
||||
),
|
||||
quality: 70,
|
||||
);
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
cachedFile = await File(
|
||||
'${tempDir.path}/${mediaFile.path.split('/').last}',
|
||||
).create().then((it) => it.writeAsBytes(resizedImage!));
|
||||
}
|
||||
}
|
||||
|
||||
final file = AttachmentFile(
|
||||
path: cachedFile.path,
|
||||
size: await cachedFile.length(),
|
||||
bytes: cachedFile.readAsBytesSync(),
|
||||
);
|
||||
|
||||
final extraDataMap = <String, Object>{};
|
||||
|
||||
final mimeType = file.mimeType?.mimeType;
|
||||
|
||||
if (mimeType != null) {
|
||||
extraDataMap['mime_type'] = mimeType;
|
||||
}
|
||||
|
||||
extraDataMap['file_size'] = file.size!;
|
||||
|
||||
final attachment = Attachment(
|
||||
id: asset.id,
|
||||
file: file,
|
||||
type: asset.type.toAttachmentType(),
|
||||
extraData: extraDataMap,
|
||||
);
|
||||
|
||||
return addAttachment(attachment);
|
||||
}
|
||||
|
||||
///
|
||||
Future<void> removeAssetAttachment(AssetEntity asset) async {
|
||||
if (asset.type == AssetType.image) {
|
||||
final image = await asset.originFile;
|
||||
if (image != null) {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final cachedFile =
|
||||
File('${tempDir.path}/${image.path.split('/').last}');
|
||||
if (cachedFile.existsSync()) {
|
||||
cachedFile.deleteSync();
|
||||
}
|
||||
}
|
||||
}
|
||||
return removeAttachmentById(asset.id);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
extension AssetTypeX on AssetType {
|
||||
///
|
||||
String toAttachmentType() {
|
||||
switch (this) {
|
||||
case AssetType.image:
|
||||
return 'image';
|
||||
case AssetType.video:
|
||||
return 'video';
|
||||
case AssetType.audio:
|
||||
return 'audio';
|
||||
case AssetType.other:
|
||||
return 'file';
|
||||
}
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler.dart';
|
||||
import 'package:stream_chat_flutter/src/message_input/attachment_picker/stream_attachment_picker.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/src/utils/helpers.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// Widget used to pick images from the device.
|
||||
class StreamImagePicker extends StatelessWidget {
|
||||
/// Creates a [StreamImagePicker] widget.
|
||||
const StreamImagePicker({
|
||||
super.key,
|
||||
required this.onImagePicked,
|
||||
this.source = ImageSource.camera,
|
||||
this.maxWidth,
|
||||
this.maxHeight,
|
||||
this.imageQuality,
|
||||
this.preferredCameraDevice = CameraDevice.rear,
|
||||
});
|
||||
|
||||
/// Callback called when an image is picked.
|
||||
final ValueSetter<Attachment?> onImagePicked;
|
||||
|
||||
/// Source of the image to pick.
|
||||
final ImageSource source;
|
||||
|
||||
/// Maximum width of the image.
|
||||
final double? maxWidth;
|
||||
|
||||
/// Maximum height of the image.
|
||||
final double? maxHeight;
|
||||
|
||||
/// Quality of the image.
|
||||
final int? imageQuality;
|
||||
|
||||
/// Preferred camera device to use.
|
||||
final CameraDevice preferredCameraDevice;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
return OptionDrawer(
|
||||
child: EndOfFrameCallbackWidget(
|
||||
child: StreamSvgIcon.camera(
|
||||
size: 240,
|
||||
color: theme.colorTheme.disabled,
|
||||
),
|
||||
onEndOfFrame: (_) async {
|
||||
final pickedImage = await runInPermissionRequestLock(() {
|
||||
return StreamAttachmentHandler.instance.pickImage(
|
||||
source: source,
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
imageQuality: imageQuality,
|
||||
preferredCameraDevice: preferredCameraDevice,
|
||||
);
|
||||
});
|
||||
|
||||
onImagePicked.call(pickedImage);
|
||||
},
|
||||
errorBuilder: (context, error, stacktrace) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
StreamSvgIcon.camera(
|
||||
size: 240,
|
||||
color: theme.colorTheme.disabled,
|
||||
),
|
||||
Text(
|
||||
context.translations.enablePhotoAndVideoAccessMessage,
|
||||
style: theme.textTheme.body.copyWith(
|
||||
color: theme.colorTheme.textLowEmphasis,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: PhotoManager.openSetting,
|
||||
child: Text(
|
||||
context.translations.allowGalleryAccessMessage,
|
||||
style: theme.textTheme.bodyBold.copyWith(
|
||||
color: theme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Widget used to capture video using the device camera.
|
||||
class StreamVideoPicker extends StatelessWidget {
|
||||
/// Creates a [StreamVideoPicker] widget.
|
||||
const StreamVideoPicker({
|
||||
super.key,
|
||||
required this.onVideoPicked,
|
||||
this.source = ImageSource.camera,
|
||||
this.preferredCameraDevice = CameraDevice.rear,
|
||||
this.maxDuration,
|
||||
});
|
||||
|
||||
/// Callback called when a video is picked.
|
||||
final ValueSetter<Attachment?> onVideoPicked;
|
||||
|
||||
/// Source of the video to pick.
|
||||
final ImageSource source;
|
||||
|
||||
/// Preferred camera device to use.
|
||||
final CameraDevice preferredCameraDevice;
|
||||
|
||||
/// Maximum duration of the video.
|
||||
final Duration? maxDuration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
return OptionDrawer(
|
||||
child: EndOfFrameCallbackWidget(
|
||||
child: StreamSvgIcon.record(
|
||||
size: 240,
|
||||
color: theme.colorTheme.disabled,
|
||||
),
|
||||
onEndOfFrame: (_) async {
|
||||
final pickedVideo = await runInPermissionRequestLock(() {
|
||||
return StreamAttachmentHandler.instance.pickVideo(
|
||||
source: source,
|
||||
preferredCameraDevice: preferredCameraDevice,
|
||||
maxDuration: maxDuration,
|
||||
);
|
||||
});
|
||||
|
||||
onVideoPicked.call(pickedVideo);
|
||||
},
|
||||
errorBuilder: (context, error, stacktrace) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
StreamSvgIcon.record(
|
||||
size: 240,
|
||||
color: theme.colorTheme.disabled,
|
||||
),
|
||||
Text(
|
||||
context.translations.enablePhotoAndVideoAccessMessage,
|
||||
style: theme.textTheme.body.copyWith(
|
||||
color: theme.colorTheme.textLowEmphasis,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton(
|
||||
onPressed: PhotoManager.openSetting,
|
||||
child: Text(
|
||||
context.translations.allowGalleryAccessMessage,
|
||||
style: theme.textTheme.bodyBold.copyWith(
|
||||
color: theme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+821
@@ -0,0 +1,821 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/message_input/attachment_picker/options/options.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// The default maximum size for media attachments.
|
||||
const kDefaultMaxAttachmentSize = 100 * 1024 * 1024; // 100MB in Bytes
|
||||
|
||||
/// The default maximum number of media attachments.
|
||||
const kDefaultMaxAttachmentCount = 10;
|
||||
|
||||
/// Controller class for [StreamAttachmentPicker].
|
||||
class StreamAttachmentPickerController extends ValueNotifier<List<Attachment>> {
|
||||
/// Creates a new instance of [StreamAttachmentPickerController].
|
||||
StreamAttachmentPickerController({
|
||||
this.initialAttachments,
|
||||
this.maxAttachmentSize = kDefaultMaxAttachmentSize,
|
||||
this.maxAttachmentCount = kDefaultMaxAttachmentCount,
|
||||
}) : assert(
|
||||
(initialAttachments?.length ?? 0) <= maxAttachmentCount,
|
||||
'''The initial attachments count must be less than or equal to maxAttachmentCount''',
|
||||
),
|
||||
super(initialAttachments ?? const []);
|
||||
|
||||
/// The max attachment size allowed in bytes.
|
||||
final int maxAttachmentSize;
|
||||
|
||||
/// The max attachment count allowed.
|
||||
final int maxAttachmentCount;
|
||||
|
||||
/// The initial attachments.
|
||||
final List<Attachment>? initialAttachments;
|
||||
|
||||
@override
|
||||
set value(List<Attachment> newValue) {
|
||||
if (newValue.length > maxAttachmentCount) {
|
||||
throw ArgumentError(
|
||||
'The maximum number of attachments is $maxAttachmentCount.',
|
||||
);
|
||||
}
|
||||
super.value = newValue;
|
||||
}
|
||||
|
||||
Future<String> _saveToCache(AttachmentFile file) async {
|
||||
// Cache the attachment in a temporary file.
|
||||
return StreamAttachmentHandler.instance.saveAttachmentFile(
|
||||
attachmentFile: file,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _removeFromCache(AttachmentFile file) {
|
||||
// Remove the cached attachment file.
|
||||
return StreamAttachmentHandler.instance.deleteAttachmentFile(
|
||||
attachmentFile: file,
|
||||
);
|
||||
}
|
||||
|
||||
/// Adds a new attachment to the message.
|
||||
Future<void> addAttachment(Attachment attachment) async {
|
||||
assert(attachment.fileSize != null, '');
|
||||
if (attachment.fileSize! > maxAttachmentSize) {
|
||||
throw ArgumentError(
|
||||
'The size of the attachment is ${attachment.fileSize} bytes, '
|
||||
'but the maximum size allowed is $maxAttachmentSize bytes.',
|
||||
);
|
||||
}
|
||||
|
||||
final file = attachment.file;
|
||||
final uploadState = attachment.uploadState;
|
||||
|
||||
// No need to cache the attachment if it's already uploaded
|
||||
// or we are on web.
|
||||
if (file == null || uploadState.isSuccess || isWeb) {
|
||||
value = [...value, attachment];
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache the attachment in a temporary file.
|
||||
final tempFilePath = await _saveToCache(file);
|
||||
|
||||
value = [
|
||||
...value,
|
||||
attachment.copyWith(
|
||||
file: file.copyWith(
|
||||
path: tempFilePath,
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Removes the specified [attachment] from the message.
|
||||
Future<void> removeAttachment(Attachment attachment) async {
|
||||
final file = attachment.file;
|
||||
final uploadState = attachment.uploadState;
|
||||
|
||||
if (file == null || uploadState.isSuccess || isWeb) {
|
||||
value = [...value]..remove(attachment);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the cached attachment file.
|
||||
await _removeFromCache(file);
|
||||
|
||||
value = [...value]..remove(attachment);
|
||||
}
|
||||
|
||||
/// Remove the attachment with the given [attachmentId].
|
||||
void removeAttachmentById(String attachmentId) {
|
||||
final attachment = value.firstWhereOrNull(
|
||||
(attachment) => attachment.id == attachmentId,
|
||||
);
|
||||
|
||||
if (attachment == null) return;
|
||||
|
||||
removeAttachment(attachment);
|
||||
}
|
||||
|
||||
/// Clears all the attachments.
|
||||
Future<void> clear() async {
|
||||
final attachments = [...value];
|
||||
for (final attachment in attachments) {
|
||||
final file = attachment.file;
|
||||
final uploadState = attachment.uploadState;
|
||||
|
||||
if (file == null || uploadState.isSuccess || isWeb) continue;
|
||||
|
||||
await _removeFromCache(file);
|
||||
}
|
||||
value = const [];
|
||||
}
|
||||
|
||||
/// Resets the controller to its initial state.
|
||||
Future<void> reset() async {
|
||||
final attachments = [...value];
|
||||
for (final attachment in attachments) {
|
||||
final file = attachment.file;
|
||||
final uploadState = attachment.uploadState;
|
||||
|
||||
if (file == null || uploadState.isSuccess || isWeb) continue;
|
||||
|
||||
await _removeFromCache(file);
|
||||
}
|
||||
value = initialAttachments ?? const [];
|
||||
}
|
||||
}
|
||||
|
||||
/// The possible picker types of the attachment picker.
|
||||
enum AttachmentPickerType {
|
||||
/// The attachment picker will only allow to pick images.
|
||||
images,
|
||||
|
||||
/// The attachment picker will only allow to pick videos.
|
||||
videos,
|
||||
|
||||
/// The attachment picker will only allow to pick audios.
|
||||
audios,
|
||||
|
||||
/// The attachment picker will only allow to pick files or documents.
|
||||
files,
|
||||
}
|
||||
|
||||
/// Function signature for building the attachment picker option view.
|
||||
typedef AttachmentPickerOptionViewBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
StreamAttachmentPickerController controller,
|
||||
);
|
||||
|
||||
/// Model class for the attachment picker options.
|
||||
class AttachmentPickerOption {
|
||||
/// Creates a new instance of [AttachmentPickerOption].
|
||||
const AttachmentPickerOption({
|
||||
this.key,
|
||||
required this.supportedTypes,
|
||||
required this.icon,
|
||||
this.title,
|
||||
this.optionViewBuilder,
|
||||
});
|
||||
|
||||
/// A key to identify the option.
|
||||
final String? key;
|
||||
|
||||
/// The icon of the option.
|
||||
final Widget icon;
|
||||
|
||||
/// The title of the option.
|
||||
final String? title;
|
||||
|
||||
/// The supported types of the option.
|
||||
final Iterable<AttachmentPickerType> supportedTypes;
|
||||
|
||||
/// The option view builder.
|
||||
final AttachmentPickerOptionViewBuilder? optionViewBuilder;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AttachmentPickerOption &&
|
||||
runtimeType == other.runtimeType &&
|
||||
key == other.key &&
|
||||
const IterableEquality().equals(supportedTypes, other.supportedTypes);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
key.hashCode ^ const IterableEquality().hash(supportedTypes);
|
||||
}
|
||||
|
||||
/// The attachment picker option for the web or desktop platforms.
|
||||
class WebOrDesktopAttachmentPickerOption extends AttachmentPickerOption {
|
||||
/// Creates a new instance of [WebOrDesktopAttachmentPickerOption].
|
||||
WebOrDesktopAttachmentPickerOption({
|
||||
super.key,
|
||||
required AttachmentPickerType type,
|
||||
required super.icon,
|
||||
required super.title,
|
||||
}) : super(supportedTypes: [type]);
|
||||
|
||||
/// Creates a new instance of [WebOrDesktopAttachmentPickerOption] from
|
||||
/// [option].
|
||||
factory WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption(
|
||||
AttachmentPickerOption option,
|
||||
) {
|
||||
return WebOrDesktopAttachmentPickerOption(
|
||||
key: option.key,
|
||||
type: option.supportedTypes.first,
|
||||
icon: option.icon,
|
||||
title: option.title,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String get title => super.title!;
|
||||
|
||||
/// Type of the option.
|
||||
AttachmentPickerType get type => supportedTypes.first;
|
||||
}
|
||||
|
||||
/// Helpful extensions for [StreamAttachmentPickerController].
|
||||
extension AttachmentPickerOptionTypeX on StreamAttachmentPickerController {
|
||||
/// Returns the list of available attachment picker options.
|
||||
Set<AttachmentPickerType> get currentAttachmentPickerTypes {
|
||||
final containsImage = value.any((it) => it.type == 'image');
|
||||
final containsVideo = value.any((it) => it.type == 'video');
|
||||
final containsAudio = value.any((it) => it.type == 'audio');
|
||||
final containsFile = value.any((it) => it.type == 'file');
|
||||
|
||||
return {
|
||||
if (containsImage) AttachmentPickerType.images,
|
||||
if (containsVideo) AttachmentPickerType.videos,
|
||||
if (containsAudio) AttachmentPickerType.audios,
|
||||
if (containsFile) AttachmentPickerType.files,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the list of enabled picker types.
|
||||
Set<AttachmentPickerType> filterEnabledTypes({
|
||||
required Iterable<AttachmentPickerOption> options,
|
||||
}) {
|
||||
final availableTypes = currentAttachmentPickerTypes;
|
||||
final enabledTypes = <AttachmentPickerType>{};
|
||||
for (final option in options) {
|
||||
final supportedTypes = option.supportedTypes;
|
||||
if (availableTypes.any(supportedTypes.contains)) {
|
||||
enabledTypes.addAll(supportedTypes);
|
||||
}
|
||||
}
|
||||
return enabledTypes;
|
||||
}
|
||||
|
||||
/// Returns true if the [initialAttachments] are changed.
|
||||
bool get isValueChanged {
|
||||
final isEqual = UnorderedIterableEquality(
|
||||
EqualityBy((Attachment attachment) => attachment.id),
|
||||
).equals(value, initialAttachments);
|
||||
|
||||
return !isEqual;
|
||||
}
|
||||
}
|
||||
|
||||
/// Function signature for the callback when the web or desktop attachment
|
||||
/// picker option gets tapped.
|
||||
typedef OnWebOrDesktopAttachmentPickerOptionTap = void Function(
|
||||
BuildContext context,
|
||||
StreamAttachmentPickerController controller,
|
||||
WebOrDesktopAttachmentPickerOption option,
|
||||
);
|
||||
|
||||
/// Bottom sheet widget for the web or desktop version of the attachment picker.
|
||||
class StreamWebOrDesktopAttachmentPickerBottomSheet extends StatelessWidget {
|
||||
/// Creates a new instance of [StreamWebOrDesktopAttachmentPickerBottomSheet].
|
||||
const StreamWebOrDesktopAttachmentPickerBottomSheet({
|
||||
super.key,
|
||||
required this.options,
|
||||
required this.controller,
|
||||
this.onOptionTap,
|
||||
});
|
||||
|
||||
/// The list of options.
|
||||
final Set<WebOrDesktopAttachmentPickerOption> options;
|
||||
|
||||
/// The controller of the attachment picker.
|
||||
final StreamAttachmentPickerController controller;
|
||||
|
||||
/// The callback when the option gets tapped.
|
||||
final OnWebOrDesktopAttachmentPickerOptionTap? onOptionTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enabledTypes = controller.filterEnabledTypes(options: options);
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
...options.map((option) {
|
||||
VoidCallback? onOptionTap;
|
||||
if (this.onOptionTap != null) {
|
||||
onOptionTap = () {
|
||||
this.onOptionTap?.call(context, controller, option);
|
||||
};
|
||||
}
|
||||
|
||||
final enabled = enabledTypes.isEmpty ||
|
||||
enabledTypes.any((it) => it == option.type);
|
||||
|
||||
return ListTile(
|
||||
enabled: enabled,
|
||||
leading: option.icon,
|
||||
title: Text(option.title),
|
||||
onTap: onOptionTap,
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bottom sheet widget for the mobile version of the attachment picker.
|
||||
class StreamMobileAttachmentPickerBottomSheet extends StatefulWidget {
|
||||
/// Creates a new instance of [StreamMobileAttachmentPickerBottomSheet].
|
||||
const StreamMobileAttachmentPickerBottomSheet({
|
||||
super.key,
|
||||
required this.options,
|
||||
required this.controller,
|
||||
this.initialOption,
|
||||
this.onSendAttachments,
|
||||
});
|
||||
|
||||
/// The list of options.
|
||||
final Set<AttachmentPickerOption> options;
|
||||
|
||||
/// The initial option to be selected.
|
||||
final AttachmentPickerOption? initialOption;
|
||||
|
||||
/// The controller of the attachment picker.
|
||||
final StreamAttachmentPickerController controller;
|
||||
|
||||
/// The callback when the send button gets tapped.
|
||||
final ValueSetter<List<Attachment>>? onSendAttachments;
|
||||
|
||||
@override
|
||||
State<StreamMobileAttachmentPickerBottomSheet> createState() =>
|
||||
_StreamMobileAttachmentPickerBottomSheetState();
|
||||
}
|
||||
|
||||
class _StreamMobileAttachmentPickerBottomSheetState
|
||||
extends State<StreamMobileAttachmentPickerBottomSheet> {
|
||||
late AttachmentPickerOption _currentOption;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.initialOption == null) {
|
||||
final enabledTypes = widget.controller.filterEnabledTypes(
|
||||
options: widget.options,
|
||||
);
|
||||
if (enabledTypes.isNotEmpty) {
|
||||
_currentOption = widget.options.firstWhere((it) {
|
||||
return it.supportedTypes.contains(enabledTypes.first);
|
||||
});
|
||||
} else {
|
||||
_currentOption = widget.options.first;
|
||||
}
|
||||
} else {
|
||||
_currentOption = widget.initialOption!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<List<Attachment>>(
|
||||
valueListenable: widget.controller,
|
||||
builder: (context, attachments, _) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
_AttachmentPickerOptions(
|
||||
controller: widget.controller,
|
||||
options: widget.options,
|
||||
currentOption: _currentOption,
|
||||
onSendAttachment: widget.onSendAttachments,
|
||||
onOptionSelected: (option) async {
|
||||
setState(() => _currentOption = option);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: _currentOption.optionViewBuilder
|
||||
?.call(context, widget.controller) ??
|
||||
const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AttachmentPickerOptions extends StatelessWidget {
|
||||
const _AttachmentPickerOptions({
|
||||
required this.options,
|
||||
required this.currentOption,
|
||||
required this.controller,
|
||||
this.onOptionSelected,
|
||||
this.onSendAttachment,
|
||||
});
|
||||
|
||||
final Iterable<AttachmentPickerOption> options;
|
||||
final AttachmentPickerOption currentOption;
|
||||
final StreamAttachmentPickerController controller;
|
||||
final ValueSetter<AttachmentPickerOption>? onOptionSelected;
|
||||
final ValueSetter<List<Attachment>>? onSendAttachment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
||||
return ValueListenableBuilder<List<Attachment>>(
|
||||
valueListenable: controller,
|
||||
builder: (context, attachments, __) {
|
||||
final enabledTypes = controller.filterEnabledTypes(options: options);
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
...options.map(
|
||||
(option) {
|
||||
final supportedTypes = option.supportedTypes;
|
||||
|
||||
final isSelected = option == currentOption;
|
||||
final isEnabled = enabledTypes.isEmpty ||
|
||||
enabledTypes.any(supportedTypes.contains);
|
||||
|
||||
final color = isSelected
|
||||
? colorTheme.accentPrimary
|
||||
: colorTheme.textLowEmphasis;
|
||||
|
||||
final onPressed =
|
||||
isEnabled ? () => onOptionSelected!(option) : null;
|
||||
|
||||
return IconButton(
|
||||
color: color,
|
||||
disabledColor: colorTheme.disabled,
|
||||
icon: option.icon,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final isEnabled =
|
||||
onSendAttachment != null && controller.isValueChanged;
|
||||
|
||||
final onPressed = isEnabled
|
||||
? () {
|
||||
onSendAttachment!(attachments);
|
||||
}
|
||||
: null;
|
||||
|
||||
return IconButton(
|
||||
iconSize: 22,
|
||||
color: colorTheme.accentPrimary,
|
||||
disabledColor: colorTheme.disabled,
|
||||
icon: StreamSvgIcon.emptyCircleLeft().toIconThemeSvgIcon(),
|
||||
onPressed: onPressed,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Signature used by [EndOfFrameCallbackWidget.errorBuilder] to create a
|
||||
/// replacement widget to render.
|
||||
typedef EndOfFrameCallbackErrorWidgetBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
Object error,
|
||||
StackTrace? stackTrace,
|
||||
);
|
||||
|
||||
/// Function signature for a callback that is called when the end of the frame
|
||||
/// is reached.
|
||||
typedef EndOfFrameCallback = FutureOr<void> Function(BuildContext context);
|
||||
|
||||
/// A widget that calls the given [callback] when the end of the frame is
|
||||
/// reached.
|
||||
class EndOfFrameCallbackWidget extends StatefulWidget {
|
||||
/// Creates a new instance of [EndOfFrameCallbackWidget].
|
||||
const EndOfFrameCallbackWidget({
|
||||
super.key,
|
||||
required this.onEndOfFrame,
|
||||
this.child,
|
||||
this.errorBuilder,
|
||||
});
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
final Widget? child;
|
||||
|
||||
/// The callback that is called when the end of the frame is reached.x
|
||||
final EndOfFrameCallback onEndOfFrame;
|
||||
|
||||
/// The callback that will be called if the [onEndOfFrame] callback throws an
|
||||
/// error.
|
||||
final EndOfFrameCallbackErrorWidgetBuilder? errorBuilder;
|
||||
|
||||
@override
|
||||
State<EndOfFrameCallbackWidget> createState() =>
|
||||
_EndOfFrameCallbackWidgetState();
|
||||
}
|
||||
|
||||
class _EndOfFrameCallbackWidgetState extends State<EndOfFrameCallbackWidget> {
|
||||
Object? _error;
|
||||
StackTrace? _stackTrace;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.endOfFrame.then((_) async {
|
||||
if (mounted) {
|
||||
try {
|
||||
await widget.onEndOfFrame(context);
|
||||
} catch (error, stackTrace) {
|
||||
setState(() {
|
||||
_error = error;
|
||||
_stackTrace = stackTrace;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final error = _error;
|
||||
final stackTrace = _stackTrace;
|
||||
|
||||
if (error != null) {
|
||||
final errorBuilder = widget.errorBuilder;
|
||||
if (errorBuilder != null) {
|
||||
return errorBuilder(context, error, stackTrace);
|
||||
}
|
||||
return const Text('An error occurred');
|
||||
}
|
||||
|
||||
// Reset the error and stack trace so that we don't keep showing the same
|
||||
// error over and over.
|
||||
_error = null;
|
||||
_stackTrace = null;
|
||||
|
||||
return widget.child ?? const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
const _kDefaultOptionDrawerShape = RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
);
|
||||
|
||||
/// A widget that will be shown in the attachment picker.
|
||||
/// It can be used to show a custom view for each attachment picker option.
|
||||
class OptionDrawer extends StatelessWidget {
|
||||
/// Creates a widget that will be shown in the attachment picker.
|
||||
const OptionDrawer({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.color,
|
||||
this.elevation = 2,
|
||||
this.margin = EdgeInsets.zero,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.shape = _kDefaultOptionDrawerShape,
|
||||
this.title,
|
||||
this.actions = const [],
|
||||
});
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
final Widget child;
|
||||
|
||||
/// The background color of the options card.
|
||||
///
|
||||
/// Defaults to [StreamColorTheme.barsBg].
|
||||
final Color? color;
|
||||
|
||||
/// The elevation of the options card.
|
||||
///
|
||||
/// The default value is 2.
|
||||
final double elevation;
|
||||
|
||||
/// The margin of the options card.
|
||||
///
|
||||
/// The default value is [EdgeInsets.zero].
|
||||
final EdgeInsetsGeometry margin;
|
||||
|
||||
/// The clip behavior of the options card.
|
||||
///
|
||||
/// The default value is [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// The shape of the options card.
|
||||
final ShapeBorder shape;
|
||||
|
||||
/// The title of the options card.
|
||||
final Widget? title;
|
||||
|
||||
/// The actions available for the options card.
|
||||
final List<Widget> actions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
||||
|
||||
var height = 20.0;
|
||||
if (title != null || actions.isNotEmpty) {
|
||||
height = 40.0;
|
||||
}
|
||||
|
||||
final leading = title ?? const SizedBox.shrink();
|
||||
|
||||
Widget trailing;
|
||||
if (actions.isNotEmpty) {
|
||||
trailing = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: actions,
|
||||
);
|
||||
} else {
|
||||
trailing = const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: elevation,
|
||||
color: color ?? colorTheme.barsBg,
|
||||
margin: margin,
|
||||
shape: shape,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: height,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: leading),
|
||||
Container(
|
||||
height: 4,
|
||||
width: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: colorTheme.disabled,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
Expanded(child: trailing),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the mobile version of the attachment picker.
|
||||
Widget mobileAttachmentPickerBuilder({
|
||||
required BuildContext context,
|
||||
required StreamAttachmentPickerController controller,
|
||||
Iterable<AttachmentPickerOption>? customOptions,
|
||||
ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400),
|
||||
ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg,
|
||||
int attachmentThumbnailQuality = 100,
|
||||
double attachmentThumbnailScale = 1,
|
||||
}) {
|
||||
return StreamMobileAttachmentPickerBottomSheet(
|
||||
controller: controller,
|
||||
onSendAttachments: Navigator.of(context).pop,
|
||||
options: {
|
||||
if (customOptions != null) ...customOptions,
|
||||
AttachmentPickerOption(
|
||||
key: 'gallery-picker',
|
||||
icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(),
|
||||
supportedTypes: [
|
||||
AttachmentPickerType.images,
|
||||
AttachmentPickerType.videos,
|
||||
],
|
||||
optionViewBuilder: (context, controller) {
|
||||
final selectedIds = controller.value.map((it) => it.id);
|
||||
return StreamGalleryPicker(
|
||||
selectedMediaItems: selectedIds,
|
||||
mediaThumbnailSize: attachmentThumbnailSize,
|
||||
mediaThumbnailFormat: attachmentThumbnailFormat,
|
||||
mediaThumbnailQuality: attachmentThumbnailQuality,
|
||||
mediaThumbnailScale: attachmentThumbnailScale,
|
||||
onMediaItemSelected: (media) async {
|
||||
if (selectedIds.contains(media.id)) {
|
||||
return controller.removeAssetAttachment(media);
|
||||
}
|
||||
return controller.addAssetAttachment(media);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
AttachmentPickerOption(
|
||||
key: 'file-picker',
|
||||
icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(),
|
||||
supportedTypes: [AttachmentPickerType.files],
|
||||
optionViewBuilder: (context, controller) {
|
||||
return StreamFilePicker(
|
||||
onFilePicked: (file) async {
|
||||
if (file != null) await controller.addAttachment(file);
|
||||
return Navigator.pop(context, controller.value);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
AttachmentPickerOption(
|
||||
key: 'image-picker',
|
||||
icon: StreamSvgIcon.camera(size: 36).toIconThemeSvgIcon(),
|
||||
supportedTypes: [AttachmentPickerType.images],
|
||||
optionViewBuilder: (context, controller) {
|
||||
return StreamImagePicker(
|
||||
onImagePicked: (image) async {
|
||||
if (image != null) {
|
||||
await controller.addAttachment(image);
|
||||
}
|
||||
return Navigator.pop(context, controller.value);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
AttachmentPickerOption(
|
||||
key: 'video-picker',
|
||||
icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(),
|
||||
supportedTypes: [AttachmentPickerType.videos],
|
||||
optionViewBuilder: (context, controller) {
|
||||
return StreamVideoPicker(
|
||||
onVideoPicked: (video) async {
|
||||
if (video != null) {
|
||||
await controller.addAttachment(video);
|
||||
}
|
||||
return Navigator.pop(context, controller.value);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the web or desktop version of the attachment picker.
|
||||
Widget webOrDesktopAttachmentPickerBuilder({
|
||||
required BuildContext context,
|
||||
required StreamAttachmentPickerController controller,
|
||||
Iterable<WebOrDesktopAttachmentPickerOption>? customOptions,
|
||||
ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400),
|
||||
ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg,
|
||||
int attachmentThumbnailQuality = 100,
|
||||
double attachmentThumbnailScale = 1,
|
||||
}) {
|
||||
return StreamWebOrDesktopAttachmentPickerBottomSheet(
|
||||
controller: controller,
|
||||
options: {
|
||||
if (customOptions != null) ...customOptions,
|
||||
WebOrDesktopAttachmentPickerOption(
|
||||
key: 'image-picker',
|
||||
type: AttachmentPickerType.images,
|
||||
icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(),
|
||||
title: 'Upload a photo',
|
||||
),
|
||||
WebOrDesktopAttachmentPickerOption(
|
||||
key: 'video-picker',
|
||||
type: AttachmentPickerType.videos,
|
||||
icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(),
|
||||
title: 'Upload a video',
|
||||
),
|
||||
WebOrDesktopAttachmentPickerOption(
|
||||
key: 'file-picker',
|
||||
type: AttachmentPickerType.files,
|
||||
icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(),
|
||||
title: 'Upload a file',
|
||||
),
|
||||
},
|
||||
onOptionTap: (context, controller, option) async {
|
||||
final attachment = await StreamAttachmentHandler.instance.pickFile(
|
||||
type: option.type.fileType,
|
||||
);
|
||||
if (attachment != null) {
|
||||
await controller.addAttachment(attachment);
|
||||
}
|
||||
return Navigator.pop(context, controller.value);
|
||||
},
|
||||
);
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Shows a modal material design bottom sheet.
|
||||
///
|
||||
/// A modal bottom sheet is an alternative to a menu or a dialog and prevents
|
||||
/// the user from interacting with the rest of the app.
|
||||
///
|
||||
/// A closely related widget is a persistent bottom sheet, which shows
|
||||
/// information that supplements the primary content of the app without
|
||||
/// preventing the use from interacting with the app. Persistent bottom sheets
|
||||
/// can be created and displayed with the [showBottomSheet] function or the
|
||||
/// [ScaffoldState.showBottomSheet] method.
|
||||
///
|
||||
/// The `context` argument is used to look up the [Navigator] and [Theme] for
|
||||
/// the bottom sheet. It is only used when the method is called. Its
|
||||
/// corresponding widget can be safely removed from the tree before the bottom
|
||||
/// sheet is closed.
|
||||
///
|
||||
/// The `isScrollControlled` parameter specifies whether this is a route for
|
||||
/// a bottom sheet that will utilize [DraggableScrollableSheet]. If you wish
|
||||
/// to have a bottom sheet that has a scrollable child such as a [ListView] or
|
||||
/// a [GridView] and have the bottom sheet be draggable, you should set this
|
||||
/// parameter to true.
|
||||
///
|
||||
/// The `useRootNavigator` parameter ensures that the root navigator is used to
|
||||
/// display the [BottomSheet] when set to `true`. This is useful in the case
|
||||
/// that a modal [BottomSheet] needs to be displayed above all other content
|
||||
/// but the caller is inside another [Navigator].
|
||||
///
|
||||
/// The [isDismissible] parameter specifies whether the bottom sheet will be
|
||||
/// dismissed when user taps on the scrim.
|
||||
///
|
||||
/// The [enableDrag] parameter specifies whether the bottom sheet can be
|
||||
/// dragged up and down and dismissed by swiping downwards.
|
||||
///
|
||||
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior],
|
||||
/// [constraints] and [transitionAnimationController]
|
||||
/// parameters can be passed in to customize the appearance and behavior of
|
||||
/// modal bottom sheets (see the documentation for these on [BottomSheet]
|
||||
/// for more details).
|
||||
///
|
||||
/// The [transitionAnimationController] controls the bottom sheet's entrance and
|
||||
/// exit animations if provided.
|
||||
///
|
||||
/// The optional `routeSettings` parameter sets the [RouteSettings]
|
||||
/// of the modal bottom sheet sheet.
|
||||
/// This is particularly useful in the case that a user wants to observe
|
||||
/// [PopupRoute]s within a [NavigatorObserver].
|
||||
///
|
||||
/// Returns a `Future` that resolves to the value (if any) that was passed to
|
||||
/// [Navigator.pop] when the modal bottom sheet was closed.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [BottomSheet], which becomes the parent of the widget returned by the
|
||||
/// function passed as the `builder` argument to [showModalBottomSheet].
|
||||
/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing
|
||||
/// non-modal bottom sheets.
|
||||
/// * [DraggableScrollableSheet], which allows you to create a bottom sheet
|
||||
/// that grows and then becomes scrollable once it reaches its maximum size.
|
||||
/// * <https://material.io/design/components/sheets-bottom.html#modal-bottom-sheet>
|
||||
Future<T?> showStreamAttachmentPickerModalBottomSheet<T>({
|
||||
required BuildContext context,
|
||||
Iterable<AttachmentPickerOption>? customOptions,
|
||||
List<Attachment>? initialAttachments,
|
||||
StreamAttachmentPickerController? controller,
|
||||
Color? backgroundColor,
|
||||
double? elevation,
|
||||
BoxConstraints? constraints,
|
||||
Color? barrierColor,
|
||||
bool isScrollControlled = false,
|
||||
bool useRootNavigator = false,
|
||||
bool isDismissible = true,
|
||||
bool enableDrag = true,
|
||||
RouteSettings? routeSettings,
|
||||
AnimationController? transitionAnimationController,
|
||||
Clip? clipBehavior = Clip.hardEdge,
|
||||
ShapeBorder? shape,
|
||||
ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400),
|
||||
ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg,
|
||||
int attachmentThumbnailQuality = 100,
|
||||
double attachmentThumbnailScale = 1,
|
||||
}) {
|
||||
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
||||
final color = backgroundColor ?? colorTheme.inputBg;
|
||||
|
||||
return showModalBottomSheet<T>(
|
||||
context: context,
|
||||
backgroundColor: color,
|
||||
elevation: elevation,
|
||||
shape: shape,
|
||||
clipBehavior: clipBehavior,
|
||||
constraints: constraints,
|
||||
barrierColor: barrierColor,
|
||||
isScrollControlled: isScrollControlled,
|
||||
useRootNavigator: useRootNavigator,
|
||||
isDismissible: isDismissible,
|
||||
enableDrag: enableDrag,
|
||||
routeSettings: routeSettings,
|
||||
transitionAnimationController: transitionAnimationController,
|
||||
builder: (BuildContext context) {
|
||||
return StreamPlatformAttachmentPickerBottomSheetBuilder(
|
||||
controller: controller,
|
||||
initialAttachments: initialAttachments,
|
||||
builder: (context, controller, child) {
|
||||
return PlatformWidget(
|
||||
web: (context) {
|
||||
return webOrDesktopAttachmentPickerBuilder.call(
|
||||
context: context,
|
||||
controller: controller,
|
||||
customOptions: customOptions?.map(
|
||||
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
|
||||
),
|
||||
attachmentThumbnailSize: attachmentThumbnailSize,
|
||||
attachmentThumbnailFormat: attachmentThumbnailFormat,
|
||||
attachmentThumbnailQuality: attachmentThumbnailQuality,
|
||||
attachmentThumbnailScale: attachmentThumbnailScale,
|
||||
);
|
||||
},
|
||||
mobile: (context) {
|
||||
return mobileAttachmentPickerBuilder.call(
|
||||
context: context,
|
||||
controller: controller,
|
||||
customOptions: customOptions,
|
||||
attachmentThumbnailSize: attachmentThumbnailSize,
|
||||
attachmentThumbnailFormat: attachmentThumbnailFormat,
|
||||
attachmentThumbnailQuality: attachmentThumbnailQuality,
|
||||
attachmentThumbnailScale: attachmentThumbnailScale,
|
||||
);
|
||||
},
|
||||
desktop: (context) {
|
||||
return webOrDesktopAttachmentPickerBuilder.call(
|
||||
context: context,
|
||||
controller: controller,
|
||||
customOptions: customOptions?.map(
|
||||
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
|
||||
),
|
||||
attachmentThumbnailSize: attachmentThumbnailSize,
|
||||
attachmentThumbnailFormat: attachmentThumbnailFormat,
|
||||
attachmentThumbnailQuality: attachmentThumbnailQuality,
|
||||
attachmentThumbnailScale: attachmentThumbnailScale,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the attachment picker bottom sheet.
|
||||
class StreamPlatformAttachmentPickerBottomSheetBuilder extends StatefulWidget {
|
||||
/// Creates a new instance of the widget.
|
||||
const StreamPlatformAttachmentPickerBottomSheetBuilder({
|
||||
super.key,
|
||||
this.customOptions,
|
||||
this.initialAttachments,
|
||||
this.child,
|
||||
this.controller,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
/// The child widget.
|
||||
final Widget? child;
|
||||
|
||||
/// Builder for the attachment picker bottom sheet.
|
||||
final Widget Function(
|
||||
BuildContext context,
|
||||
StreamAttachmentPickerController controller,
|
||||
Widget? child,
|
||||
) builder;
|
||||
|
||||
/// The custom options to be displayed in the attachment picker.
|
||||
final List<AttachmentPickerOption>? customOptions;
|
||||
|
||||
/// The initial attachments.
|
||||
final List<Attachment>? initialAttachments;
|
||||
|
||||
/// The controller.
|
||||
final StreamAttachmentPickerController? controller;
|
||||
|
||||
@override
|
||||
State<StreamPlatformAttachmentPickerBottomSheetBuilder> createState() =>
|
||||
_StreamPlatformAttachmentPickerBottomSheetBuilderState();
|
||||
}
|
||||
|
||||
class _StreamPlatformAttachmentPickerBottomSheetBuilderState
|
||||
extends State<StreamPlatformAttachmentPickerBottomSheetBuilder> {
|
||||
late StreamAttachmentPickerController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = widget.controller ??
|
||||
StreamAttachmentPickerController(
|
||||
initialAttachments: widget.initialAttachments,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle a potential change in StreamAttachmentPickerController by properly
|
||||
// disposing of the old one and setting up the new one, if needed.
|
||||
void _updateTextEditingController(
|
||||
StreamAttachmentPickerController? old,
|
||||
StreamAttachmentPickerController? current,
|
||||
) {
|
||||
if ((old == null && current == null) || old == current) return;
|
||||
if (old == null) {
|
||||
_controller.dispose();
|
||||
_controller = current!;
|
||||
} else if (current == null) {
|
||||
_controller = StreamAttachmentPickerController();
|
||||
} else {
|
||||
_controller = current;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(
|
||||
StreamPlatformAttachmentPickerBottomSheetBuilder oldWidget,
|
||||
) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_updateTextEditingController(
|
||||
oldWidget.controller,
|
||||
widget.controller,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.controller == null) _controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.builder(context, _controller, widget.child);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template clearInputItemButton}
|
||||
/// Used to clear items from the [MessageInput] field, such as attachments
|
||||
/// or message quotes.
|
||||
/// {@endtemplate}
|
||||
class ClearInputItemButton extends StatelessWidget {
|
||||
/// {@macro clearInputItemButton}
|
||||
const ClearInputItemButton({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
/// The callback to be performed when the button is tapped or clicked.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _streamChatTheme = StreamChatTheme.of(context);
|
||||
return SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: onTap,
|
||||
fillColor:
|
||||
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||
child: Center(
|
||||
child: StreamSvgIcon.close(
|
||||
size: 24,
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart';
|
||||
|
||||
/// {@template commandButton}
|
||||
/// The button that allows a user to use commands in a chat.
|
||||
/// {@endtemplate}
|
||||
class CommandButton extends StatelessWidget {
|
||||
/// {@macro commandButton}
|
||||
const CommandButton({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
/// The color of the button.
|
||||
final Color color;
|
||||
|
||||
/// The action to perform when the button is pressed or clicked.
|
||||
final VoidCallback onPressed;
|
||||
|
||||
/// Returns a copy of this object with the given fields updated.
|
||||
CommandButton copyWith({
|
||||
Key? key,
|
||||
Color? color,
|
||||
VoidCallback? onPressed,
|
||||
}) {
|
||||
return CommandButton(
|
||||
key: key ?? this.key,
|
||||
color: color ?? this.color,
|
||||
onPressed: onPressed ?? this.onPressed,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: StreamSvgIcon.lightning(
|
||||
color: color,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
splashRadius: 24,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Button for showing visual component of slow mode.
|
||||
class StreamCountdownButton extends StatelessWidget {
|
||||
/// Constructor for creating [StreamCountdownButton].
|
||||
const StreamCountdownButton({
|
||||
super.key,
|
||||
required this.count,
|
||||
});
|
||||
|
||||
/// The amount of time remaining until the user can send a message again.
|
||||
/// Measured in seconds.
|
||||
final int count;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.disabled,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: Center(
|
||||
child: Text('$count'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template dmCheckbox}
|
||||
/// Prompts the user to send a reply to a message thread as a DM.
|
||||
/// {@endtemplate}
|
||||
class DmCheckbox extends StatelessWidget {
|
||||
/// {@macro dmCheckbox}
|
||||
const DmCheckbox({
|
||||
super.key,
|
||||
required this.foregroundDecoration,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
required this.crossFadeState,
|
||||
});
|
||||
|
||||
/// The decoration to use for the button's foreground.
|
||||
final BoxDecoration foregroundDecoration;
|
||||
|
||||
/// The color to use for the button.
|
||||
final Color color;
|
||||
|
||||
/// The action to perform when the button is tapped or clicked.
|
||||
final VoidCallback onTap;
|
||||
|
||||
/// The [CrossFadeState] of the animation.
|
||||
final CrossFadeState crossFadeState;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _streamChatTheme = StreamChatTheme.of(context);
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
foregroundDecoration: foregroundDecoration,
|
||||
child: Center(
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
color: color,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
reverseDuration: const Duration(milliseconds: 300),
|
||||
crossFadeState: crossFadeState,
|
||||
firstChild: StreamSvgIcon.check(
|
||||
size: 16,
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
),
|
||||
secondChild: const SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
context.translations.alsoSendAsDirectMessageLabel,
|
||||
style: _streamChatTheme.textTheme.footnote.copyWith(
|
||||
color:
|
||||
_streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/// Location for actions on the [StreamMessageInput].
|
||||
enum ActionsLocation {
|
||||
/// Align to left
|
||||
left,
|
||||
|
||||
/// Align to right
|
||||
right,
|
||||
|
||||
/// Align to left but inside the [TextField]
|
||||
leftInside,
|
||||
|
||||
/// Align to right but inside the [TextField]
|
||||
rightInside,
|
||||
}
|
||||
|
||||
/// Default attachments for widget.
|
||||
enum DefaultAttachmentTypes {
|
||||
/// Image Attachment
|
||||
image,
|
||||
|
||||
/// Video Attachment
|
||||
video,
|
||||
|
||||
/// File Attachment
|
||||
file,
|
||||
}
|
||||
|
||||
/// Available locations for the `sendMessage` button relative to the textField.
|
||||
enum SendButtonLocation {
|
||||
/// inside the textField
|
||||
inside,
|
||||
|
||||
/// outside the textField
|
||||
outside,
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/platform_widget_builder/platform_widget_builder.dart';
|
||||
import 'package:stream_chat_flutter/src/message_input/clear_input_item_button.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/utils.dart';
|
||||
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
/// {@template streamQuotedMessage}
|
||||
/// Widget for the quoted message.
|
||||
/// {@endtemplate}
|
||||
class StreamQuotedMessageWidget extends StatelessWidget {
|
||||
/// {@macro streamQuotedMessage}
|
||||
const StreamQuotedMessageWidget({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.messageTheme,
|
||||
this.reverse = false,
|
||||
this.showBorder = false,
|
||||
this.textLimit = 170,
|
||||
this.attachmentThumbnailBuilders,
|
||||
this.padding = const EdgeInsets.all(8),
|
||||
this.onTap,
|
||||
this.onQuotedMessageClear,
|
||||
this.composing = true,
|
||||
});
|
||||
|
||||
/// The message
|
||||
final Message message;
|
||||
|
||||
/// The message theme
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// If true the widget will be mirrored
|
||||
final bool reverse;
|
||||
|
||||
/// If true the message will show a grey border
|
||||
final bool showBorder;
|
||||
|
||||
/// limit of the text message shown
|
||||
final int textLimit;
|
||||
|
||||
/// Map that defines a thumbnail builder for an attachment type
|
||||
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
|
||||
attachmentThumbnailBuilders;
|
||||
|
||||
/// Padding around the widget
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
/// Callback for tap on widget
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
/// Callback for clearing quoted messages.
|
||||
final VoidCallback? onQuotedMessageClear;
|
||||
|
||||
/// True if the message is being composed
|
||||
final bool composing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = [
|
||||
Flexible(
|
||||
child: _QuotedMessage(
|
||||
message: message,
|
||||
textLimit: textLimit,
|
||||
composing: composing,
|
||||
onQuotedMessageClear: onQuotedMessageClear,
|
||||
messageTheme: messageTheme,
|
||||
showBorder: showBorder,
|
||||
reverse: reverse,
|
||||
attachmentThumbnailBuilders: attachmentThumbnailBuilders,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (message.user != null)
|
||||
StreamUserAvatar(
|
||||
user: message.user!,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
showOnlineStatus: false,
|
||||
),
|
||||
];
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: reverse ? children.reversed.toList() : children,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuotedMessage extends StatelessWidget {
|
||||
const _QuotedMessage({
|
||||
required this.message,
|
||||
required this.textLimit,
|
||||
required this.composing,
|
||||
required this.onQuotedMessageClear,
|
||||
required this.messageTheme,
|
||||
required this.showBorder,
|
||||
required this.reverse,
|
||||
this.attachmentThumbnailBuilders,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
final int textLimit;
|
||||
final bool composing;
|
||||
final VoidCallback? onQuotedMessageClear;
|
||||
final StreamMessageThemeData messageTheme;
|
||||
final bool showBorder;
|
||||
final bool reverse;
|
||||
|
||||
/// Map that defines a thumbnail builder for an attachment type
|
||||
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
|
||||
attachmentThumbnailBuilders;
|
||||
|
||||
bool get _hasAttachments => message.attachments.isNotEmpty;
|
||||
|
||||
bool get _containsText => message.text?.isNotEmpty == true;
|
||||
|
||||
bool get _containsLinkAttachment =>
|
||||
message.attachments.any((element) => element.titleLink != null);
|
||||
|
||||
bool get _isGiphy =>
|
||||
message.attachments.any((element) => element.type == 'giphy');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
||||
var msg = _hasAttachments && !_containsText
|
||||
? message.copyWith(text: message.attachments.last.title ?? '')
|
||||
: message;
|
||||
if (msg.text!.length > textLimit) {
|
||||
msg = msg.copyWith(text: '${msg.text!.substring(0, textLimit - 3)}...');
|
||||
}
|
||||
|
||||
final children = [
|
||||
if (composing)
|
||||
PlatformWidgetBuilder(
|
||||
web: (context, child) => child,
|
||||
desktop: (context, child) => child,
|
||||
child: ClearInputItemButton(
|
||||
onTap: onQuotedMessageClear,
|
||||
),
|
||||
),
|
||||
if (_hasAttachments)
|
||||
_ParseAttachments(
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
attachmentThumbnailBuilders: attachmentThumbnailBuilders,
|
||||
),
|
||||
if (msg.text!.isNotEmpty && !_isGiphy)
|
||||
Flexible(
|
||||
child: StreamMessageText(
|
||||
message: msg,
|
||||
messageTheme: isOnlyEmoji && _containsText
|
||||
? messageTheme.copyWith(
|
||||
messageTextStyle: messageTheme.messageTextStyle?.copyWith(
|
||||
fontSize: 32,
|
||||
),
|
||||
)
|
||||
: messageTheme.copyWith(
|
||||
messageTextStyle: messageTheme.messageTextStyle?.copyWith(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
].insertBetween(const SizedBox(width: 8));
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _getBackgroundColor(context),
|
||||
border: showBorder
|
||||
? Border.all(
|
||||
color: StreamChatTheme.of(context).colorTheme.disabled,
|
||||
)
|
||||
: null,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: const Radius.circular(12),
|
||||
topLeft: const Radius.circular(12),
|
||||
bottomRight: reverse ? const Radius.circular(12) : Radius.zero,
|
||||
bottomLeft: reverse ? Radius.zero : const Radius.circular(12),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment:
|
||||
reverse ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: reverse ? children.reversed.toList() : children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color? _getBackgroundColor(BuildContext context) {
|
||||
if (_containsLinkAttachment) {
|
||||
return messageTheme.linkBackgroundColor;
|
||||
}
|
||||
return messageTheme.messageBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
class _ParseAttachments extends StatelessWidget {
|
||||
const _ParseAttachments({
|
||||
required this.message,
|
||||
required this.messageTheme,
|
||||
this.attachmentThumbnailBuilders,
|
||||
});
|
||||
|
||||
final Message message;
|
||||
final StreamMessageThemeData messageTheme;
|
||||
final Map<String, QuotedMessageAttachmentThumbnailBuilder>?
|
||||
attachmentThumbnailBuilders;
|
||||
|
||||
bool get _containsLinkAttachment =>
|
||||
message.attachments.any((element) => element.titleLink != null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child;
|
||||
Attachment attachment;
|
||||
if (_containsLinkAttachment) {
|
||||
attachment = message.attachments.firstWhere(
|
||||
(element) => element.ogScrapeUrl != null || element.titleLink != null,
|
||||
);
|
||||
child = _UrlAttachment(attachment: attachment);
|
||||
} else {
|
||||
QuotedMessageAttachmentThumbnailBuilder? attachmentBuilder;
|
||||
attachment = message.attachments.last;
|
||||
if (attachmentThumbnailBuilders?.containsKey(attachment.type) == true) {
|
||||
attachmentBuilder = attachmentThumbnailBuilders![attachment.type];
|
||||
}
|
||||
attachmentBuilder = _defaultAttachmentBuilder[attachment.type];
|
||||
if (attachmentBuilder == null) {
|
||||
child = const Offstage();
|
||||
} else {
|
||||
child = attachmentBuilder(context, attachment);
|
||||
}
|
||||
}
|
||||
child = AbsorbPointer(child: child);
|
||||
return Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
type: MaterialType.transparency,
|
||||
shape: attachment.type == 'file'
|
||||
? null
|
||||
: RoundedRectangleBorder(
|
||||
side: const BorderSide(width: 0, color: Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
||||
get _defaultAttachmentBuilder {
|
||||
return {
|
||||
'image': (_, attachment) {
|
||||
return StreamImageAttachment(
|
||||
attachment: attachment,
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
constraints: BoxConstraints.loose(const Size(32, 32)),
|
||||
);
|
||||
},
|
||||
'video': (_, attachment) {
|
||||
return StreamVideoThumbnailImage(
|
||||
key: ValueKey(attachment.assetUrl),
|
||||
video: attachment.file?.path ?? attachment.assetUrl!,
|
||||
constraints: BoxConstraints.loose(const Size(32, 32)),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __) => AttachmentError(
|
||||
constraints: BoxConstraints.loose(const Size(32, 32)),
|
||||
),
|
||||
);
|
||||
},
|
||||
'giphy': (_, attachment) {
|
||||
const size = Size(32, 32);
|
||||
return CachedNetworkImage(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
placeholder: (_, __) {
|
||||
return SizedBox(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl: attachment.thumbUrl ??
|
||||
attachment.imageUrl ??
|
||||
attachment.assetUrl!,
|
||||
errorWidget: (context, url, error) =>
|
||||
AttachmentError(constraints: BoxConstraints.loose(size)),
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
'file': (_, attachment) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: getFileTypeImage(
|
||||
attachment.extraData['mime_type'] as String?,
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _UrlAttachment extends StatelessWidget {
|
||||
const _UrlAttachment({
|
||||
required this.attachment,
|
||||
});
|
||||
|
||||
final Attachment attachment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const size = Size(32, 32);
|
||||
if (attachment.thumbUrl != null) {
|
||||
return Container(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: CachedNetworkImageProvider(
|
||||
attachment.thumbUrl!,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return AttachmentError(constraints: BoxConstraints.loose(size));
|
||||
}
|
||||
}
|
||||
|
||||
class _VideoAttachmentThumbnail extends StatefulWidget {
|
||||
const _VideoAttachmentThumbnail({
|
||||
required this.attachment,
|
||||
});
|
||||
|
||||
final Attachment attachment;
|
||||
|
||||
@override
|
||||
_VideoAttachmentThumbnailState createState() =>
|
||||
_VideoAttachmentThumbnailState();
|
||||
}
|
||||
|
||||
class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
|
||||
late VideoPlayerController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = VideoPlayerController.network(widget.attachment.assetUrl!)
|
||||
..initialize().then((_) {
|
||||
// ignore: no-empty-block
|
||||
setState(() {}); //when your thumbnail will show.
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: _controller.value.isInitialized
|
||||
? VideoPlayer(_controller)
|
||||
: const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template quotingMessageTopArea}
|
||||
/// The area that appears above [MessageInput] when the user is quoting a
|
||||
/// message.
|
||||
///
|
||||
/// Should only be used on mobile platforms.
|
||||
/// {@endtemplate}
|
||||
class QuotingMessageTopArea extends StatelessWidget {
|
||||
/// {@macro quotingMessageTopArea}
|
||||
const QuotingMessageTopArea({
|
||||
super.key,
|
||||
required this.hasQuotedMessage,
|
||||
this.onQuotedMessageCleared,
|
||||
});
|
||||
|
||||
///
|
||||
final bool hasQuotedMessage;
|
||||
|
||||
/// The callback to perform when the "close" button is tapped.
|
||||
///
|
||||
/// Should be [MessageInput.onQuotedMessageCleared].
|
||||
final VoidCallback? onQuotedMessageCleared;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _streamChatTheme = StreamChatTheme.of(context);
|
||||
if (hasQuotedMessage) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: StreamSvgIcon.reply(
|
||||
color: _streamChatTheme.colorTheme.disabled,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
context.translations.replyToMessageLabel,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: StreamSvgIcon.closeSmall(),
|
||||
onPressed: onQuotedMessageCleared?.call,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A [SafeArea] with an enabled toggle
|
||||
class SimpleSafeArea extends StatelessWidget {
|
||||
/// Constructor for [SimpleSafeArea]
|
||||
const SimpleSafeArea({
|
||||
super.key,
|
||||
this.enabled = true,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
/// Wrap [child] with [SafeArea]
|
||||
final bool enabled;
|
||||
|
||||
/// Child widget to wrap
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SafeArea(
|
||||
left: enabled,
|
||||
top: enabled,
|
||||
right: enabled,
|
||||
bottom: enabled,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// A widget that displays a sending button.
|
||||
class StreamMessageSendButton extends StatelessWidget {
|
||||
/// Returns a [StreamMessageSendButton] with the given [timeOut], [isIdle],
|
||||
/// [isCommandEnabled], [isEditEnabled], [idleSendButton], [activeSendButton],
|
||||
/// [onSendMessage].
|
||||
const StreamMessageSendButton({
|
||||
super.key,
|
||||
this.timeOut = 0,
|
||||
this.isIdle = true,
|
||||
this.isCommandEnabled = false,
|
||||
this.isEditEnabled = false,
|
||||
this.idleSendButton,
|
||||
this.activeSendButton,
|
||||
required this.onSendMessage,
|
||||
});
|
||||
|
||||
/// Time out related to slow mode.
|
||||
final int timeOut;
|
||||
|
||||
/// If true the button will be disabled.
|
||||
final bool isIdle;
|
||||
|
||||
/// True if a command is being sent.
|
||||
final bool isCommandEnabled;
|
||||
|
||||
/// True if in editing mode.
|
||||
final bool isEditEnabled;
|
||||
|
||||
/// The widget to display when the button is disabled.
|
||||
final Widget? idleSendButton;
|
||||
|
||||
/// The widget to display when the button is enabled.
|
||||
final Widget? activeSendButton;
|
||||
|
||||
/// The callback to call when the button is pressed.
|
||||
final VoidCallback onSendMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _streamChatTheme = StreamChatTheme.of(context);
|
||||
|
||||
late Widget sendButton;
|
||||
if (timeOut > 0) {
|
||||
sendButton = StreamCountdownButton(count: timeOut);
|
||||
} else if (isIdle) {
|
||||
sendButton = idleSendButton ?? _buildIdleSendButton(context);
|
||||
} else {
|
||||
sendButton = activeSendButton != null
|
||||
? InkWell(
|
||||
onTap: onSendMessage,
|
||||
child: activeSendButton,
|
||||
)
|
||||
: _buildSendButton(context);
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
|
||||
child: sendButton,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIdleSendButton(BuildContext context) {
|
||||
final _messageInputTheme = StreamMessageInputTheme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: StreamSvgIcon(
|
||||
assetName: _getIdleSendIcon(),
|
||||
color: _messageInputTheme.sendButtonIdleColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSendButton(BuildContext context) {
|
||||
final _messageInputTheme = StreamMessageInputTheme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: IconButton(
|
||||
onPressed: onSendMessage,
|
||||
padding: EdgeInsets.zero,
|
||||
splashRadius: 24,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
icon: StreamSvgIcon(
|
||||
assetName: _getSendIcon(),
|
||||
color: _messageInputTheme.sendButtonColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getIdleSendIcon() {
|
||||
if (isCommandEnabled) {
|
||||
return 'Icon_search.svg';
|
||||
} else {
|
||||
return 'Icon_circle_right.svg';
|
||||
}
|
||||
}
|
||||
|
||||
String _getSendIcon() {
|
||||
if (isEditEnabled) {
|
||||
return 'Icon_circle_up.svg';
|
||||
} else if (isCommandEnabled) {
|
||||
return 'Icon_search.svg';
|
||||
} else {
|
||||
return 'Icon_circle_up.svg';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,770 @@
|
||||
// ignore_for_file: prefer-trailing-comma, cascade_invocations, lines_longer_than_80_chars
|
||||
|
||||
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
export 'package:flutter/services.dart'
|
||||
show
|
||||
TextInputType,
|
||||
TextInputAction,
|
||||
TextCapitalization,
|
||||
SmartQuotesType,
|
||||
SmartDashesType;
|
||||
|
||||
/// A widget the wraps the [TextField] and adds some StreamChat specifics.
|
||||
class StreamMessageTextField extends StatefulWidget {
|
||||
/// Creates a Material Design text field.
|
||||
///
|
||||
/// If [decoration] is non-null (which is the default), the text field
|
||||
/// requires one of its ancestors to be a [Material] widget.
|
||||
///
|
||||
/// To remove the decoration entirely (including the extra padding introduced
|
||||
/// by the decoration to save space for the labels), set the [decoration] to
|
||||
/// null.
|
||||
///
|
||||
/// The [maxLines] property can be set to null to remove the restriction on
|
||||
/// the number of lines. By default, it is one, meaning this is a single-line
|
||||
/// text field. [maxLines] must not be zero.
|
||||
///
|
||||
/// The [maxLength] property is set to null by default, which means the
|
||||
/// number of characters allowed in the text field is not restricted. If
|
||||
/// [maxLength] is set a character counter will be displayed below the
|
||||
/// field showing how many characters have been entered. If the value is
|
||||
/// set to a positive integer it will also display the maximum allowed
|
||||
/// number of characters to be entered. If the value is set to
|
||||
/// [TextField.noMaxLength] then only the current length is displayed.
|
||||
///
|
||||
/// After [maxLength] characters have been input, additional input
|
||||
/// is ignored, unless [maxLengthEnforcement] is set to
|
||||
/// [MaxLengthEnforcement.none].
|
||||
/// The text field enforces the length with a
|
||||
/// [LengthLimitingTextInputFormatter],
|
||||
/// which is evaluated after the supplied [inputFormatters], if any.
|
||||
/// The [maxLength] value must be either null or greater than zero.
|
||||
///
|
||||
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
|
||||
/// is null (the default) and [readOnly] is true.
|
||||
///
|
||||
/// The [selectionHeightStyle] and [selectionWidthStyle] properties allow
|
||||
/// changing the shape of the selection highlighting. These properties default
|
||||
/// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and
|
||||
/// must not be null.
|
||||
///
|
||||
/// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
|
||||
/// [scrollPadding], [maxLines], [maxLength],
|
||||
/// [selectionHeightStyle], [selectionWidthStyle], [enableSuggestions], and
|
||||
/// [enableIMEPersonalizedLearning] arguments must not be null.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [maxLength], which discusses the precise meaning of "number of
|
||||
/// characters" and how it may differ from the intuitive meaning.
|
||||
const StreamMessageTextField({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.decoration = const InputDecoration(),
|
||||
TextInputType? keyboardType,
|
||||
this.textInputAction,
|
||||
this.textCapitalization = TextCapitalization.none,
|
||||
this.style,
|
||||
this.strutStyle,
|
||||
this.textAlign = TextAlign.start,
|
||||
this.textAlignVertical,
|
||||
this.textDirection,
|
||||
this.readOnly = false,
|
||||
ToolbarOptions? toolbarOptions,
|
||||
this.showCursor,
|
||||
this.autofocus = false,
|
||||
this.obscuringCharacter = '•',
|
||||
this.obscureText = false,
|
||||
this.autocorrect = true,
|
||||
SmartDashesType? smartDashesType,
|
||||
SmartQuotesType? smartQuotesType,
|
||||
this.enableSuggestions = true,
|
||||
this.maxLines,
|
||||
this.minLines,
|
||||
this.expands = false,
|
||||
this.maxLength,
|
||||
this.maxLengthEnforcement,
|
||||
this.onChanged,
|
||||
this.onEditingComplete,
|
||||
this.onSubmitted,
|
||||
this.onAppPrivateCommand,
|
||||
this.inputFormatters,
|
||||
this.enabled,
|
||||
this.cursorWidth = 2.0,
|
||||
this.cursorHeight,
|
||||
this.cursorRadius,
|
||||
this.cursorColor,
|
||||
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
this.keyboardAppearance,
|
||||
this.scrollPadding = const EdgeInsets.all(20),
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
bool? enableInteractiveSelection,
|
||||
this.selectionControls,
|
||||
this.onTap,
|
||||
this.mouseCursor,
|
||||
this.buildCounter,
|
||||
this.scrollController,
|
||||
this.scrollPhysics,
|
||||
this.autofillHints = const <String>[],
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.restorationId,
|
||||
this.scribbleEnabled = true,
|
||||
this.enableIMEPersonalizedLearning = true,
|
||||
}) : assert(obscuringCharacter.length == 1, ''),
|
||||
smartDashesType = smartDashesType ??
|
||||
(obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||
smartQuotesType = smartQuotesType ??
|
||||
(obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||
assert(maxLines == null || maxLines > 0, ''),
|
||||
assert(minLines == null || minLines > 0, ''),
|
||||
assert(
|
||||
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
||||
"minLines can't be greater than maxLines",
|
||||
),
|
||||
assert(
|
||||
!expands || (maxLines == null && minLines == null),
|
||||
'minLines and maxLines must be null when expands is true.',
|
||||
),
|
||||
assert(!obscureText || maxLines == 1,
|
||||
'Obscured fields cannot be multiline.'),
|
||||
assert(
|
||||
maxLength == null ||
|
||||
maxLength == TextField.noMaxLength ||
|
||||
maxLength > 0,
|
||||
'maxLength must be null or a positive integer.'),
|
||||
|
||||
// Assert the following instead of setting it directly to avoid
|
||||
// surprising the user by silently changing the value they set.
|
||||
assert(
|
||||
!identical(textInputAction, TextInputAction.newline) ||
|
||||
maxLines == 1 ||
|
||||
!identical(keyboardType, TextInputType.text),
|
||||
'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.',
|
||||
),
|
||||
keyboardType = keyboardType ??
|
||||
(maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||
enableInteractiveSelection =
|
||||
enableInteractiveSelection ?? (!readOnly || !obscureText),
|
||||
toolbarOptions = toolbarOptions ??
|
||||
(obscureText
|
||||
? (readOnly
|
||||
// No point in even offering "Select All" in a read-only obscured
|
||||
// field.
|
||||
? const ToolbarOptions()
|
||||
// Writable, but obscured.
|
||||
: const ToolbarOptions(
|
||||
selectAll: true,
|
||||
paste: true,
|
||||
))
|
||||
: (readOnly
|
||||
// Read-only, not obscured.
|
||||
? const ToolbarOptions(
|
||||
selectAll: true,
|
||||
copy: true,
|
||||
)
|
||||
// Writable, not obscured.
|
||||
: const ToolbarOptions(
|
||||
copy: true,
|
||||
cut: true,
|
||||
selectAll: true,
|
||||
paste: true,
|
||||
)));
|
||||
|
||||
/// Controls the message being edited.
|
||||
///
|
||||
/// If null, this widget will create its own [StreamMessageInputController].
|
||||
final StreamMessageInputController? controller;
|
||||
|
||||
/// Defines the keyboard focus for this widget.
|
||||
///
|
||||
/// The [focusNode] is a long-lived object that's typically managed by a
|
||||
/// [StatefulWidget] parent. See [FocusNode] for more information.
|
||||
///
|
||||
/// To give the keyboard focus to this widget, provide a [focusNode] and then
|
||||
/// use the current [FocusScope] to request the focus:
|
||||
///
|
||||
/// ```dart
|
||||
/// FocusScope.of(context).requestFocus(myFocusNode);
|
||||
/// ```
|
||||
///
|
||||
/// This happens automatically when the widget is tapped.
|
||||
///
|
||||
/// To be notified when the widget gains or loses the focus, add a listener
|
||||
/// to the [focusNode]:
|
||||
///
|
||||
/// ```dart
|
||||
/// focusNode.addListener(() { print(myFocusNode.hasFocus); });
|
||||
/// ```
|
||||
///
|
||||
/// If null, this widget will create its own [FocusNode].
|
||||
///
|
||||
/// ## Keyboard
|
||||
///
|
||||
/// Requesting the focus will typically cause the keyboard to be shown
|
||||
/// if it's not showing already.
|
||||
///
|
||||
/// On Android, the user can hide the keyboard - without changing the focus -
|
||||
/// with the system back button. They can restore the keyboard's visibility
|
||||
/// by tapping on a text field. The user might hide the keyboard and
|
||||
/// switch to a physical keyboard, or they might just need to get it
|
||||
/// out of the way for a moment, to expose something it's
|
||||
/// obscuring. In this case requesting the focus again will not
|
||||
/// cause the focus to change, and will not make the keyboard visible.
|
||||
///
|
||||
/// This widget builds an [EditableText] and will ensure that the keyboard is
|
||||
/// showing when it is tapped by calling
|
||||
/// [EditableTextState.requestKeyboard()].
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// The decoration to show around the text field.
|
||||
///
|
||||
/// By default, draws a horizontal line under the text field but can be
|
||||
/// configured to show an icon, label, hint text, and error text.
|
||||
///
|
||||
/// Specify null to remove the decoration entirely (including the
|
||||
/// extra padding introduced by the decoration to save space for the labels).
|
||||
final InputDecoration? decoration;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.keyboardType}
|
||||
final TextInputType keyboardType;
|
||||
|
||||
/// The type of action button to use for the keyboard.
|
||||
///
|
||||
/// Defaults to [TextInputAction.newline] if [keyboardType] is
|
||||
/// [TextInputType.multiline] and [TextInputAction.done] otherwise.
|
||||
final TextInputAction? textInputAction;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.textCapitalization}
|
||||
final TextCapitalization textCapitalization;
|
||||
|
||||
/// The style to use for the text being edited.
|
||||
///
|
||||
/// This text style is also used as the base style for the [decoration].
|
||||
///
|
||||
/// If null, defaults to the `subtitle1` text style from the current [Theme].
|
||||
final TextStyle? style;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.strutStyle}
|
||||
final StrutStyle? strutStyle;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.textAlign}
|
||||
final TextAlign textAlign;
|
||||
|
||||
/// {@macro flutter.material.InputDecorator.textAlignVertical}
|
||||
final TextAlignVertical? textAlignVertical;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.textDirection}
|
||||
final TextDirection? textDirection;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.obscuringCharacter}
|
||||
final String obscuringCharacter;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.obscureText}
|
||||
final bool obscureText;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.autocorrect}
|
||||
final bool autocorrect;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.smartDashesType}
|
||||
final SmartDashesType smartDashesType;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.smartQuotesType}
|
||||
final SmartQuotesType smartQuotesType;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.enableSuggestions}
|
||||
final bool enableSuggestions;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.maxLines}
|
||||
/// * [expands], which determines whether the field should fill the height of
|
||||
/// its parent.
|
||||
final int? maxLines;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.minLines}
|
||||
/// * [expands], which determines whether the field should fill the height of
|
||||
/// its parent.
|
||||
final int? minLines;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.expands}
|
||||
final bool expands;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.readOnly}
|
||||
final bool readOnly;
|
||||
|
||||
/// Configuration of toolbar options.
|
||||
///
|
||||
/// If not set, select all and paste will default to be enabled. Copy and cut
|
||||
/// will be disabled if [obscureText] is true. If [readOnly] is true,
|
||||
/// paste and cut will be disabled regardless.
|
||||
final ToolbarOptions toolbarOptions;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.showCursor}
|
||||
final bool? showCursor;
|
||||
|
||||
/// If [maxLength] is set to this value, only the "current input length"
|
||||
/// part of the character counter is shown.
|
||||
static const int noMaxLength = -1;
|
||||
|
||||
/// The maximum number of characters (Unicode scalar values) to allow in the
|
||||
/// text field.
|
||||
///
|
||||
/// If set, a character counter will be displayed below the
|
||||
/// field showing how many characters have been entered. If set to a number
|
||||
/// greater than 0, it will also display the maximum number allowed. If set
|
||||
/// to [TextField.noMaxLength] then only the current character count is
|
||||
/// displayed.
|
||||
///
|
||||
/// After [maxLength] characters have been input, additional input
|
||||
/// is ignored, unless [maxLengthEnforcement] is set to
|
||||
/// [MaxLengthEnforcement.none].
|
||||
///
|
||||
/// The text field enforces the length with a
|
||||
/// [LengthLimitingTextInputFormatter], which is evaluated after the supplied
|
||||
/// [inputFormatters], if any.
|
||||
///
|
||||
/// This value must be either null, [TextField.noMaxLength], or greater than
|
||||
/// 0.
|
||||
///
|
||||
/// If null (the default) then there is no limit to the number of characters
|
||||
/// that can be entered. If set to [TextField.noMaxLength], then no limit will
|
||||
/// be enforced, but the number of characters entered will still be displayed.
|
||||
///
|
||||
/// Whitespace characters (e.g. newline, space, tab) are included in the
|
||||
/// character count.
|
||||
///
|
||||
/// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
|
||||
final int? maxLength;
|
||||
|
||||
/// Determines how the [maxLength] limit should be enforced.
|
||||
///
|
||||
/// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement}
|
||||
///
|
||||
/// {@macro flutter.services.textFormatter.maxLengthEnforcement}
|
||||
final MaxLengthEnforcement? maxLengthEnforcement;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.onChanged}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [inputFormatters], which are called before [onChanged]
|
||||
/// runs and can validate and change ("format") the input value.
|
||||
/// * [onEditingComplete], [onSubmitted]:
|
||||
/// which are more specialized input change notifications.
|
||||
final ValueChanged<String>? onChanged;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.onEditingComplete}
|
||||
final VoidCallback? onEditingComplete;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.onSubmitted}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextInputAction.next] and [TextInputAction.previous], which
|
||||
/// automatically shift the focus to the next/previous focusable item when
|
||||
/// the user is done editing.
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.onAppPrivateCommand}
|
||||
final AppPrivateCommandCallback? onAppPrivateCommand;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.inputFormatters}
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
|
||||
/// If false the text field is "disabled": it ignores taps and its
|
||||
/// [decoration] is rendered in grey.
|
||||
///
|
||||
/// If non-null this property overrides the [decoration]'s
|
||||
/// [InputDecoration.enabled] property.
|
||||
final bool? enabled;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.cursorWidth}
|
||||
final double cursorWidth;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.cursorHeight}
|
||||
final double? cursorHeight;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.cursorRadius}
|
||||
final Radius? cursorRadius;
|
||||
|
||||
/// The color of the cursor.
|
||||
///
|
||||
/// The cursor indicates the current location of text insertion point in
|
||||
/// the field.
|
||||
///
|
||||
/// If this is null it will default to the ambient
|
||||
/// [TextSelectionThemeData.cursorColor]. If that is null, and the
|
||||
/// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS]
|
||||
/// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use
|
||||
/// the value of [ColorScheme.primary] of [ThemeData.colorScheme].
|
||||
final Color? cursorColor;
|
||||
|
||||
/// Controls how tall the selection highlight boxes are computed to be.
|
||||
///
|
||||
/// See [ui.BoxHeightStyle] for details on available styles.
|
||||
final ui.BoxHeightStyle selectionHeightStyle;
|
||||
|
||||
/// Controls how wide the selection highlight boxes are computed to be.
|
||||
///
|
||||
/// See [ui.BoxWidthStyle] for details on available styles.
|
||||
final ui.BoxWidthStyle selectionWidthStyle;
|
||||
|
||||
/// The appearance of the keyboard.
|
||||
///
|
||||
/// This setting is only honored on iOS devices.
|
||||
///
|
||||
/// If unset, defaults to the brightness of [ThemeData.brightness].
|
||||
final Brightness? keyboardAppearance;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.scrollPadding}
|
||||
final EdgeInsets scrollPadding;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
|
||||
final bool enableInteractiveSelection;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.selectionControls}
|
||||
final TextSelectionControls? selectionControls;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.selectionEnabled}
|
||||
bool get selectionEnabled => enableInteractiveSelection;
|
||||
|
||||
/// {@template flutter.material.textfield.onTap}
|
||||
/// Called for each distinct tap except for every second tap of a double tap.
|
||||
///
|
||||
/// The text field builds a [GestureDetector] to handle input events like tap,
|
||||
/// to trigger focus requests, to move the caret, adjust the selection, etc.
|
||||
/// Handling some of those events by wrapping the text field with a competing
|
||||
/// GestureDetector is problematic.
|
||||
///
|
||||
/// To unconditionally handle taps, without interfering with the text field's
|
||||
/// internal gesture detector, provide this callback.
|
||||
///
|
||||
/// If the text field is created with [enabled] false, taps will not be
|
||||
/// recognized.
|
||||
///
|
||||
/// To be notified when the text field gains or loses the focus, provide a
|
||||
/// [focusNode] and add a listener to that.
|
||||
///
|
||||
/// To listen to arbitrary pointer events without competing with the
|
||||
/// text field's internal gesture detector, use a [Listener].
|
||||
/// {@endtemplate}
|
||||
final GestureTapCallback? onTap;
|
||||
|
||||
/// The cursor for a mouse pointer when it enters or is hovering over the
|
||||
/// widget.
|
||||
///
|
||||
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
|
||||
/// [MaterialStateProperty.resolve] is used for the following
|
||||
/// [MaterialState]s:
|
||||
///
|
||||
/// * [MaterialState.error].
|
||||
/// * [MaterialState.hovered].
|
||||
/// * [MaterialState.focused].
|
||||
/// * [MaterialState.disabled].
|
||||
///
|
||||
/// If this property is null, [MaterialStateMouseCursor.textable] will be
|
||||
/// used.
|
||||
///
|
||||
/// The [mouseCursor] is the only property of [TextField] that controls the
|
||||
/// appearance of the mouse pointer. All other properties related to "cursor"
|
||||
/// stand for the text cursor, which is usually a blinking vertical line at
|
||||
/// the editing position.
|
||||
final MouseCursor? mouseCursor;
|
||||
|
||||
/// Callback that generates a custom [InputDecoration.counter] widget.
|
||||
///
|
||||
/// See [InputCounterWidgetBuilder] for an explanation of the passed in
|
||||
/// arguments. The returned widget will be placed below the line in place of
|
||||
/// the default widget built when [InputDecoration.counterText] is specified.
|
||||
///
|
||||
/// The returned widget will be wrapped in a [Semantics] widget for
|
||||
/// accessibility, but it also needs to be accessible itself. For example,
|
||||
/// if returning a Text widget, set the [Text.semanticsLabel] property.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
/// ```dart
|
||||
/// Widget counter(
|
||||
/// BuildContext context,
|
||||
/// {
|
||||
/// required int currentLength,
|
||||
/// required int? maxLength,
|
||||
/// required bool isFocused,
|
||||
/// }
|
||||
/// ) {
|
||||
/// return Text(
|
||||
/// '$currentLength of $maxLength characters',
|
||||
/// semanticsLabel: 'character count',
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// If buildCounter returns null, then no counter and no Semantics widget will
|
||||
/// be created at all.
|
||||
final InputCounterWidgetBuilder? buildCounter;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.scrollPhysics}
|
||||
final ScrollPhysics? scrollPhysics;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.scrollController}
|
||||
final ScrollController? scrollController;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.autofillHints}
|
||||
/// {@macro flutter.services.AutofillConfiguration.autofillHints}
|
||||
final Iterable<String>? autofillHints;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// {@template flutter.material.textfield.restorationId}
|
||||
/// Restoration ID to save and restore the state of the text field.
|
||||
///
|
||||
/// If non-null, the text field will persist and restore its current scroll
|
||||
/// offset and - if no [controller] has been provided - the content of the
|
||||
/// text field. If a [controller] has been provided, it is the responsibility
|
||||
/// of the owner of that controller to persist and restore it, e.g. by using
|
||||
/// a [RestorableTextEditingController].
|
||||
///
|
||||
/// The state of this widget is persisted in a [RestorationBucket] claimed
|
||||
/// from the surrounding [RestorationScope] using the provided restoration ID.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RestorationManager], which explains how state restoration works in
|
||||
/// Flutter.
|
||||
/// {@endtemplate}
|
||||
final String? restorationId;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.scribbleEnabled}
|
||||
final bool scribbleEnabled;
|
||||
|
||||
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
|
||||
final bool enableIMEPersonalizedLearning;
|
||||
|
||||
@override
|
||||
_StreamMessageTextFieldState createState() => _StreamMessageTextFieldState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<StreamMessageInputController>(
|
||||
'controller', controller,
|
||||
defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode,
|
||||
defaultValue: null));
|
||||
properties
|
||||
.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<InputDecoration>(
|
||||
'decoration', decoration,
|
||||
defaultValue: const InputDecoration()));
|
||||
properties.add(DiagnosticsProperty<TextInputType>(
|
||||
'keyboardType', keyboardType,
|
||||
defaultValue: TextInputType.text));
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
|
||||
properties.add(
|
||||
DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
|
||||
properties.add(DiagnosticsProperty<String>(
|
||||
'obscuringCharacter', obscuringCharacter,
|
||||
defaultValue: '•'));
|
||||
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText,
|
||||
defaultValue: false));
|
||||
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect,
|
||||
defaultValue: true));
|
||||
properties.add(EnumProperty<SmartDashesType>(
|
||||
'smartDashesType', smartDashesType,
|
||||
defaultValue:
|
||||
obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
|
||||
properties.add(EnumProperty<SmartQuotesType>(
|
||||
'smartQuotesType', smartQuotesType,
|
||||
defaultValue:
|
||||
obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
|
||||
properties.add(DiagnosticsProperty<bool>(
|
||||
'enableSuggestions', enableSuggestions,
|
||||
defaultValue: true));
|
||||
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
|
||||
properties.add(IntProperty('minLines', minLines, defaultValue: null));
|
||||
properties.add(
|
||||
DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
||||
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
|
||||
properties.add(EnumProperty<MaxLengthEnforcement>(
|
||||
'maxLengthEnforcement', maxLengthEnforcement,
|
||||
defaultValue: null));
|
||||
properties.add(EnumProperty<TextInputAction>(
|
||||
'textInputAction', textInputAction,
|
||||
defaultValue: null));
|
||||
properties.add(EnumProperty<TextCapitalization>(
|
||||
'textCapitalization', textCapitalization,
|
||||
defaultValue: TextCapitalization.none));
|
||||
properties.add(EnumProperty<TextAlign>('textAlign', textAlign,
|
||||
defaultValue: TextAlign.start));
|
||||
properties.add(DiagnosticsProperty<TextAlignVertical>(
|
||||
'textAlignVertical', textAlignVertical,
|
||||
defaultValue: null));
|
||||
properties.add(EnumProperty<TextDirection>('textDirection', textDirection,
|
||||
defaultValue: null));
|
||||
properties
|
||||
.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
|
||||
properties
|
||||
.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius,
|
||||
defaultValue: null));
|
||||
properties
|
||||
.add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<Brightness>(
|
||||
'keyboardAppearance', keyboardAppearance,
|
||||
defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>(
|
||||
'scrollPadding', scrollPadding,
|
||||
defaultValue: const EdgeInsets.all(20)));
|
||||
properties.add(FlagProperty('selectionEnabled',
|
||||
value: selectionEnabled,
|
||||
defaultValue: true,
|
||||
ifFalse: 'selection disabled'));
|
||||
properties.add(DiagnosticsProperty<TextSelectionControls>(
|
||||
'selectionControls', selectionControls,
|
||||
defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<ScrollController>(
|
||||
'scrollController', scrollController,
|
||||
defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<ScrollPhysics>(
|
||||
'scrollPhysics', scrollPhysics,
|
||||
defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior,
|
||||
defaultValue: Clip.hardEdge));
|
||||
properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled,
|
||||
defaultValue: true));
|
||||
properties.add(DiagnosticsProperty<bool>(
|
||||
'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning,
|
||||
defaultValue: true));
|
||||
}
|
||||
}
|
||||
|
||||
class _StreamMessageTextFieldState extends State<StreamMessageTextField>
|
||||
with RestorationMixin<StreamMessageTextField> {
|
||||
StreamMessageInputController get _effectiveController =>
|
||||
widget.controller ?? _controller!.value;
|
||||
StreamRestorableMessageInputController? _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.controller == null) {
|
||||
_createLocalController();
|
||||
}
|
||||
}
|
||||
|
||||
void _createLocalController([Message? message]) {
|
||||
assert(_controller == null, '');
|
||||
_controller = StreamRestorableMessageInputController(message: message);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant StreamMessageTextField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller == null && oldWidget.controller != null) {
|
||||
_createLocalController(oldWidget.controller!.message);
|
||||
} else if (widget.controller != null && oldWidget.controller == null) {
|
||||
unregisterFromRestoration(_controller!);
|
||||
_controller!.dispose();
|
||||
_controller = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||
if (_controller != null) {
|
||||
_registerController();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String? get restorationId => widget.restorationId;
|
||||
|
||||
void _registerController() {
|
||||
assert(_controller != null, '');
|
||||
registerForRestoration(_controller!, 'controller');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => TextField(
|
||||
controller: _effectiveController.textFieldController,
|
||||
focusNode: widget.focusNode,
|
||||
decoration: widget.decoration,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.textInputAction ??
|
||||
(widget.keyboardType == TextInputType.multiline
|
||||
? TextInputAction.newline
|
||||
: TextInputAction.send),
|
||||
textCapitalization: widget.textCapitalization,
|
||||
style: widget.style,
|
||||
strutStyle: widget.strutStyle,
|
||||
textAlign: widget.textAlign,
|
||||
textAlignVertical: widget.textAlignVertical,
|
||||
textDirection: widget.textDirection,
|
||||
readOnly: widget.readOnly,
|
||||
toolbarOptions: widget.toolbarOptions,
|
||||
showCursor: widget.showCursor,
|
||||
autofocus: widget.autofocus,
|
||||
obscuringCharacter: widget.obscuringCharacter,
|
||||
obscureText: widget.obscureText,
|
||||
autocorrect: widget.autocorrect,
|
||||
smartDashesType: widget.smartDashesType,
|
||||
smartQuotesType: widget.smartQuotesType,
|
||||
enableSuggestions: widget.enableSuggestions,
|
||||
maxLines: widget.maxLines,
|
||||
minLines: widget.minLines,
|
||||
expands: widget.expands,
|
||||
maxLength: widget.maxLength,
|
||||
maxLengthEnforcement: widget.maxLengthEnforcement,
|
||||
onEditingComplete: widget.onEditingComplete,
|
||||
onSubmitted: widget.onSubmitted,
|
||||
onAppPrivateCommand: widget.onAppPrivateCommand,
|
||||
inputFormatters: widget.inputFormatters,
|
||||
enabled: widget.enabled,
|
||||
cursorWidth: widget.cursorWidth,
|
||||
cursorHeight: widget.cursorHeight,
|
||||
cursorRadius: widget.cursorRadius,
|
||||
cursorColor: widget.cursorColor,
|
||||
selectionHeightStyle: widget.selectionHeightStyle,
|
||||
selectionWidthStyle: widget.selectionWidthStyle,
|
||||
keyboardAppearance: widget.keyboardAppearance,
|
||||
scrollPadding: widget.scrollPadding,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||
selectionControls: widget.selectionControls,
|
||||
onTap: widget.onTap,
|
||||
mouseCursor: widget.mouseCursor,
|
||||
buildCounter: widget.buildCounter,
|
||||
scrollController: widget.scrollController,
|
||||
scrollPhysics: widget.scrollPhysics,
|
||||
autofillHints: widget.autofillHints,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
restorationId: widget.restorationId,
|
||||
scribbleEnabled: widget.scribbleEnabled,
|
||||
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user