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,221 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget/sending_indicator_wrapper.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget/thread_painter.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget/thread_participants.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget/username.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template bottomRow}
|
||||
/// The bottom row of a [StreamMessageWidget].
|
||||
///
|
||||
/// Used in [MessageWidgetContent]. Should not be used elsewhere.
|
||||
/// {@endtemplate}
|
||||
class BottomRow extends StatelessWidget {
|
||||
/// {@macro bottomRow}
|
||||
const BottomRow({
|
||||
super.key,
|
||||
required this.isDeleted,
|
||||
required this.message,
|
||||
required this.showThreadReplyIndicator,
|
||||
required this.showInChannel,
|
||||
required this.showTimeStamp,
|
||||
required this.showUsername,
|
||||
required this.reverse,
|
||||
required this.showSendingIndicator,
|
||||
required this.hasUrlAttachments,
|
||||
required this.isGiphy,
|
||||
required this.isOnlyEmoji,
|
||||
required this.messageTheme,
|
||||
required this.streamChatTheme,
|
||||
required this.hasNonUrlAttachments,
|
||||
required this.streamChat,
|
||||
this.deletedBottomRowBuilder,
|
||||
this.onThreadTap,
|
||||
this.usernameBuilder,
|
||||
});
|
||||
|
||||
/// {@macro messageIsDeleted}
|
||||
final bool isDeleted;
|
||||
|
||||
/// {@macro deletedBottomRowBuilder}
|
||||
final Widget Function(BuildContext, Message)? deletedBottomRowBuilder;
|
||||
|
||||
/// {@macro message}
|
||||
final Message message;
|
||||
|
||||
/// {@macro showThreadReplyIndicator}
|
||||
final bool showThreadReplyIndicator;
|
||||
|
||||
/// {@macro showInChannelIndicator}
|
||||
final bool showInChannel;
|
||||
|
||||
/// {@macro showTimestamp}
|
||||
final bool showTimeStamp;
|
||||
|
||||
/// {@macro showUsername}
|
||||
final bool showUsername;
|
||||
|
||||
/// {@macro reverse}
|
||||
final bool reverse;
|
||||
|
||||
/// {@macro showSendingIndicator}
|
||||
final bool showSendingIndicator;
|
||||
|
||||
/// {@macro hasUrlAttachments}
|
||||
final bool hasUrlAttachments;
|
||||
|
||||
/// {@macro isGiphy}
|
||||
final bool isGiphy;
|
||||
|
||||
/// {@macro isOnlyEmoji}
|
||||
final bool isOnlyEmoji;
|
||||
|
||||
/// {@macro hasNonUrlAttachments}
|
||||
final bool hasNonUrlAttachments;
|
||||
|
||||
/// {@macro messageTheme}
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// {@macro onThreadTap}
|
||||
final void Function(Message)? onThreadTap;
|
||||
|
||||
/// {@macro streamChatThemeData}
|
||||
final StreamChatThemeData streamChatTheme;
|
||||
|
||||
/// {@macro streamChat}
|
||||
final StreamChatState streamChat;
|
||||
|
||||
/// {@macro usernameBuilder}
|
||||
final Widget Function(BuildContext, Message)? usernameBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isDeleted) {
|
||||
return deletedBottomRowBuilder?.call(
|
||||
context,
|
||||
message,
|
||||
) ??
|
||||
const Offstage();
|
||||
}
|
||||
|
||||
final children = <WidgetSpan>[];
|
||||
|
||||
final threadParticipants = message.threadParticipants?.take(2);
|
||||
final showThreadParticipants = threadParticipants?.isNotEmpty == true;
|
||||
final replyCount = message.replyCount;
|
||||
|
||||
var msg = context.translations.threadReplyLabel;
|
||||
if (showThreadReplyIndicator && replyCount! > 1) {
|
||||
msg = context.translations.threadReplyCountText(replyCount);
|
||||
}
|
||||
|
||||
// ignore: prefer_function_declarations_over_variables
|
||||
final _onThreadTap = () async {
|
||||
try {
|
||||
var message = this.message;
|
||||
if (showInChannel) {
|
||||
final channel = StreamChannel.of(context);
|
||||
message = await channel.getMessage(message.parentId!);
|
||||
}
|
||||
return onThreadTap!(message);
|
||||
} catch (e, stk) {
|
||||
print(e);
|
||||
print(stk);
|
||||
// ignore: avoid_returning_null_for_void
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const usernameKey = Key('username');
|
||||
|
||||
children.addAll([
|
||||
if (showUsername)
|
||||
WidgetSpan(
|
||||
child: usernameBuilder?.call(context, message) ??
|
||||
Username(
|
||||
key: usernameKey,
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
),
|
||||
),
|
||||
if (showTimeStamp)
|
||||
WidgetSpan(
|
||||
child: Text(
|
||||
Jiffy(message.createdAt.toLocal()).jm,
|
||||
style: messageTheme.createdAtStyle,
|
||||
),
|
||||
),
|
||||
if (showSendingIndicator)
|
||||
WidgetSpan(
|
||||
child: SendingIndicatorWrapper(
|
||||
messageTheme: messageTheme,
|
||||
message: message,
|
||||
hasNonUrlAttachments: hasNonUrlAttachments,
|
||||
streamChat: streamChat,
|
||||
streamChatTheme: streamChatTheme,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
final showThreadTail = !(hasUrlAttachments || isGiphy || isOnlyEmoji) &&
|
||||
(showThreadReplyIndicator || showInChannel);
|
||||
|
||||
final threadIndicatorWidgets = <WidgetSpan>[
|
||||
if (showThreadTail)
|
||||
WidgetSpan(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: context.textScaleFactor *
|
||||
((messageTheme.repliesStyle?.fontSize ?? 1) / 2),
|
||||
),
|
||||
child: CustomPaint(
|
||||
size: const Size(16, 32) * context.textScaleFactor,
|
||||
painter: ThreadReplyPainter(
|
||||
context: context,
|
||||
color: messageTheme.messageBorderColor,
|
||||
reverse: reverse,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showInChannel || showThreadReplyIndicator) ...[
|
||||
if (showThreadParticipants)
|
||||
WidgetSpan(
|
||||
child: SizedBox.fromSize(
|
||||
size: Size((threadParticipants!.length * 8.0) + 8, 16),
|
||||
child: ThreadParticipants(
|
||||
threadParticipants: threadParticipants,
|
||||
streamChatTheme: streamChatTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: _onThreadTap,
|
||||
child: Text(msg, style: messageTheme.repliesStyle),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
if (reverse) {
|
||||
children.addAll(threadIndicatorWidgets.reversed);
|
||||
} else {
|
||||
children.insertAll(0, threadIndicatorWidgets);
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
...children,
|
||||
].insertBetween(const WidgetSpan(child: SizedBox(width: 8))),
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: reverse ? TextAlign.right : TextAlign.left,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
|
||||
/// {@template streamDeletedMessage}
|
||||
/// Displays that a message was deleted at this position in the message list.
|
||||
/// {@endtemplate}
|
||||
class StreamDeletedMessage extends StatelessWidget {
|
||||
/// {@macro streamDeletedMessage}
|
||||
const StreamDeletedMessage({
|
||||
super.key,
|
||||
required this.messageTheme,
|
||||
this.borderRadiusGeometry,
|
||||
this.shape,
|
||||
this.borderSide,
|
||||
this.reverse = false,
|
||||
});
|
||||
|
||||
/// The theme of the message
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// The border radius of the message text
|
||||
final BorderRadiusGeometry? borderRadiusGeometry;
|
||||
|
||||
/// The shape of the message text
|
||||
final ShapeBorder? shape;
|
||||
|
||||
/// The [BorderSide] of the message text
|
||||
final BorderSide? borderSide;
|
||||
|
||||
/// If true the widget will be mirrored
|
||||
final bool reverse;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
return Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
shape: shape ??
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: borderRadiusGeometry ?? BorderRadius.zero,
|
||||
side: borderSide ??
|
||||
BorderSide(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? chatThemeData.colorTheme.barsBg.withAlpha(24)
|
||||
: chatThemeData.colorTheme.textHighEmphasis.withAlpha(24),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Text(
|
||||
context.translations.messageDeletedLabel,
|
||||
style: messageTheme.messageTextStyle?.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: messageTheme.createdAtStyle?.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template messageCard}
|
||||
/// The widget containing a quoted message.
|
||||
///
|
||||
/// Used in [MessageWidgetContent]. Should not be used elsewhere.
|
||||
/// {@endtemplate}
|
||||
class MessageCard extends StatefulWidget {
|
||||
/// {@macro messageCard}
|
||||
const MessageCard({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.isFailedState,
|
||||
required this.showUserAvatar,
|
||||
required this.messageTheme,
|
||||
required this.hasQuotedMessage,
|
||||
required this.hasUrlAttachments,
|
||||
required this.hasNonUrlAttachments,
|
||||
required this.isOnlyEmoji,
|
||||
required this.isGiphy,
|
||||
required this.attachmentBuilders,
|
||||
required this.attachmentPadding,
|
||||
required this.textPadding,
|
||||
required this.reverse,
|
||||
this.shape,
|
||||
this.borderSide,
|
||||
this.borderRadiusGeometry,
|
||||
this.textBuilder,
|
||||
this.onLinkTap,
|
||||
this.onMentionTap,
|
||||
this.onQuotedMessageTap,
|
||||
});
|
||||
|
||||
/// {@macro isFailedState}
|
||||
final bool isFailedState;
|
||||
|
||||
/// {@macro showUserAvatar}
|
||||
final DisplayWidget showUserAvatar;
|
||||
|
||||
/// {@macro shape}
|
||||
final ShapeBorder? shape;
|
||||
|
||||
/// {@macro borderSide}
|
||||
final BorderSide? borderSide;
|
||||
|
||||
/// {@macro messageTheme}
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// {@macro borderRadiusGeometry}
|
||||
final BorderRadiusGeometry? borderRadiusGeometry;
|
||||
|
||||
/// {@macro hasQuotedMessage}
|
||||
final bool hasQuotedMessage;
|
||||
|
||||
/// {@macro hasUrlAttachments}
|
||||
final bool hasUrlAttachments;
|
||||
|
||||
/// {@macro hasNonUrlAttachments}
|
||||
final bool hasNonUrlAttachments;
|
||||
|
||||
/// {@macro isOnlyEmoji}
|
||||
final bool isOnlyEmoji;
|
||||
|
||||
/// {@macro isGiphy}
|
||||
final bool isGiphy;
|
||||
|
||||
/// {@macro message}
|
||||
final Message message;
|
||||
|
||||
/// {@macro attachmentBuilders}
|
||||
final Map<String, AttachmentBuilder> attachmentBuilders;
|
||||
|
||||
/// {@macro attachmentPadding}
|
||||
final EdgeInsetsGeometry attachmentPadding;
|
||||
|
||||
/// {@macro textPadding}
|
||||
final EdgeInsets textPadding;
|
||||
|
||||
/// {@macro textBuilder}
|
||||
final Widget Function(BuildContext, Message)? textBuilder;
|
||||
|
||||
/// {@macro onLinkTap}
|
||||
final void Function(String)? onLinkTap;
|
||||
|
||||
/// {@macro onMentionTap}
|
||||
final void Function(User)? onMentionTap;
|
||||
|
||||
/// {@macro onQuotedMessageTap}
|
||||
final OnQuotedMessageTap? onQuotedMessageTap;
|
||||
|
||||
/// {@macro reverse}
|
||||
final bool reverse;
|
||||
|
||||
@override
|
||||
State<MessageCard> createState() => _MessageCardState();
|
||||
}
|
||||
|
||||
class _MessageCardState extends State<MessageCard> {
|
||||
final GlobalKey attachmentsKey = GlobalKey();
|
||||
final GlobalKey linksKey = GlobalKey();
|
||||
double? widthLimit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final attachmentsRenderBox =
|
||||
attachmentsKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
final attachmentsWidth = attachmentsRenderBox?.size.width;
|
||||
|
||||
final linkRenderBox =
|
||||
linksKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
final linkWidth = linkRenderBox?.size.width;
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (attachmentsWidth != null && linkWidth != null) {
|
||||
widthLimit = max(attachmentsWidth, linkWidth);
|
||||
} else {
|
||||
widthLimit = attachmentsWidth ?? linkWidth;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: (widget.isFailedState ? 15.0 : 0.0) +
|
||||
(widget.showUserAvatar == DisplayWidget.gone ? 0 : 4.0),
|
||||
),
|
||||
shape: widget.shape ??
|
||||
RoundedRectangleBorder(
|
||||
side: widget.borderSide ??
|
||||
BorderSide(
|
||||
color: widget.messageTheme.messageBorderColor ?? Colors.grey,
|
||||
),
|
||||
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
|
||||
),
|
||||
color: _getBackgroundColor(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: widthLimit ?? double.infinity,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.hasQuotedMessage)
|
||||
QuotedMessage(
|
||||
reverse: widget.reverse,
|
||||
message: widget.message,
|
||||
hasNonUrlAttachments: widget.hasNonUrlAttachments,
|
||||
onQuotedMessageTap: widget.onQuotedMessageTap,
|
||||
),
|
||||
if (widget.hasNonUrlAttachments)
|
||||
ParseAttachments(
|
||||
key: attachmentsKey,
|
||||
message: widget.message,
|
||||
attachmentBuilders: widget.attachmentBuilders,
|
||||
attachmentPadding: widget.attachmentPadding,
|
||||
),
|
||||
if (!widget.isGiphy)
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(const Size.fromWidth(500)),
|
||||
child: TextBubble(
|
||||
messageTheme: widget.messageTheme,
|
||||
message: widget.message,
|
||||
textPadding: widget.textPadding,
|
||||
textBuilder: widget.textBuilder,
|
||||
isOnlyEmoji: widget.isOnlyEmoji,
|
||||
hasQuotedMessage: widget.hasQuotedMessage,
|
||||
hasUrlAttachments: widget.hasUrlAttachments,
|
||||
onLinkTap: widget.onLinkTap,
|
||||
onMentionTap: widget.onMentionTap,
|
||||
),
|
||||
),
|
||||
if (widget.hasUrlAttachments && !widget.hasQuotedMessage)
|
||||
_buildUrlAttachment(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUrlAttachment() {
|
||||
final urlAttachment = widget.message.attachments
|
||||
.firstWhere((element) => element.titleLink != null);
|
||||
|
||||
final host = Uri.parse(urlAttachment.titleLink!).host;
|
||||
final splitList = host.split('.');
|
||||
final hostName = splitList.length == 3 ? splitList[1] : splitList[0];
|
||||
final hostDisplayName = urlAttachment.authorName?.capitalize() ??
|
||||
getWebsiteName(hostName.toLowerCase()) ??
|
||||
hostName.capitalize();
|
||||
|
||||
return StreamUrlAttachment(
|
||||
key: linksKey,
|
||||
urlAttachment: urlAttachment,
|
||||
hostDisplayName: hostDisplayName,
|
||||
textPadding: widget.textPadding,
|
||||
messageTheme: widget.messageTheme,
|
||||
);
|
||||
}
|
||||
|
||||
Color? _getBackgroundColor() {
|
||||
if (widget.hasQuotedMessage) {
|
||||
return widget.messageTheme.messageBackgroundColor;
|
||||
}
|
||||
|
||||
if (widget.hasUrlAttachments) {
|
||||
return widget.messageTheme.linkBackgroundColor;
|
||||
}
|
||||
|
||||
if (widget.isOnlyEmoji) {
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
if (widget.isGiphy) {
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
return widget.messageTheme.messageBackgroundColor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template streamMessageText}
|
||||
/// The text content of a message.
|
||||
/// {@endtemplate}
|
||||
class StreamMessageText extends StatelessWidget {
|
||||
/// {@macro streamMessageText}
|
||||
const StreamMessageText({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.messageTheme,
|
||||
this.onMentionTap,
|
||||
this.onLinkTap,
|
||||
});
|
||||
|
||||
/// Message whose text is to be displayed
|
||||
final Message message;
|
||||
|
||||
/// The action to perform when a mention is tapped
|
||||
final void Function(User)? onMentionTap;
|
||||
|
||||
/// The action to perform when a link is tapped
|
||||
final void Function(String)? onLinkTap;
|
||||
|
||||
/// [StreamMessageThemeData] whose text theme is to be applied
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final streamChat = StreamChat.of(context);
|
||||
assert(streamChat.currentUser != null, '');
|
||||
return BetterStreamBuilder<String>(
|
||||
stream: streamChat.currentUserStream.map((it) => it!.language ?? 'en'),
|
||||
initialData: streamChat.currentUser!.language ?? 'en',
|
||||
builder: (context, language) {
|
||||
final messageText = message
|
||||
.translate(language)
|
||||
.replaceMentions()
|
||||
.text
|
||||
?.replaceAll('\n', '\n\n');
|
||||
final themeData = Theme.of(context);
|
||||
return MarkdownBody(
|
||||
data: messageText ?? '',
|
||||
selectable: isDesktopDeviceOrWeb,
|
||||
onTapLink: (
|
||||
String link,
|
||||
String? href,
|
||||
String title,
|
||||
) {
|
||||
if (link.startsWith('@')) {
|
||||
final mentionedUser = message.mentionedUsers.firstWhereOrNull(
|
||||
(u) => '@${u.name}' == link,
|
||||
);
|
||||
|
||||
if (mentionedUser == null) return;
|
||||
|
||||
onMentionTap?.call(mentionedUser);
|
||||
} else {
|
||||
if (onLinkTap != null) {
|
||||
onLinkTap!(link);
|
||||
} else {
|
||||
launchURL(context, link);
|
||||
}
|
||||
}
|
||||
},
|
||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||
themeData.copyWith(
|
||||
textTheme: themeData.textTheme.apply(
|
||||
bodyColor: messageTheme.messageTextStyle?.color,
|
||||
decoration: messageTheme.messageTextStyle?.decoration,
|
||||
decorationColor: messageTheme.messageTextStyle?.decorationColor,
|
||||
decorationStyle: messageTheme.messageTextStyle?.decorationStyle,
|
||||
fontFamily: messageTheme.messageTextStyle?.fontFamily,
|
||||
),
|
||||
),
|
||||
).copyWith(
|
||||
a: messageTheme.messageLinksStyle,
|
||||
p: messageTheme.messageTextStyle,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,461 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget/message_widget_content_components.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget/reactions/desktop_reactions_builder.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/utils.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template messageWidgetContent}
|
||||
/// The main content of a [StreamMessageWidget].
|
||||
///
|
||||
/// Should not be used outside of [MessageWidget.
|
||||
/// {@endtemplate}
|
||||
class MessageWidgetContent extends StatelessWidget {
|
||||
/// {@macro messageWidgetContent}
|
||||
const MessageWidgetContent({
|
||||
super.key,
|
||||
required this.reverse,
|
||||
required this.isPinned,
|
||||
required this.showPinHighlight,
|
||||
required this.showBottomRow,
|
||||
required this.message,
|
||||
required this.showUserAvatar,
|
||||
required this.avatarWidth,
|
||||
required this.showReactions,
|
||||
required this.messageTheme,
|
||||
required this.shouldShowReactions,
|
||||
required this.streamChatTheme,
|
||||
required this.isFailedState,
|
||||
required this.hasQuotedMessage,
|
||||
required this.hasUrlAttachments,
|
||||
required this.hasNonUrlAttachments,
|
||||
required this.isOnlyEmoji,
|
||||
required this.isGiphy,
|
||||
required this.attachmentBuilders,
|
||||
required this.attachmentPadding,
|
||||
required this.textPadding,
|
||||
required this.showReactionPickerIndicator,
|
||||
required this.translateUserAvatar,
|
||||
required this.bottomRowPadding,
|
||||
required this.showInChannel,
|
||||
required this.streamChat,
|
||||
required this.showSendingIndicator,
|
||||
required this.showThreadReplyIndicator,
|
||||
required this.showTimeStamp,
|
||||
required this.showUsername,
|
||||
required this.messageWidget,
|
||||
this.onUserAvatarTap,
|
||||
this.borderRadiusGeometry,
|
||||
this.borderSide,
|
||||
this.shape,
|
||||
this.onQuotedMessageTap,
|
||||
this.onMentionTap,
|
||||
this.onLinkTap,
|
||||
this.textBuilder,
|
||||
this.bottomRowBuilder,
|
||||
this.onThreadTap,
|
||||
this.deletedBottomRowBuilder,
|
||||
this.userAvatarBuilder,
|
||||
this.usernameBuilder,
|
||||
});
|
||||
|
||||
/// {@macro reverse}
|
||||
final bool reverse;
|
||||
|
||||
/// {@macro isPinned}
|
||||
final bool isPinned;
|
||||
|
||||
/// {@macro showPinHighlight}
|
||||
final bool showPinHighlight;
|
||||
|
||||
/// {@macro showBottomRow}
|
||||
final bool showBottomRow;
|
||||
|
||||
/// {@macro message}
|
||||
final Message message;
|
||||
|
||||
/// {@macro showUserAvatar}
|
||||
final DisplayWidget showUserAvatar;
|
||||
|
||||
/// The width of the avatar.
|
||||
final double avatarWidth;
|
||||
|
||||
/// {@macro showReactions}
|
||||
final bool showReactions;
|
||||
|
||||
/// {@macro messageTheme}
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// {@macro shouldShowReactions}
|
||||
final bool shouldShowReactions;
|
||||
|
||||
/// {@macro onUserAvatarTap}
|
||||
final void Function(User)? onUserAvatarTap;
|
||||
|
||||
/// {@macro streamChatThemeData}
|
||||
final StreamChatThemeData streamChatTheme;
|
||||
|
||||
/// {@macro isFailedState}
|
||||
final bool isFailedState;
|
||||
|
||||
/// {@macro borderRadiusGeometry}
|
||||
final BorderRadiusGeometry? borderRadiusGeometry;
|
||||
|
||||
/// {@macro borderSide}
|
||||
final BorderSide? borderSide;
|
||||
|
||||
/// {@macro shape}
|
||||
final ShapeBorder? shape;
|
||||
|
||||
/// {@macro hasQuotedMessage}
|
||||
final bool hasQuotedMessage;
|
||||
|
||||
/// {@macro hasUrlAttachments}
|
||||
final bool hasUrlAttachments;
|
||||
|
||||
/// {@macro hasNonUrlAttachments}
|
||||
final bool hasNonUrlAttachments;
|
||||
|
||||
/// {@macro isOnlyEmoji}
|
||||
final bool isOnlyEmoji;
|
||||
|
||||
/// {@macro isGiphy}
|
||||
final bool isGiphy;
|
||||
|
||||
/// {@macro attachmentBuilders}
|
||||
final Map<String, AttachmentBuilder> attachmentBuilders;
|
||||
|
||||
/// {@macro attachmentPadding}
|
||||
final EdgeInsetsGeometry attachmentPadding;
|
||||
|
||||
/// {@macro textPadding}
|
||||
final EdgeInsets textPadding;
|
||||
|
||||
/// {@macro onQuotedMessageTap}
|
||||
final OnQuotedMessageTap? onQuotedMessageTap;
|
||||
|
||||
/// {@macro onMentionTap}
|
||||
final void Function(User)? onMentionTap;
|
||||
|
||||
/// {@macro onLinkTap}
|
||||
final void Function(String)? onLinkTap;
|
||||
|
||||
/// {@macro textBuilder}
|
||||
final Widget Function(BuildContext, Message)? textBuilder;
|
||||
|
||||
/// {@macro showReactionPickerIndicator}
|
||||
final bool showReactionPickerIndicator;
|
||||
|
||||
/// {@macro translateUserAvatar}
|
||||
final bool translateUserAvatar;
|
||||
|
||||
/// The padding to use for this widget.
|
||||
final double bottomRowPadding;
|
||||
|
||||
/// {@macro bottomRowBuilder}
|
||||
final Widget Function(BuildContext, Message)? bottomRowBuilder;
|
||||
|
||||
/// {@macro showInChannelIndicator}
|
||||
final bool showInChannel;
|
||||
|
||||
/// {@macro streamChat}
|
||||
final StreamChatState streamChat;
|
||||
|
||||
/// {@macro showSendingIndicator}
|
||||
final bool showSendingIndicator;
|
||||
|
||||
/// {@macro showThreadReplyIndicator}
|
||||
final bool showThreadReplyIndicator;
|
||||
|
||||
/// {@macro showTimestamp}
|
||||
final bool showTimeStamp;
|
||||
|
||||
/// {@macro showUsername}
|
||||
final bool showUsername;
|
||||
|
||||
/// {@macro onThreadTap}
|
||||
final void Function(Message)? onThreadTap;
|
||||
|
||||
/// {@macro deletedBottomRowBuilder}
|
||||
final Widget Function(BuildContext, Message)? deletedBottomRowBuilder;
|
||||
|
||||
/// {@macro messageWidget}
|
||||
final StreamMessageWidget messageWidget;
|
||||
|
||||
/// {@macro userAvatarBuilder}
|
||||
final Widget Function(BuildContext, User)? userAvatarBuilder;
|
||||
|
||||
/// {@macro usernameBuilder}
|
||||
final Widget Function(BuildContext, Message)? usernameBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment:
|
||||
reverse ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: reverse
|
||||
? AlignmentDirectional.bottomEnd
|
||||
: AlignmentDirectional.bottomStart,
|
||||
children: [
|
||||
if (showBottomRow)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: !reverse ? bottomRowPadding : 0,
|
||||
right: reverse ? bottomRowPadding : 0,
|
||||
bottom: isPinned && showPinHighlight ? 6.0 : 0.0,
|
||||
),
|
||||
child: bottomRowBuilder?.call(
|
||||
context,
|
||||
message,
|
||||
) ??
|
||||
BottomRow(
|
||||
message: message,
|
||||
reverse: reverse,
|
||||
messageTheme: messageTheme,
|
||||
hasUrlAttachments: hasUrlAttachments,
|
||||
isOnlyEmoji: isOnlyEmoji,
|
||||
isDeleted: message.isDeleted,
|
||||
isGiphy: isGiphy,
|
||||
showInChannel: showInChannel,
|
||||
showSendingIndicator: showSendingIndicator,
|
||||
showThreadReplyIndicator: showThreadReplyIndicator,
|
||||
showTimeStamp: showTimeStamp,
|
||||
showUsername: showUsername,
|
||||
streamChatTheme: streamChatTheme,
|
||||
onThreadTap: onThreadTap,
|
||||
deletedBottomRowBuilder: deletedBottomRowBuilder,
|
||||
streamChat: streamChat,
|
||||
hasNonUrlAttachments: hasNonUrlAttachments,
|
||||
usernameBuilder: usernameBuilder,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: isPinned && showPinHighlight ? 8.0 : 0.0,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
reverse ? CrossAxisAlignment.end : CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (message.pinned &&
|
||||
message.pinnedBy != null &&
|
||||
showPinHighlight)
|
||||
PinnedMessage(
|
||||
pinnedBy: message.pinnedBy!,
|
||||
currentUser: streamChat.currentUser!,
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!reverse &&
|
||||
showUserAvatar == DisplayWidget.show &&
|
||||
message.user != null) ...[
|
||||
UserAvatarTransform(
|
||||
onUserAvatarTap: onUserAvatarTap,
|
||||
userAvatarBuilder: userAvatarBuilder,
|
||||
translateUserAvatar: translateUserAvatar,
|
||||
messageTheme: messageTheme,
|
||||
message: message,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (showUserAvatar == DisplayWidget.hide)
|
||||
SizedBox(width: avatarWidth + 4),
|
||||
Flexible(
|
||||
child: PortalTarget(
|
||||
visible: isMobileDevice && showReactions,
|
||||
portalFollower: isMobileDevice && showReactions
|
||||
? ReactionIndicator(
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
ownId: streamChat.currentUser!.id,
|
||||
reverse: reverse,
|
||||
shouldShowReactions: shouldShowReactions,
|
||||
onTap: () => _showMessageReactionsModal(
|
||||
context,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
anchor: Aligned(
|
||||
follower: Alignment(
|
||||
reverse ? 1 : -1,
|
||||
-1,
|
||||
),
|
||||
target: Alignment(
|
||||
reverse ? -1 : 1,
|
||||
-1,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
padding: showReactions
|
||||
? EdgeInsets.only(
|
||||
top: message.reactionCounts
|
||||
?.isNotEmpty ==
|
||||
true
|
||||
? 18
|
||||
: 0,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: (message.isDeleted && !isFailedState)
|
||||
? Container(
|
||||
// ignore: lines_longer_than_80_chars
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
// ignore: lines_longer_than_80_chars
|
||||
showUserAvatar ==
|
||||
// ignore: lines_longer_than_80_chars
|
||||
DisplayWidget.gone
|
||||
? 0
|
||||
: 4.0,
|
||||
),
|
||||
child: StreamDeletedMessage(
|
||||
borderRadiusGeometry:
|
||||
borderRadiusGeometry,
|
||||
borderSide: borderSide,
|
||||
shape: shape,
|
||||
messageTheme: messageTheme,
|
||||
),
|
||||
)
|
||||
: MessageCard(
|
||||
message: message,
|
||||
isFailedState: isFailedState,
|
||||
showUserAvatar: showUserAvatar,
|
||||
messageTheme: messageTheme,
|
||||
hasQuotedMessage: hasQuotedMessage,
|
||||
hasUrlAttachments: hasUrlAttachments,
|
||||
hasNonUrlAttachments:
|
||||
hasNonUrlAttachments,
|
||||
isOnlyEmoji: isOnlyEmoji,
|
||||
isGiphy: isGiphy,
|
||||
attachmentBuilders: attachmentBuilders,
|
||||
attachmentPadding: attachmentPadding,
|
||||
textPadding: textPadding,
|
||||
reverse: reverse,
|
||||
onQuotedMessageTap: onQuotedMessageTap,
|
||||
onMentionTap: onMentionTap,
|
||||
onLinkTap: onLinkTap,
|
||||
textBuilder: textBuilder,
|
||||
borderRadiusGeometry:
|
||||
borderRadiusGeometry,
|
||||
borderSide: borderSide,
|
||||
shape: shape,
|
||||
),
|
||||
),
|
||||
if (showReactionPickerIndicator)
|
||||
Positioned(
|
||||
right: reverse ? null : 4,
|
||||
left: reverse ? 4 : null,
|
||||
top: -8,
|
||||
child: CustomPaint(
|
||||
painter: ReactionBubblePainter(
|
||||
streamChatTheme.colorTheme.barsBg,
|
||||
Colors.transparent,
|
||||
Colors.transparent,
|
||||
tailCirclesSpace: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (reverse &&
|
||||
showUserAvatar == DisplayWidget.show &&
|
||||
message.user != null) ...[
|
||||
UserAvatarTransform(
|
||||
translateUserAvatar: translateUserAvatar,
|
||||
messageTheme: messageTheme,
|
||||
message: message,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (showUserAvatar == DisplayWidget.hide)
|
||||
SizedBox(width: avatarWidth + 4),
|
||||
],
|
||||
),
|
||||
if (isDesktopDeviceOrWeb && shouldShowReactions) ...[
|
||||
Padding(
|
||||
padding: showUserAvatar != DisplayWidget.gone
|
||||
? EdgeInsets.only(
|
||||
left: avatarWidth + 4,
|
||||
right: avatarWidth + 4,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: DesktopReactionsBuilder(
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
shouldShowReactions: shouldShowReactions,
|
||||
borderSide: borderSide,
|
||||
reverse: reverse,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (showBottomRow)
|
||||
SizedBox(
|
||||
height: context.textScaleFactor * 18.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isFailedState)
|
||||
Positioned(
|
||||
right: reverse ? 0 : null,
|
||||
left: reverse ? null : 0,
|
||||
bottom: showBottomRow ? 18 : -2,
|
||||
child: StreamSvgIcon.error(size: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showMessageReactionsModal(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
showDialog(
|
||||
useRootNavigator: false,
|
||||
context: context,
|
||||
barrierColor: streamChatTheme.colorTheme.overlay,
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: StreamMessageReactionsModal(
|
||||
messageWidget: messageWidget.copyWith(
|
||||
key: const Key('MessageWidget'),
|
||||
message: message.copyWith(
|
||||
text: (message.text?.length ?? 0) > 200
|
||||
? '${message.text!.substring(0, 200)}...'
|
||||
: message.text,
|
||||
),
|
||||
showReactions: false,
|
||||
showUsername: false,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
showSendingIndicator: false,
|
||||
padding: EdgeInsets.zero,
|
||||
showReactionPickerIndicator:
|
||||
showReactions && (message.status == MessageSendingStatus.sent),
|
||||
showPinHighlight: false,
|
||||
showUserAvatar:
|
||||
message.user!.id == channel.client.state.currentUser!.id
|
||||
? DisplayWidget.gone
|
||||
: DisplayWidget.show,
|
||||
),
|
||||
onUserAvatarTap: onUserAvatarTap,
|
||||
messageTheme: messageTheme,
|
||||
reverse: reverse,
|
||||
message: message,
|
||||
showReactions: showReactions,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export 'bottom_row.dart';
|
||||
export 'message_card.dart';
|
||||
export 'parse_attachments.dart';
|
||||
export 'pinned_message.dart';
|
||||
export 'quoted_message.dart';
|
||||
export 'reactions/message_reactions_modal.dart';
|
||||
export 'reactions/reaction_bubble.dart';
|
||||
export 'reactions/reaction_indicator.dart';
|
||||
export 'user_avatar_transform.dart';
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget/message_widget_content_components.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/utils.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template parseAttachments}
|
||||
/// Parses the attachments of a [StreamMessageWidget].
|
||||
///
|
||||
/// Used in [MessageCard]. Should not be used elsewhere.
|
||||
/// {@endtemplate}
|
||||
class ParseAttachments extends StatelessWidget {
|
||||
/// {@macro parseAttachments}
|
||||
const ParseAttachments({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.attachmentBuilders,
|
||||
required this.attachmentPadding,
|
||||
});
|
||||
|
||||
/// {@macro message}
|
||||
final Message message;
|
||||
|
||||
/// {@macro attachmentBuilders}
|
||||
final Map<String, AttachmentBuilder> attachmentBuilders;
|
||||
|
||||
/// {@macro attachmentPadding}
|
||||
final EdgeInsetsGeometry attachmentPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final attachmentGroups = <String, List<Attachment>>{};
|
||||
|
||||
message.attachments
|
||||
.where((element) =>
|
||||
(element.titleLink == null && element.type != null) ||
|
||||
element.type == 'giphy')
|
||||
.forEach((e) {
|
||||
if (attachmentGroups[e.type] == null) {
|
||||
attachmentGroups[e.type!] = [];
|
||||
}
|
||||
|
||||
attachmentGroups[e.type]?.add(e);
|
||||
});
|
||||
|
||||
final attachmentList = <Widget>[];
|
||||
|
||||
attachmentGroups.forEach((type, attachments) {
|
||||
final attachmentBuilder = attachmentBuilders[type];
|
||||
|
||||
if (attachmentBuilder == null) return;
|
||||
final attachmentWidget = attachmentBuilder(
|
||||
context,
|
||||
message,
|
||||
attachments,
|
||||
);
|
||||
attachmentList.add(attachmentWidget);
|
||||
});
|
||||
|
||||
return Padding(
|
||||
padding: attachmentPadding,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: attachmentList.insertBetween(
|
||||
SizedBox(
|
||||
height: attachmentPadding.vertical / 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template pinnedMessage}
|
||||
/// A pinned message in a chat.
|
||||
///
|
||||
/// Used in [MessageWidgetContent]. Should not be used elsewhere.
|
||||
/// {@endtemplate}
|
||||
class PinnedMessage extends StatelessWidget {
|
||||
/// {@macro pinnedMessage}
|
||||
const PinnedMessage({
|
||||
super.key,
|
||||
required this.pinnedBy,
|
||||
required this.currentUser,
|
||||
});
|
||||
|
||||
/// The [User] who pinned this message.
|
||||
final User pinnedBy;
|
||||
|
||||
/// The current [User].
|
||||
final User currentUser;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.pin(
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
Text(
|
||||
context.translations.pinnedByUserText(
|
||||
pinnedBy: pinnedBy,
|
||||
currentUser: currentUser,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.textLowEmphasis,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/message_input/quoted_message_widget.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template quotedMessage}
|
||||
/// A quoted message in a chat.
|
||||
///
|
||||
/// Used in [QuotedMessageCard]. Should not be used elsewhere.
|
||||
/// {@endtemplate}
|
||||
class QuotedMessage extends StatefulWidget {
|
||||
/// {@macro quotedMessage}
|
||||
const QuotedMessage({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.reverse,
|
||||
required this.hasNonUrlAttachments,
|
||||
this.onQuotedMessageTap,
|
||||
});
|
||||
|
||||
/// {@macro message}
|
||||
final Message message;
|
||||
|
||||
/// {@macro onQuotedMessageTap}
|
||||
final OnQuotedMessageTap? onQuotedMessageTap;
|
||||
|
||||
/// {@macro reverse}
|
||||
final bool reverse;
|
||||
|
||||
/// {@macro hasNonUrlAttachments}
|
||||
final bool hasNonUrlAttachments;
|
||||
|
||||
@override
|
||||
State<QuotedMessage> createState() => _QuotedMessageState();
|
||||
}
|
||||
|
||||
class _QuotedMessageState extends State<QuotedMessage> {
|
||||
late StreamChatState _streamChat;
|
||||
late StreamChatThemeData _streamChatTheme;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_streamChatTheme = StreamChatTheme.of(context);
|
||||
_streamChat = StreamChat.of(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMyMessage = widget.message.user?.id == _streamChat.currentUser?.id;
|
||||
final onTap = widget.message.quotedMessage?.isDeleted != true &&
|
||||
widget.onQuotedMessageTap != null
|
||||
? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId)
|
||||
: null;
|
||||
final chatThemeData = _streamChatTheme;
|
||||
return StreamQuotedMessageWidget(
|
||||
onTap: onTap,
|
||||
message: widget.message.quotedMessage!,
|
||||
messageTheme: isMyMessage
|
||||
? chatThemeData.otherMessageTheme
|
||||
: chatThemeData.ownMessageTheme,
|
||||
reverse: widget.reverse,
|
||||
padding: EdgeInsets.only(
|
||||
right: 8,
|
||||
left: 8,
|
||||
top: 8,
|
||||
bottom: widget.hasNonUrlAttachments ? 8 : 0,
|
||||
),
|
||||
composing: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
// ignore_for_file: cascade_invocations
|
||||
|
||||
/// {@template desktopReactionsBuilder}
|
||||
/// Builds a list of reactions to a message on desktop & web.
|
||||
///
|
||||
/// Not intended for use outside of [MessageWidgetContent].
|
||||
/// {@endtemplate}
|
||||
class DesktopReactionsBuilder extends StatefulWidget {
|
||||
/// {@macro desktopReactionsBuilder}
|
||||
const DesktopReactionsBuilder({
|
||||
super.key,
|
||||
required this.shouldShowReactions,
|
||||
required this.message,
|
||||
required this.messageTheme,
|
||||
this.borderSide,
|
||||
required this.reverse,
|
||||
});
|
||||
|
||||
/// Whether reactions should be shown.
|
||||
final bool shouldShowReactions;
|
||||
|
||||
/// The message to show reactions for.
|
||||
final Message message;
|
||||
|
||||
/// The theme to use for the reactions.
|
||||
///
|
||||
/// [StreamMessageThemeData] is used because the design spec for desktop
|
||||
/// reactions matches the design spec for messages.
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// {@macro borderSide}
|
||||
final BorderSide? borderSide;
|
||||
|
||||
/// {@macro reverse}
|
||||
final bool reverse;
|
||||
|
||||
@override
|
||||
State<DesktopReactionsBuilder> createState() =>
|
||||
_DesktopReactionsBuilderState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(
|
||||
DiagnosticsProperty<bool>(
|
||||
'shouldShowReactions',
|
||||
shouldShowReactions,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<Message>('message', message),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<StreamMessageThemeData>(
|
||||
'messageTheme',
|
||||
messageTheme,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<BorderSide?>('borderSide', borderSide),
|
||||
);
|
||||
properties.add(DiagnosticsProperty<bool>('reverse', reverse));
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopReactionsBuilderState extends State<DesktopReactionsBuilder> {
|
||||
bool _showReactionsPopup = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final streamChat = StreamChat.of(context);
|
||||
final reactionIcons = StreamChatConfiguration.of(context).reactionIcons;
|
||||
final streamChatTheme = StreamChatTheme.of(context);
|
||||
|
||||
final reactionsMap = <String, Reaction>{};
|
||||
var reactionsList = <Reaction>[];
|
||||
if (widget.shouldShowReactions) {
|
||||
widget.message.latestReactions?.forEach((element) {
|
||||
if (!reactionsMap.containsKey(element.type) ||
|
||||
element.user!.id == streamChat.currentUser?.id) {
|
||||
reactionsMap[element.type] = element;
|
||||
}
|
||||
});
|
||||
|
||||
reactionsList = reactionsMap.values.toList()
|
||||
..sort((a, b) => a.user!.id == streamChat.currentUser?.id ? 1 : -1);
|
||||
}
|
||||
|
||||
return PortalTarget(
|
||||
visible: _showReactionsPopup,
|
||||
portalCandidateLabels: const [kPortalMessageListViewLabel],
|
||||
anchor: Aligned(
|
||||
target: widget.reverse ? Alignment.topRight : Alignment.topLeft,
|
||||
follower: widget.reverse ? Alignment.bottomRight : Alignment.bottomLeft,
|
||||
shiftToWithinBound: const AxisFlag(
|
||||
y: true,
|
||||
),
|
||||
),
|
||||
portalFollower: MouseRegion(
|
||||
onEnter: (event) async {
|
||||
setState(() => _showReactionsPopup = !_showReactionsPopup);
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() => _showReactionsPopup = !_showReactionsPopup);
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 336,
|
||||
maxHeight: 342,
|
||||
),
|
||||
child: Card(
|
||||
color: streamChatTheme.colorTheme.barsBg,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
'''${widget.message.latestReactions!.length} ${context.translations.messageReactionsLabel}''',
|
||||
style: streamChatTheme.textTheme.headlineBold,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
...widget.message.latestReactions!.map((reaction) {
|
||||
final reactionIcon = reactionIcons.firstWhereOrNull(
|
||||
(r) => r.type == reaction.type,
|
||||
);
|
||||
return _StackedReaction(
|
||||
reaction: reaction,
|
||||
streamChatTheme: streamChatTheme,
|
||||
reactionIcon: reactionIcon,
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (event) async {
|
||||
setState(() => _showReactionsPopup = !_showReactionsPopup);
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() => _showReactionsPopup = !_showReactionsPopup);
|
||||
},
|
||||
child: Wrap(
|
||||
children: [
|
||||
...reactionsList.map((reaction) {
|
||||
final reactionIcon = reactionIcons.firstWhereOrNull(
|
||||
(r) => r.type == reaction.type,
|
||||
);
|
||||
|
||||
return _BottomReaction(
|
||||
reaction: reaction,
|
||||
message: widget.message,
|
||||
borderSide: widget.borderSide,
|
||||
messageTheme: widget.messageTheme,
|
||||
reactionIcon: reactionIcon,
|
||||
streamChatTheme: streamChatTheme,
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomReaction extends StatelessWidget {
|
||||
const _BottomReaction({
|
||||
required this.reaction,
|
||||
required this.message,
|
||||
required this.borderSide,
|
||||
required this.messageTheme,
|
||||
required this.reactionIcon,
|
||||
required this.streamChatTheme,
|
||||
});
|
||||
|
||||
final Reaction reaction;
|
||||
final Message message;
|
||||
final BorderSide? borderSide;
|
||||
final StreamMessageThemeData? messageTheme;
|
||||
final StreamReactionIcon? reactionIcon;
|
||||
final StreamChatThemeData streamChatTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userId = StreamChat.of(context).currentUser?.id;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (reaction.userId == userId) {
|
||||
StreamChannel.of(context).channel.deleteReaction(
|
||||
message,
|
||||
reaction,
|
||||
);
|
||||
} else if (reactionIcon != null) {
|
||||
StreamChannel.of(context).channel.sendReaction(
|
||||
message,
|
||||
reactionIcon!.type,
|
||||
score: reaction.score + 1,
|
||||
enforceUnique:
|
||||
StreamChatConfiguration.of(context).enforceUniqueReactions,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Card(
|
||||
shape: StadiumBorder(
|
||||
side: borderSide ??
|
||||
BorderSide(
|
||||
color: messageTheme?.messageBorderColor ?? Colors.grey,
|
||||
),
|
||||
),
|
||||
color: messageTheme?.messageBackgroundColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints.tight(
|
||||
const Size.square(16),
|
||||
),
|
||||
child: reactionIcon?.builder(
|
||||
context,
|
||||
reaction.user?.id == userId,
|
||||
16,
|
||||
) ??
|
||||
Icon(
|
||||
Icons.help_outline_rounded,
|
||||
size: 16,
|
||||
color: reaction.user?.id == userId
|
||||
? streamChatTheme.colorTheme.accentPrimary
|
||||
: streamChatTheme.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${reaction.score}',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<Reaction>('reaction', reaction));
|
||||
properties.add(DiagnosticsProperty<Message>('message', message));
|
||||
}
|
||||
}
|
||||
|
||||
class _StackedReaction extends StatelessWidget {
|
||||
const _StackedReaction({
|
||||
required this.reaction,
|
||||
required this.streamChatTheme,
|
||||
required this.reactionIcon,
|
||||
});
|
||||
|
||||
final Reaction reaction;
|
||||
final StreamChatThemeData streamChatTheme;
|
||||
final StreamReactionIcon? reactionIcon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final userId = StreamChat.of(context).currentUser?.id;
|
||||
return SizedBox(
|
||||
width: 80,
|
||||
child: Column(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
StreamUserAvatar(
|
||||
user: reaction.user!,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: streamChatTheme.colorTheme.inputBg,
|
||||
border: Border.all(
|
||||
color: streamChatTheme.colorTheme.barsBg,
|
||||
width: 2,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: reactionIcon?.builder(
|
||||
context,
|
||||
reaction.userId == userId,
|
||||
16,
|
||||
) ??
|
||||
Icon(
|
||||
Icons.help_outline_rounded,
|
||||
size: 16,
|
||||
color: reaction.user?.id == userId
|
||||
? streamChatTheme.colorTheme.accentPrimary
|
||||
: streamChatTheme.colorTheme.textHighEmphasis
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
userId == reaction.user!.name ? 'You' : reaction.user!.name,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(
|
||||
DiagnosticsProperty<Reaction>('reaction', reaction),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<StreamReactionIcon?>(
|
||||
'reactionIcon',
|
||||
reactionIcon,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template streamMessageReactionsModal}
|
||||
/// Modal widget for displaying message reactions
|
||||
/// {@endtemplate}
|
||||
class StreamMessageReactionsModal extends StatelessWidget {
|
||||
/// {@macro streamMessageReactionsModal}
|
||||
const StreamMessageReactionsModal({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.messageWidget,
|
||||
required this.messageTheme,
|
||||
this.showReactions,
|
||||
this.reverse = false,
|
||||
this.onUserAvatarTap,
|
||||
});
|
||||
|
||||
/// Widget that shows the message
|
||||
final Widget messageWidget;
|
||||
|
||||
/// Message to display reactions of
|
||||
final Message message;
|
||||
|
||||
/// [StreamMessageThemeData] to apply to [message]
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// {@macro reverse}
|
||||
final bool reverse;
|
||||
|
||||
/// {@macro showReactions}
|
||||
final bool? showReactions;
|
||||
|
||||
/// {@macro onUserAvatarTap}
|
||||
final void Function(User)? onUserAvatarTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final user = StreamChat.of(context).currentUser;
|
||||
final _userPermissions = StreamChannel.of(context).channel.ownCapabilities;
|
||||
|
||||
final hasReactionPermission =
|
||||
_userPermissions.contains(PermissionType.sendReaction);
|
||||
|
||||
final roughMaxSize = size.width * 2 / 3;
|
||||
var messageTextLength = message.text!.length;
|
||||
if (message.quotedMessage != null) {
|
||||
var quotedMessageLength = message.quotedMessage!.text!.length + 40;
|
||||
if (message.quotedMessage!.attachments.isNotEmpty) {
|
||||
quotedMessageLength += 40;
|
||||
}
|
||||
if (quotedMessageLength > messageTextLength) {
|
||||
messageTextLength = quotedMessageLength;
|
||||
}
|
||||
}
|
||||
final roughSentenceSize = messageTextLength *
|
||||
(messageTheme.messageTextStyle?.fontSize ?? 1) *
|
||||
1.2;
|
||||
final divFactor = message.attachments.isNotEmpty
|
||||
? 1
|
||||
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
||||
|
||||
final numberOfReactions =
|
||||
StreamChatConfiguration.of(context).reactionIcons.length;
|
||||
final shiftFactor =
|
||||
numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0;
|
||||
|
||||
final child = Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
if ((showReactions ?? hasReactionPermission) &&
|
||||
(message.status == MessageSendingStatus.sent))
|
||||
Align(
|
||||
alignment: Alignment(
|
||||
user!.id == message.user!.id
|
||||
? (divFactor >= 1.0
|
||||
? -0.2 - shiftFactor
|
||||
: (1.2 - divFactor))
|
||||
: (divFactor >= 1.0
|
||||
? shiftFactor + 0.2
|
||||
: -(1.2 - divFactor)),
|
||||
0,
|
||||
),
|
||||
child: StreamReactionPicker(
|
||||
message: message,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
IgnorePointer(
|
||||
child: messageWidget,
|
||||
),
|
||||
if (message.latestReactions?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildReactionCard(
|
||||
context,
|
||||
user,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => Navigator.of(context).maybePop(),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 10,
|
||||
sigmaY: 10,
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.overlay,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOutBack,
|
||||
builder: (context, val, widget) => Transform.scale(
|
||||
scale: val,
|
||||
child: widget,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReactionCard(BuildContext context, User? user) {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
return Card(
|
||||
color: chatThemeData.colorTheme.barsBg,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.translations.messageReactionsLabel,
|
||||
style: chatThemeData.textTheme.headlineBold,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: message.latestReactions!
|
||||
.map((e) => _buildReaction(
|
||||
e,
|
||||
user!,
|
||||
context,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReaction(
|
||||
Reaction reaction,
|
||||
User currentUser,
|
||||
BuildContext context,
|
||||
) {
|
||||
final isCurrentUser = reaction.user?.id == currentUser.id;
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(
|
||||
const Size(64, 100),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
StreamUserAvatar(
|
||||
onTap: onUserAvatarTap,
|
||||
user: reaction.user!,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
onlineIndicatorConstraints: const BoxConstraints.tightFor(
|
||||
height: 12,
|
||||
width: 12,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 6,
|
||||
left: isCurrentUser ? -3 : null,
|
||||
right: isCurrentUser ? -3 : null,
|
||||
child: Align(
|
||||
alignment:
|
||||
reverse ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: StreamReactionBubble(
|
||||
reactions: [reaction],
|
||||
flipTail: !reverse,
|
||||
borderColor:
|
||||
messageTheme.reactionsBorderColor ?? Colors.transparent,
|
||||
backgroundColor: messageTheme.reactionsBackgroundColor ??
|
||||
Colors.transparent,
|
||||
maskColor: chatThemeData.colorTheme.barsBg,
|
||||
tailCirclesSpacing: 1,
|
||||
highlightOwnReactions: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
reaction.user!.name.split(' ')[0],
|
||||
style: chatThemeData.textTheme.footnoteBold,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template streamReactionBubble}
|
||||
/// Creates a reaction bubble that displays over messages.
|
||||
/// {@endtemplate}
|
||||
class StreamReactionBubble extends StatelessWidget {
|
||||
/// {@macro streamReactionBubble}
|
||||
const StreamReactionBubble({
|
||||
super.key,
|
||||
required this.reactions,
|
||||
required this.borderColor,
|
||||
required this.backgroundColor,
|
||||
required this.maskColor,
|
||||
this.reverse = false,
|
||||
this.flipTail = false,
|
||||
this.highlightOwnReactions = true,
|
||||
this.tailCirclesSpacing = 0,
|
||||
});
|
||||
|
||||
/// Reactions to show
|
||||
final List<Reaction> reactions;
|
||||
|
||||
/// Border color of bubble
|
||||
final Color borderColor;
|
||||
|
||||
/// Background color of bubble
|
||||
final Color backgroundColor;
|
||||
|
||||
/// Mask color
|
||||
final Color maskColor;
|
||||
|
||||
/// Reverse for other side
|
||||
final bool reverse;
|
||||
|
||||
/// Reverse tail for other side
|
||||
final bool flipTail;
|
||||
|
||||
/// Flag for highlighting own reactions
|
||||
final bool highlightOwnReactions;
|
||||
|
||||
/// Spacing for tail circles
|
||||
final double tailCirclesSpacing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reactionIcons = StreamChatConfiguration.of(context).reactionIcons;
|
||||
final totalReactions = reactions.length;
|
||||
final offset =
|
||||
totalReactions > 1 ? 16.0.mirrorConditionally(flipTail) : 2.0;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: Offset(-offset, 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: maskColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: totalReactions > 1 ? 4.0 : 0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: borderColor,
|
||||
),
|
||||
color: backgroundColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) => Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (constraints.maxWidth < double.infinity)
|
||||
...reactions
|
||||
.take((constraints.maxWidth) ~/ 24)
|
||||
.map((reaction) => _buildReaction(
|
||||
reactionIcons,
|
||||
reaction,
|
||||
context,
|
||||
))
|
||||
.toList(),
|
||||
if (constraints.maxWidth == double.infinity)
|
||||
...reactions
|
||||
.map((reaction) => _buildReaction(
|
||||
reactionIcons,
|
||||
reaction,
|
||||
context,
|
||||
))
|
||||
.toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 2,
|
||||
left: reverse ? null : 13,
|
||||
right: reverse ? 13 : null,
|
||||
child: _buildReactionsTail(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReaction(
|
||||
List<StreamReactionIcon> reactionIcons,
|
||||
Reaction reaction,
|
||||
BuildContext context,
|
||||
) {
|
||||
final reactionIcon = reactionIcons.firstWhereOrNull(
|
||||
(r) => r.type == reaction.type,
|
||||
);
|
||||
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
final userId = StreamChat.of(context).currentUser?.id;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
),
|
||||
child: reactionIcon != null
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints.tight(const Size.square(16)),
|
||||
child: reactionIcon.builder(
|
||||
context,
|
||||
!highlightOwnReactions || reaction.user?.id == userId,
|
||||
16,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.help_outline_rounded,
|
||||
size: 16,
|
||||
color: (!highlightOwnReactions || reaction.user?.id == userId)
|
||||
? chatThemeData.colorTheme.accentPrimary
|
||||
: chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.5),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReactionsTail(BuildContext context) {
|
||||
final tail = CustomPaint(
|
||||
painter: ReactionBubblePainter(
|
||||
backgroundColor,
|
||||
borderColor,
|
||||
maskColor,
|
||||
tailCirclesSpace: tailCirclesSpacing,
|
||||
flipTail: !flipTail,
|
||||
numberOfReactions: reactions.length,
|
||||
),
|
||||
);
|
||||
return tail;
|
||||
}
|
||||
}
|
||||
|
||||
/// Painter widget for a reaction bubble
|
||||
class ReactionBubblePainter extends CustomPainter {
|
||||
/// Constructor for creating a [ReactionBubblePainter]
|
||||
ReactionBubblePainter(
|
||||
this.color,
|
||||
this.borderColor,
|
||||
this.maskColor, {
|
||||
this.tailCirclesSpace = 0,
|
||||
this.flipTail = false,
|
||||
this.numberOfReactions = 0,
|
||||
});
|
||||
|
||||
/// Color of bubble
|
||||
final Color color;
|
||||
|
||||
/// Border color of bubble
|
||||
final Color borderColor;
|
||||
|
||||
/// Mask color
|
||||
final Color maskColor;
|
||||
|
||||
/// Tail circle space
|
||||
final double tailCirclesSpace;
|
||||
|
||||
/// Flip tail
|
||||
final bool flipTail;
|
||||
|
||||
/// Number of reactions on the page
|
||||
final int numberOfReactions;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
_drawOvalMask(size, canvas);
|
||||
|
||||
_drawMask(size, canvas);
|
||||
|
||||
_drawOval(size, canvas);
|
||||
|
||||
_drawOvalBorder(size, canvas);
|
||||
|
||||
_drawArc(size, canvas);
|
||||
|
||||
_drawBorder(size, canvas);
|
||||
}
|
||||
|
||||
void _drawOvalMask(Size size, Canvas canvas) {
|
||||
final paint = Paint()
|
||||
..color = maskColor
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final path = Path()
|
||||
..addOval(
|
||||
Rect.fromCircle(
|
||||
center: const Offset(4, 3).mirrorConditionally(flipTail) +
|
||||
Offset(tailCirclesSpace, tailCirclesSpace)
|
||||
.mirrorConditionally(flipTail),
|
||||
radius: 4,
|
||||
),
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void _drawOvalBorder(Size size, Canvas canvas) {
|
||||
final paint = Paint()
|
||||
..color = borderColor
|
||||
..strokeWidth = 1
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final path = Path()
|
||||
..addOval(
|
||||
Rect.fromCircle(
|
||||
center: const Offset(4, 3).mirrorConditionally(flipTail) +
|
||||
Offset(tailCirclesSpace, tailCirclesSpace)
|
||||
.mirrorConditionally(flipTail),
|
||||
radius: 2,
|
||||
),
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void _drawOval(Size size, Canvas canvas) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 1;
|
||||
|
||||
final path = Path()
|
||||
..addOval(Rect.fromCircle(
|
||||
center: const Offset(4, 3).mirrorConditionally(flipTail) +
|
||||
Offset(tailCirclesSpace, tailCirclesSpace)
|
||||
.mirrorConditionally(flipTail),
|
||||
radius: 2,
|
||||
));
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void _drawBorder(Size size, Canvas canvas) {
|
||||
final paint = Paint()
|
||||
..color = borderColor
|
||||
..strokeWidth = 1
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
const dy = -2.2;
|
||||
final startAngle = flipTail ? -0.1 : 1.1;
|
||||
final sweepAngle = flipTail ? -1.2 : (numberOfReactions > 1 ? 1.2 : 0.9);
|
||||
final path = Path()
|
||||
..addArc(
|
||||
Rect.fromCircle(
|
||||
center: const Offset(1, dy).mirrorConditionally(flipTail),
|
||||
radius: 4,
|
||||
),
|
||||
-pi * startAngle,
|
||||
-pi / sweepAngle,
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void _drawArc(Size size, Canvas canvas) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 1;
|
||||
|
||||
const dy = -2.2;
|
||||
final startAngle = flipTail ? -0.0 : 1.0;
|
||||
final sweepAngle = flipTail ? -1.3 : 1.3;
|
||||
final path = Path()
|
||||
..addArc(
|
||||
Rect.fromCircle(
|
||||
center: const Offset(1, dy).mirrorConditionally(flipTail),
|
||||
radius: 4,
|
||||
),
|
||||
-pi * startAngle,
|
||||
-pi * sweepAngle,
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void _drawMask(Size size, Canvas canvas) {
|
||||
final paint = Paint()
|
||||
..color = maskColor
|
||||
..strokeWidth = 1
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
const dy = -2.2;
|
||||
final startAngle = flipTail ? -0.1 : 1.1;
|
||||
final sweepAngle = flipTail ? -1.2 : 1.2;
|
||||
final path = Path()
|
||||
..addArc(
|
||||
Rect.fromCircle(
|
||||
center: const Offset(1, dy).mirrorConditionally(flipTail),
|
||||
radius: 6,
|
||||
),
|
||||
-pi * startAngle,
|
||||
-pi / sweepAngle,
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) => true;
|
||||
}
|
||||
|
||||
/// Extension on [Offset]
|
||||
extension YTransformer on Offset {
|
||||
/// Flips x coordinate when flip is true
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
Offset mirrorConditionally(bool flip) => Offset(flip ? -dx : dx, dy);
|
||||
}
|
||||
|
||||
/// Extension on [Offset]
|
||||
extension IntTransformer on double {
|
||||
/// Flips x coordinate when flip is true
|
||||
// ignore: avoid_positional_boolean_parameters
|
||||
double mirrorConditionally(bool flip) => flip ? -this : this;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget/reactions/reaction_bubble.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template reactionIndicator}
|
||||
/// Indicates the reaction a [StreamMessageWidget] has.
|
||||
///
|
||||
/// Used in [MessageWidgetContent].
|
||||
/// {@endtemplate}
|
||||
class ReactionIndicator extends StatelessWidget {
|
||||
/// {@macro reactionIndicator}
|
||||
const ReactionIndicator({
|
||||
super.key,
|
||||
required this.ownId,
|
||||
required this.message,
|
||||
required this.shouldShowReactions,
|
||||
required this.onTap,
|
||||
required this.reverse,
|
||||
required this.messageTheme,
|
||||
});
|
||||
|
||||
/// The id of the current user.
|
||||
final String ownId;
|
||||
|
||||
/// {@macro message}
|
||||
final Message message;
|
||||
|
||||
/// {@macro shouldShowReactions}
|
||||
final bool shouldShowReactions;
|
||||
|
||||
/// The callback to perform when the widget is tapped or clicked.
|
||||
final VoidCallback onTap;
|
||||
|
||||
/// {@macro reverse}
|
||||
final bool reverse;
|
||||
|
||||
/// {@macro messageTheme}
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reactionsMap = <String, Reaction>{};
|
||||
message.latestReactions?.forEach((element) {
|
||||
if (!reactionsMap.containsKey(element.type) ||
|
||||
element.user!.id == ownId) {
|
||||
reactionsMap[element.type] = element;
|
||||
}
|
||||
});
|
||||
final reactionsList = reactionsMap.values.toList()
|
||||
..sort((a, b) => a.user!.id == ownId ? 1 : -1);
|
||||
|
||||
return Transform(
|
||||
transform: Matrix4.translationValues(
|
||||
reverse ? 12 : -12,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 22 * 6.0,
|
||||
),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: shouldShowReactions
|
||||
? GestureDetector(
|
||||
onTap: onTap,
|
||||
child: StreamReactionBubble(
|
||||
key: ValueKey('${message.id}.reactions'),
|
||||
reverse: reverse,
|
||||
flipTail: reverse,
|
||||
backgroundColor: messageTheme.reactionsBackgroundColor ??
|
||||
Colors.transparent,
|
||||
borderColor:
|
||||
messageTheme.reactionsBorderColor ?? Colors.transparent,
|
||||
maskColor:
|
||||
messageTheme.reactionsMaskColor ?? Colors.transparent,
|
||||
reactions: reactionsList,
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import 'package:ezanimation/ezanimation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template streamReactionPicker}
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// Allows the user to select reactions to a message on mobile.
|
||||
///
|
||||
/// It is not recommended to use this widget directly as it's one of the
|
||||
/// default widgets used by [StreamMessageWidget.onMessageActions].
|
||||
/// {@endtemplate}
|
||||
class StreamReactionPicker extends StatefulWidget {
|
||||
/// {@macro streamReactionPicker}
|
||||
const StreamReactionPicker({
|
||||
super.key,
|
||||
required this.message,
|
||||
});
|
||||
|
||||
/// Message to attach the reaction to
|
||||
final Message message;
|
||||
|
||||
@override
|
||||
_StreamReactionPickerState createState() => _StreamReactionPickerState();
|
||||
}
|
||||
|
||||
class _StreamReactionPickerState extends State<StreamReactionPicker>
|
||||
with TickerProviderStateMixin {
|
||||
List<EzAnimation> animations = [];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
final reactionIcons = StreamChatConfiguration.of(context).reactionIcons;
|
||||
|
||||
if (animations.isEmpty && reactionIcons.isNotEmpty) {
|
||||
reactionIcons.forEach((element) {
|
||||
animations.add(
|
||||
EzAnimation.tween(
|
||||
Tween(begin: 0.0, end: 1.0),
|
||||
const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOutBack,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
triggerAnimations();
|
||||
}
|
||||
|
||||
final child = Material(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: chatThemeData.colorTheme.barsBg,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: reactionIcons
|
||||
.map<Widget>((reactionIcon) {
|
||||
final ownReactionIndex =
|
||||
widget.message.ownReactions?.indexWhere(
|
||||
(reaction) => reaction.type == reactionIcon.type,
|
||||
) ??
|
||||
-1;
|
||||
final index = reactionIcons.indexOf(reactionIcon);
|
||||
|
||||
final child = reactionIcon.builder(
|
||||
context,
|
||||
ownReactionIndex != -1,
|
||||
24,
|
||||
);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
child: RawMaterialButton(
|
||||
elevation: 0,
|
||||
shape: ContinuousRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
onPressed: () {
|
||||
if (ownReactionIndex != -1) {
|
||||
removeReaction(
|
||||
context,
|
||||
widget.message.ownReactions![ownReactionIndex],
|
||||
);
|
||||
} else {
|
||||
sendReaction(
|
||||
context,
|
||||
reactionIcon.type,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: AnimatedBuilder(
|
||||
animation: animations[index],
|
||||
builder: (context, child) => Transform.scale(
|
||||
scale: animations[index].value,
|
||||
child: child,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
.insertBetween(
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1),
|
||||
curve: Curves.easeInOutBack,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
builder: (context, val, widget) => Transform.scale(
|
||||
scale: val,
|
||||
child: widget,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> triggerAnimations() async {
|
||||
for (final a in animations) {
|
||||
a.start();
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pop() async {
|
||||
for (final a in animations) {
|
||||
a.stop();
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
/// Add a reaction to the message
|
||||
void sendReaction(BuildContext context, String reactionType) {
|
||||
StreamChannel.of(context).channel.sendReaction(
|
||||
widget.message,
|
||||
reactionType,
|
||||
enforceUnique:
|
||||
StreamChatConfiguration.of(context).enforceUniqueReactions,
|
||||
);
|
||||
pop();
|
||||
}
|
||||
|
||||
/// Remove a reaction from the message
|
||||
void removeReaction(BuildContext context, Reaction reaction) {
|
||||
StreamChannel.of(context).channel.deleteReaction(widget.message, reaction);
|
||||
pop();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final a in animations) {
|
||||
a.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/utils/extensions.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template sendingIndicatorWrapper}
|
||||
/// Helper widget for building a [StreamSendingIndicator].
|
||||
///
|
||||
/// Used in [BottomRow]. Should not be used elsewhere.
|
||||
/// {@endtemplate}
|
||||
class SendingIndicatorWrapper extends StatelessWidget {
|
||||
/// {@macro sendingIndicatorWrapper}
|
||||
const SendingIndicatorWrapper({
|
||||
super.key,
|
||||
required this.messageTheme,
|
||||
required this.message,
|
||||
required this.hasNonUrlAttachments,
|
||||
required this.streamChat,
|
||||
required this.streamChatTheme,
|
||||
});
|
||||
|
||||
/// {@macro messageTheme}
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// {@macro message}
|
||||
final Message message;
|
||||
|
||||
/// {@macro hasNonUrlAttachments}
|
||||
final bool hasNonUrlAttachments;
|
||||
|
||||
/// {@macro streamChat}
|
||||
final StreamChatState streamChat;
|
||||
|
||||
/// {@macro streamChatThemeData}
|
||||
final StreamChatThemeData streamChatTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = messageTheme.createdAtStyle;
|
||||
final memberCount = StreamChannel.of(context).channel.memberCount ?? 0;
|
||||
|
||||
if (hasNonUrlAttachments &&
|
||||
(message.status == MessageSendingStatus.sending ||
|
||||
message.status == MessageSendingStatus.updating)) {
|
||||
final totalAttachments = message.attachments.length;
|
||||
final uploadRemaining =
|
||||
message.attachments.where((it) => !it.uploadState.isSuccess).length;
|
||||
if (uploadRemaining == 0) {
|
||||
return StreamSvgIcon.check(
|
||||
size: style!.fontSize,
|
||||
color: IconTheme.of(context).color!.withOpacity(0.5),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
context.translations.attachmentsUploadProgressText(
|
||||
remaining: uploadRemaining,
|
||||
total: totalAttachments,
|
||||
),
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
return BetterStreamBuilder<List<Read>>(
|
||||
stream: channel.state?.readStream,
|
||||
initialData: channel.state?.read,
|
||||
builder: (context, data) {
|
||||
final readList = data.where((it) =>
|
||||
it.user.id != streamChat.currentUser?.id &&
|
||||
(it.lastRead.isAfter(message.createdAt) ||
|
||||
it.lastRead.isAtSameMomentAs(message.createdAt)));
|
||||
final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||
Widget child = StreamSendingIndicator(
|
||||
message: message,
|
||||
isMessageRead: isMessageRead,
|
||||
size: style!.fontSize,
|
||||
);
|
||||
if (isMessageRead) {
|
||||
child = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (memberCount > 2)
|
||||
Text(
|
||||
readList.length.toString(),
|
||||
style: style.copyWith(
|
||||
color: streamChatTheme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget/message_widget_content_components.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template textBubble}
|
||||
/// The bubble around a [StreamMessageText].
|
||||
///
|
||||
/// Used in [MessageCard]. Should not be used elsewhere.
|
||||
/// {@endtemplate}
|
||||
class TextBubble extends StatelessWidget {
|
||||
/// {@macro textBubble}
|
||||
const TextBubble({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.isOnlyEmoji,
|
||||
required this.textPadding,
|
||||
required this.messageTheme,
|
||||
required this.hasUrlAttachments,
|
||||
required this.hasQuotedMessage,
|
||||
this.textBuilder,
|
||||
this.onLinkTap,
|
||||
this.onMentionTap,
|
||||
});
|
||||
|
||||
/// {@macro message}
|
||||
final Message message;
|
||||
|
||||
/// {@macro isOnlyEmoji}
|
||||
final bool isOnlyEmoji;
|
||||
|
||||
/// {@macro textPadding}
|
||||
final EdgeInsets textPadding;
|
||||
|
||||
/// {@macro textBuilder}
|
||||
final Widget Function(BuildContext, Message)? textBuilder;
|
||||
|
||||
/// {@macro onLinkTap}
|
||||
final void Function(String)? onLinkTap;
|
||||
|
||||
/// {@macro onMentionTap}
|
||||
final void Function(User)? onMentionTap;
|
||||
|
||||
/// {@macro messageTheme}
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// {@macro hasUrlAttachments}
|
||||
final bool hasUrlAttachments;
|
||||
|
||||
/// {@macro hasQuotedMessage}
|
||||
final bool hasQuotedMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (message.text?.trim().isEmpty ?? false) return const Offstage();
|
||||
return Padding(
|
||||
padding: isOnlyEmoji ? EdgeInsets.zero : textPadding,
|
||||
child: textBuilder != null
|
||||
? textBuilder!(context, message)
|
||||
: StreamMessageText(
|
||||
onLinkTap: onLinkTap,
|
||||
message: message,
|
||||
onMentionTap: onMentionTap,
|
||||
messageTheme: isOnlyEmoji
|
||||
? messageTheme.copyWith(
|
||||
messageTextStyle: messageTheme.messageTextStyle!.copyWith(
|
||||
fontSize: 42,
|
||||
),
|
||||
)
|
||||
: messageTheme,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template threadReplyPainter}
|
||||
/// A custom painter used to render thread replies.
|
||||
///
|
||||
/// Used in [BottomRow].
|
||||
/// {@endtemplate}
|
||||
class ThreadReplyPainter extends CustomPainter {
|
||||
/// {@macro threadReplyPainter}
|
||||
const ThreadReplyPainter({
|
||||
this.context,
|
||||
required this.color,
|
||||
this.reverse = false,
|
||||
});
|
||||
|
||||
/// The color to paint the thread reply with.
|
||||
final Color? color;
|
||||
|
||||
/// The [BuildContext] to use to retrieve the [StreamChatTheme].
|
||||
final BuildContext? context;
|
||||
|
||||
/// {@macro reverse}
|
||||
final bool reverse;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color ?? StreamChatTheme.of(context!).colorTheme.disabled
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
final path = Path()
|
||||
..moveTo(reverse ? size.width : 0, 0)
|
||||
..quadraticBezierTo(
|
||||
reverse ? size.width : 0,
|
||||
size.height * 0.38,
|
||||
reverse ? size.width : 0,
|
||||
size.height * 0.5,
|
||||
)
|
||||
..quadraticBezierTo(
|
||||
reverse ? size.width : 0,
|
||||
size.height,
|
||||
reverse ? 0 : size.width,
|
||||
size.height,
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template threadParticipants}
|
||||
/// Shows the users participating in a thread.
|
||||
///
|
||||
/// Used in [BottomRow].
|
||||
/// {@endtemplate}
|
||||
class ThreadParticipants extends StatelessWidget {
|
||||
/// {@macro threadParticipants}
|
||||
const ThreadParticipants({
|
||||
super.key,
|
||||
required StreamChatThemeData streamChatTheme,
|
||||
required this.threadParticipants,
|
||||
}) : _streamChatTheme = streamChatTheme;
|
||||
|
||||
/// {@macro streamChatThemeData}
|
||||
final StreamChatThemeData _streamChatTheme;
|
||||
|
||||
/// The users participating in the thread.
|
||||
final Iterable<User> threadParticipants;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var padding = 0.0;
|
||||
return Stack(
|
||||
children: threadParticipants.map((user) {
|
||||
padding += 8.0;
|
||||
return Positioned(
|
||||
right: padding - 8,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
),
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: StreamUserAvatar(
|
||||
user: user,
|
||||
constraints: BoxConstraints.tight(const Size.fromRadius(7)),
|
||||
showOnlineStatus: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template userAvatarTransform}
|
||||
/// Transforms a [StreamUserAvatar] according to the specified translation.
|
||||
///
|
||||
/// Used in [MessageWidgetContent].
|
||||
/// {@endtemplate}
|
||||
class UserAvatarTransform extends StatelessWidget {
|
||||
/// {@macro userAvatarTransform}
|
||||
const UserAvatarTransform({
|
||||
super.key,
|
||||
required this.translateUserAvatar,
|
||||
required this.messageTheme,
|
||||
required this.message,
|
||||
this.userAvatarBuilder,
|
||||
this.onUserAvatarTap,
|
||||
});
|
||||
|
||||
/// {@macro translateUserAvatar}
|
||||
final bool translateUserAvatar;
|
||||
|
||||
/// {@macro messageTheme}
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// {@macro userAvatarBuilder}
|
||||
final Widget Function(BuildContext, User)? userAvatarBuilder;
|
||||
|
||||
/// {@macro message}
|
||||
final Message message;
|
||||
|
||||
/// {@macro onUserAvatarTap}
|
||||
final void Function(User)? onUserAvatarTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Transform.translate(
|
||||
offset: Offset(
|
||||
0,
|
||||
translateUserAvatar
|
||||
? (messageTheme.avatarTheme?.constraints.maxHeight ?? 40) / 2
|
||||
: 0,
|
||||
),
|
||||
child: userAvatarBuilder?.call(context, message.user!) ??
|
||||
StreamUserAvatar(
|
||||
user: message.user!,
|
||||
onTap: onUserAvatarTap,
|
||||
constraints: messageTheme.avatarTheme!.constraints,
|
||||
borderRadius: messageTheme.avatarTheme!.borderRadius,
|
||||
showOnlineStatus: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template username}
|
||||
/// Displays the username of a particular message's sender.
|
||||
/// {@endtemplate}
|
||||
class Username extends StatelessWidget {
|
||||
/// {@macro username}
|
||||
const Username({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.messageTheme,
|
||||
});
|
||||
|
||||
/// {@macro message}
|
||||
final Message message;
|
||||
|
||||
/// {@macro messageTheme}
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
message.user?.name ?? '',
|
||||
maxLines: 1,
|
||||
key: key,
|
||||
style: messageTheme.messageAuthorStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user