* 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:
Salvatore Giordano
2022-09-30 17:55:57 +02:00
committed by GitHub
co-authored by Sahil Kumar GroovinChip Gordon Hayes Sacha Arbonel
parent 3e1d6e91c7
commit 4f2542291c
542 changed files with 28064 additions and 136563 deletions
@@ -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,
),
);
}
}
@@ -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}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker_paint.png)
///
/// 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();
}
}