* 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
@@ -1,5 +1,7 @@
export 'attachment_error.dart';
export 'attachment_error.dart';
export 'attachment_upload_state_builder.dart';
export 'attachment_widget.dart' show AttachmentError, AttachmentSource;
export 'attachment_widget.dart' show AttachmentSource;
export 'file_attachment.dart';
export 'giphy_attachment.dart';
export 'image_attachment.dart';
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template attachmentError}
/// Widget for building in case of error
/// {@endtemplate}
class AttachmentError extends StatelessWidget {
/// {@macro attachmentError}
const AttachmentError({
super.key,
this.constraints,
});
/// constraints of error
final BoxConstraints? constraints;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
constraints: constraints ?? const BoxConstraints.expand(),
color:
StreamChatTheme.of(context).colorTheme.accentError.withOpacity(0.1),
child: Center(
child: Icon(
Icons.error_outline,
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
),
),
),
);
}
}
@@ -1,15 +1,11 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro attachment_title}
@Deprecated("Use 'StreamAttachmentTitle' instead")
typedef AttachmentTitle = StreamAttachmentTitle;
/// {@template attachment_title}
/// {@template attachmentTitle}
/// Title for attachments
/// {@endtemplate}
class StreamAttachmentTitle extends StatelessWidget {
/// Supply attachment and theme for constructing title
/// {@macro attachmentTitle}
const StreamAttachmentTitle({
super.key,
required this.attachment,
@@ -1,50 +1,39 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Widget to build in progress
typedef InProgressBuilder = Widget Function(BuildContext, int, int);
/// Widget to build on failure
typedef FailedBuilder = Widget Function(BuildContext, String);
/// {@macro attachment_upload_state_builder}
@Deprecated("Use 'StreamAttachmentsUploadStateBuilder' instead")
typedef AttachmentUploadStateBuilder = StreamAttachmentUploadStateBuilder;
/// {@template attachment_upload_state_builder}
/// {@template streamAttachmentUploadStateBuilder}
/// Widget to display attachment upload state
/// {@endtemplate}
class StreamAttachmentUploadStateBuilder extends StatelessWidget {
/// Constructor for creating an [StreamAttachmentUploadStateBuilder] widget
/// {@macro streamAttachmentUploadStateBuilder}
const StreamAttachmentUploadStateBuilder({
super.key,
required this.message,
required this.attachment,
this.failedBuilder,
this.successBuilder,
this.inProgressBuilder,
this.preparingBuilder,
this.inProgressBuilder,
this.successBuilder,
this.failedBuilder,
});
/// Message which attachment is added to
/// The message that [attachment] is associated with
final Message message;
/// Attachment in concern
/// The attachment currently being handled
final Attachment attachment;
/// Widget to display when failed
final FailedBuilder? failedBuilder;
/// Widget to display when preparing to upload the [attachment]
final PreparingBuilder? preparingBuilder;
/// Widget to display when succeeded
final WidgetBuilder? successBuilder;
/// Widget to display when in progress
/// {@macro inProgressBuilder}
final InProgressBuilder? inProgressBuilder;
/// Widget to display when in prep
final WidgetBuilder? preparingBuilder;
/// {@macro successBuilder}
final SuccessBuilder? successBuilder;
/// {@macro failedBuilder}
final FailedBuilder? failedBuilder;
@override
Widget build(BuildContext context) {
@@ -56,18 +45,22 @@ class StreamAttachmentUploadStateBuilder extends StatelessWidget {
final attachmentId = attachment.id;
final inProgress = inProgressBuilder ??
(context, int sent, int total) => _InProgressState(
sent: sent,
total: total,
attachmentId: attachmentId,
);
(context, int sent, int total) {
return _InProgressState(
sent: sent,
total: total,
attachmentId: attachmentId,
);
};
final failed = failedBuilder ??
(context, error) => _FailedState(
error: error,
messageId: messageId,
attachmentId: attachmentId,
);
(context, error) {
return _FailedState(
error: error,
messageId: messageId,
attachmentId: attachmentId,
);
};
final success = successBuilder ?? (context) => _SuccessState();
@@ -93,22 +86,24 @@ class _IconButton extends StatelessWidget {
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) => SizedBox(
height: 24,
width: 24,
child: RawMaterialButton(
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
hoverElevation: 0,
onPressed: onPressed,
fillColor: StreamChatTheme.of(context).colorTheme.overlayDark,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: icon,
Widget build(BuildContext context) {
return SizedBox(
height: 24,
width: 24,
child: RawMaterialButton(
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
hoverElevation: 0,
onPressed: onPressed,
fillColor: StreamChatTheme.of(context).colorTheme.overlayDark,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
);
child: icon,
),
);
}
}
class _PreparingState extends StatelessWidget {
@@ -237,14 +232,16 @@ class _FailedState extends StatelessWidget {
class _SuccessState extends StatelessWidget {
@override
Widget build(BuildContext context) => Align(
alignment: Alignment.topRight,
child: CircleAvatar(
backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark,
maxRadius: 12,
child: StreamSvgIcon.check(
color: StreamChatTheme.of(context).colorTheme.barsBg,
),
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topRight,
child: CircleAvatar(
backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark,
maxRadius: 12,
child: StreamSvgIcon.check(
color: StreamChatTheme.of(context).colorTheme.barsBg,
),
);
),
);
}
}
@@ -24,31 +24,28 @@ enum AttachmentSource {
}
}
/// {@macro attachment_widget}
@Deprecated("Use 'StreamAttachmentWidget' instead")
typedef AttachmentWidget = StreamAttachmentWidget;
/// {@template attachment_widget}
/// {@template streamAttachmentWidget}
/// Abstract class for deriving attachment types
/// {@endtemplate}
abstract class StreamAttachmentWidget extends StatelessWidget {
/// Constructor for creating attachment widget
/// {@macro streamAttachmentWidget}
const StreamAttachmentWidget({
super.key,
required this.message,
required this.attachment,
this.size,
this.constraints,
AttachmentSource? source,
}) : _source = source;
/// Size of attachments
final Size? size;
/// Contraints of attachments
final BoxConstraints? constraints;
final AttachmentSource? _source;
/// Message which attachment is attached to
/// The message that [attachment] is associated with
final Message message;
/// Attachment to display
/// The [Attachment] to display
final Attachment attachment;
/// Getter for source of attachment
@@ -58,33 +55,3 @@ abstract class StreamAttachmentWidget extends StatelessWidget {
? AttachmentSource.local
: AttachmentSource.network);
}
/// Widget for building in case of error
class AttachmentError extends StatelessWidget {
/// Constructor for creating AttachmentError
const AttachmentError({
super.key,
this.size,
});
/// Size of error
final Size? size;
@override
Widget build(BuildContext context) => Center(
child: Container(
width: size?.width,
height: size?.height,
color: StreamChatTheme.of(context)
.colorTheme
.accentError
.withOpacity(0.1),
child: Center(
child: Icon(
Icons.error_outline,
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
),
),
),
);
}
@@ -2,46 +2,45 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler.dart';
import 'package:stream_chat_flutter/src/indicators/upload_progress_indicator.dart';
import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// {@macro file_attachment}
@Deprecated("Use 'StreamFileAttachment' instead")
typedef FileAttachment = StreamFileAttachment;
/// {@template file_attachment}
/// Widget for displaying file attachments
/// {@template streamFileAttachment}
/// Displays file attachments that have been sent in a chat.
///
/// Used in [MessageWidget].
/// {@endtemplate}
class StreamFileAttachment extends StreamAttachmentWidget {
/// Constructor for creating a widget when attachment is of type 'file'
/// {@macro streamFileAttachment}
const StreamFileAttachment({
super.key,
required super.message,
required super.attachment,
super.size,
super.constraints,
this.title,
this.trailing,
this.onAttachmentTap,
});
/// Title for attachment
/// Title for the attachment
final Widget? title;
/// Widget for displaying at the end of attachment (such as a download button)
/// Widget for displaying at the end of the attachment
/// (such as a download button)
final Widget? trailing;
/// Callback called when attachment widget is tapped
final VoidCallback? onAttachmentTap;
/// {@macro onAttachmentTap}
final OnAttachmentTap? onAttachmentTap;
/// Check if attachment is a video
/// Checks if the attachment is a video
bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video';
/// Check if attachment is an image
/// Checks if the attachment is an image
bool get isImageAttachment => attachment.title?.mimeType?.type == 'image';
@override
@@ -51,7 +50,7 @@ class StreamFileAttachment extends StreamAttachmentWidget {
child: GestureDetector(
onTap: onAttachmentTap,
child: Container(
width: size?.width ?? 100,
constraints: constraints ?? const BoxConstraints.tightFor(width: 100),
height: 56,
decoration: BoxDecoration(
color: colorTheme.barsBg,
@@ -67,7 +66,12 @@ class StreamFileAttachment extends StreamAttachmentWidget {
height: 40,
width: 33.33,
margin: const EdgeInsets.all(8),
child: _getFileTypeImage(context),
child: _FileTypeImage(
isImageAttachment: isImageAttachment,
isVideoAttachment: isVideoAttachment,
source: source,
attachment: attachment,
),
),
const SizedBox(width: 8),
Expanded(
@@ -82,27 +86,51 @@ class StreamFileAttachment extends StreamAttachmentWidget {
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 3),
_buildSubtitle(context),
_FileAttachmentSubtitle(attachment: attachment),
],
),
),
const SizedBox(width: 8),
_buildTrailing(context),
Material(
type: MaterialType.transparency,
child: trailing ??
_Trailing(
attachment: attachment,
message: message,
),
),
],
),
),
),
);
}
}
ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder(
side: const BorderSide(width: 0, color: Colors.transparent),
borderRadius: BorderRadius.circular(8),
);
class _FileTypeImage extends StatelessWidget {
const _FileTypeImage({
required this.isImageAttachment,
required this.isVideoAttachment,
required this.source,
required this.attachment,
});
// TODO: Improve image memory. This is using the full image instead of a
// smaller version (thumbnail)
Widget _getFileTypeImage(BuildContext context) {
final bool isImageAttachment;
final bool isVideoAttachment;
final AttachmentSource source;
final Attachment attachment;
ShapeBorder _getDefaultShape(BuildContext context) {
return RoundedRectangleBorder(
side: const BorderSide(width: 0, color: Colors.transparent),
borderRadius: BorderRadius.circular(8),
);
}
// TODO: Improve image memory.
// This is using the full image instead of a smaller version (thumbnail)
@override
Widget build(BuildContext context) {
if (isImageAttachment) {
return Material(
clipBehavior: Clip.hardEdge,
@@ -111,13 +139,16 @@ class StreamFileAttachment extends StreamAttachmentWidget {
child: source.when(
local: () {
if (attachment.file?.bytes == null) {
return getFileTypeImage(attachment.extraData['other'] as String?);
return getFileTypeImage(
attachment.extraData['mime_type'] as String?,
);
}
return Image.memory(
attachment.file!.bytes!,
fit: BoxFit.cover,
errorBuilder: (_, obj, trace) =>
getFileTypeImage(attachment.extraData['other'] as String?),
errorBuilder: (_, obj, trace) => getFileTypeImage(
attachment.extraData['mime_type'] as String?,
),
);
},
network: () {
@@ -125,15 +156,18 @@ class StreamFileAttachment extends StreamAttachmentWidget {
attachment.assetUrl ??
attachment.thumbUrl) ==
null) {
return getFileTypeImage(attachment.extraData['other'] as String?);
return getFileTypeImage(
attachment.extraData['mime_type'] as String?,
);
}
return CachedNetworkImage(
imageUrl: attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl!,
fit: BoxFit.cover,
errorWidget: (_, obj, trace) =>
getFileTypeImage(attachment.extraData['other'] as String?),
errorWidget: (_, obj, trace) => getFileTypeImage(
attachment.extraData['mime_type'] as String?,
),
placeholder: (_, __) {
final image = Image.asset(
'images/placeholder.png',
@@ -187,38 +221,47 @@ class StreamFileAttachment extends StreamAttachmentWidget {
}
return getFileTypeImage(attachment.extraData['mime_type'] as String?);
}
}
Widget _buildButton({
Widget? icon,
double iconSize = 24.0,
VoidCallback? onPressed,
Color? fillColor,
}) =>
SizedBox(
height: iconSize,
width: iconSize,
child: RawMaterialButton(
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
hoverElevation: 0,
onPressed: onPressed,
fillColor: fillColor,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: icon,
),
);
class _Trailing extends StatelessWidget {
const _Trailing({
required this.attachment,
required this.message,
});
Widget _buildTrailing(BuildContext context) {
final Attachment attachment;
final Message message;
@override
Widget build(BuildContext context) {
final theme = StreamChatTheme.of(context);
final channel = StreamChannel.of(context).channel;
final attachmentId = attachment.id;
var trailingWidget = trailing;
trailingWidget ??= attachment.uploadState.when(
if (message.status == MessageSendingStatus.sent) {
return IconButton(
icon: StreamSvgIcon.cloudDownload(
color: theme.colorTheme.textHighEmphasis,
),
visualDensity: VisualDensity.compact,
splashRadius: 16,
onPressed: () async {
final assetUrl = attachment.assetUrl;
if (assetUrl != null) {
if (isMobileDeviceOrWeb) {
launchURL(context, assetUrl);
} else {
StreamAttachmentHandler.instance.downloadAttachment(attachment);
}
}
},
);
}
return attachment.uploadState.when(
preparing: () => Padding(
padding: const EdgeInsets.all(8),
child: _buildButton(
child: _TrailingButton(
icon: StreamSvgIcon.close(color: theme.colorTheme.barsBg),
fillColor: theme.colorTheme.overlayDark,
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
@@ -226,7 +269,7 @@ class StreamFileAttachment extends StreamAttachmentWidget {
),
inProgress: (_, __) => Padding(
padding: const EdgeInsets.all(8),
child: _buildButton(
child: _TrailingButton(
icon: StreamSvgIcon.close(color: theme.colorTheme.barsBg),
fillColor: theme.colorTheme.overlayDark,
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
@@ -242,7 +285,7 @@ class StreamFileAttachment extends StreamAttachmentWidget {
),
failed: (_) => Padding(
padding: const EdgeInsets.all(8),
child: _buildButton(
child: _TrailingButton(
icon: StreamSvgIcon.retry(color: theme.colorTheme.barsBg),
fillColor: theme.colorTheme.overlayDark,
onPressed: () => channel.retryAttachmentUpload(
@@ -252,28 +295,48 @@ class StreamFileAttachment extends StreamAttachmentWidget {
),
),
);
}
}
if (message.status == MessageSendingStatus.sent) {
trailingWidget = IconButton(
icon: StreamSvgIcon.cloudDownload(
color: theme.colorTheme.textHighEmphasis,
),
visualDensity: VisualDensity.compact,
splashRadius: 16,
onPressed: () {
final assetUrl = attachment.assetUrl;
if (assetUrl != null) launchURL(context, assetUrl);
},
);
}
class _TrailingButton extends StatelessWidget {
const _TrailingButton({
this.onPressed,
this.fillColor,
this.icon,
});
return Material(
type: MaterialType.transparency,
child: trailingWidget,
final VoidCallback? onPressed;
final Color? fillColor;
final Widget? icon;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 24,
width: 24,
child: RawMaterialButton(
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
hoverElevation: 0,
onPressed: onPressed,
fillColor: fillColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: icon,
),
);
}
}
Widget _buildSubtitle(BuildContext context) {
class _FileAttachmentSubtitle extends StatelessWidget {
const _FileAttachmentSubtitle({
required this.attachment,
});
final Attachment attachment;
@override
Widget build(BuildContext context) {
final theme = StreamChatTheme.of(context);
final size = attachment.file?.size ?? attachment.extraData['file_size'];
final textStyle = theme.textTheme.footnote.copyWith(
@@ -2,36 +2,32 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro giphy_attachment}
@Deprecated("Use 'StreamGiphyAttachment' instead")
typedef GiphyAttachment = StreamGiphyAttachment;
/// {@template giphy_attachment}
/// Widget for showing a GIF attachment
/// {@template streamGiphyAttachment}
/// Shows a GIF attachment in a [StreamMessageWidget].
/// {@endtemplate}
class StreamGiphyAttachment extends StreamAttachmentWidget {
/// Constructor for creating a [StreamGiphyAttachment] widget
/// {@macro streamGiphyAttachment}
const StreamGiphyAttachment({
super.key,
required super.message,
required super.attachment,
super.size,
super.constraints,
this.onShowMessage,
this.onReturnAction,
this.onReplyMessage,
this.onAttachmentTap,
});
/// Callback when show message is tapped
/// {@macro showMessageCallback}
final ShowMessageCallback? onShowMessage;
/// Callback when attachment is returned to from other screens
final ValueChanged<ReturnActionType>? onReturnAction;
/// {@macro replyMessageCallback}
final ReplyMessageCallback? onReplyMessage;
/// Callback when attachment is tapped
final VoidCallback? onAttachmentTap;
/// {@macro onAttachmentTap}
final OnAttachmentTap? onAttachmentTap;
@override
Widget build(BuildContext context) {
@@ -48,287 +44,301 @@ class StreamGiphyAttachment extends StreamAttachmentWidget {
Widget _buildSendingAttachment(BuildContext context, String imageUrl) {
final streamChannel = StreamChannel.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Card(
color: StreamChatTheme.of(context).colorTheme.barsBg,
elevation: 2,
clipBehavior: Clip.hardEdge,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(16),
topLeft: Radius.circular(16),
bottomLeft: Radius.circular(16),
return ConstrainedBox(
constraints: constraints?.copyWith(
maxHeight: double.infinity,
) ??
const BoxConstraints.expand(),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Card(
color: StreamChatTheme.of(context).colorTheme.barsBg,
elevation: 2,
clipBehavior: Clip.hardEdge,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(16),
topLeft: Radius.circular(16),
bottomLeft: Radius.circular(16),
),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
StreamSvgIcon.giphyIcon(),
const SizedBox(width: 8),
Text(
context.translations.giphyLabel,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
if (attachment.title != null)
Flexible(
child: Text(
attachment.title!,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
StreamSvgIcon.giphyIcon(),
const SizedBox(width: 8),
Text(
context.translations.giphyLabel,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
if (attachment.title != null)
Flexible(
child: Text(
attachment.title!,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
),
Padding(
padding: const EdgeInsets.all(2),
child: GestureDetector(
onTap: () {
if (onAttachmentTap != null) {
onAttachmentTap?.call();
} else {
_onImageTap(context);
}
},
child: CachedNetworkImage(
height: constraints?.maxHeight,
width: constraints?.maxWidth,
placeholder: (_, __) => SizedBox(
width: constraints?.maxHeight,
height: constraints?.maxWidth,
child: const Center(
child: CircularProgressIndicator(),
),
),
imageUrl: imageUrl,
errorWidget: (context, url, error) => AttachmentError(
constraints: constraints,
),
fit: BoxFit.cover,
),
),
),
Container(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.2),
width: double.infinity,
height: 0.5,
),
Row(
children: [
Expanded(
child: SizedBox(
height: 50,
child: TextButton(
onPressed: () {
streamChannel.channel.sendAction(
message,
{
'image_action': 'cancel',
},
);
},
child: Text(
context.translations.cancelLabel
.toLowerCase()
.capitalize(),
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
),
),
),
Container(
width: 0.5,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.2),
height: 50,
),
Expanded(
child: SizedBox(
height: 50,
child: TextButton(
onPressed: () {
streamChannel.channel.sendAction(
message,
{
'image_action': 'shuffle',
},
);
},
child: Text(
context.translations.shuffleLabel,
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
maxLines: 1,
),
),
),
),
Container(
width: 0.5,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.2),
height: 50,
),
Expanded(
child: SizedBox(
height: 50,
child: TextButton(
onPressed: () {
streamChannel.channel.sendAction(
message,
{
'image_action': 'send',
},
);
},
child: Text(
context.translations.sendLabel,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.accentPrimary,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(2),
child: GestureDetector(
onTap: () {
if (onAttachmentTap != null) {
onAttachmentTap?.call();
} else {
_onImageTap(context);
}
},
child: CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) => SizedBox(
width: size?.width,
height: size?.height,
child: const Center(
child: CircularProgressIndicator(),
),
),
imageUrl: imageUrl,
errorWidget: (context, url, error) =>
AttachmentError(size: size),
fit: BoxFit.cover,
),
),
),
Container(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.2),
width: double.infinity,
height: 0.5,
),
Row(
children: [
Expanded(
child: SizedBox(
height: 50,
child: TextButton(
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'cancel',
});
},
child: Text(
context.translations.cancelLabel
.toLowerCase()
.capitalize(),
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
),
),
),
Container(
width: 0.5,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.2),
height: 50,
),
Expanded(
child: SizedBox(
height: 50,
child: TextButton(
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'shuffle',
});
},
child: Text(
context.translations.shuffleLabel,
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
maxLines: 1,
),
),
),
),
Container(
width: 0.5,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.2),
height: 50,
),
Expanded(
child: SizedBox(
height: 50,
child: TextButton(
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'send',
});
},
child: Text(
context.translations.sendLabel,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.accentPrimary,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
],
],
),
),
),
const SizedBox(height: 4),
const Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: StreamVisibleFootnote(),
const SizedBox(height: 4),
const Align(
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: StreamVisibleFootnote(),
),
),
),
],
],
),
);
}
void _onImageTap(BuildContext context) async {
final res = await Navigator.push(
context,
Future<void> _onImageTap(BuildContext context) async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) {
final channel = StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: StreamFullScreenMedia(
child: StreamFullScreenMediaBuilder(
mediaAttachmentPackages: message.getAttachmentPackageList(),
startIndex: message.attachments.indexOf(attachment),
userName: message.user?.name,
userName: message.user!.name,
onShowMessage: onShowMessage,
onReplyMessage: onReplyMessage,
),
);
},
),
);
if (res != null) onReturnAction?.call(res);
}
Widget _buildSentAttachment(BuildContext context, String imageUrl) =>
SizedBox(
child: GestureDetector(
onTap: () {
if (onAttachmentTap != null) {
onAttachmentTap?.call();
} else {
_onImageTap(context);
}
},
child: Stack(
children: [
CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) {
final image = Image.asset(
'images/placeholder.png',
fit: BoxFit.cover,
package: 'stream_chat_flutter',
);
final colorTheme = StreamChatTheme.of(context).colorTheme;
return Shimmer.fromColors(
baseColor: colorTheme.disabled,
highlightColor: colorTheme.inputBg,
child: image,
);
},
imageUrl: imageUrl,
errorWidget: (context, url, error) =>
AttachmentError(size: size),
Widget _buildSentAttachment(BuildContext context, String imageUrl) {
return GestureDetector(
onTap: () {
if (onAttachmentTap != null) {
onAttachmentTap?.call();
} else {
_onImageTap(context);
}
},
child: Stack(
children: [
CachedNetworkImage(
height: constraints?.maxHeight,
width: constraints?.maxWidth,
placeholder: (_, __) {
final image = Image.asset(
'images/placeholder.png',
fit: BoxFit.cover,
package: 'stream_chat_flutter',
);
final colorTheme = StreamChatTheme.of(context).colorTheme;
return Shimmer.fromColors(
baseColor: colorTheme.disabled,
highlightColor: colorTheme.inputBg,
child: image,
);
},
imageUrl: imageUrl,
errorWidget: (context, url, error) => AttachmentError(
constraints: constraints,
),
fit: BoxFit.cover,
),
Positioned(
bottom: 8,
left: 8,
child: Material(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
Positioned(
bottom: 8,
left: 8,
child: Material(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
child: Row(
children: [
StreamSvgIcon.lightning(
color: StreamChatTheme.of(context).colorTheme.barsBg,
size: 16,
),
child: Row(
children: [
StreamSvgIcon.lightning(
color: StreamChatTheme.of(context).colorTheme.barsBg,
size: 16,
),
Text(
context.translations.giphyLabel.toUpperCase(),
style: TextStyle(
color:
StreamChatTheme.of(context).colorTheme.barsBg,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
],
Text(
context.translations.giphyLabel.toUpperCase(),
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.barsBg,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
),
],
),
),
],
),
),
),
);
],
),
);
}
}
@@ -0,0 +1,71 @@
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:file_selector/file_selector.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Downloads the [attachment] to the device and returns
/// the path to the file.
Future<String?> downloadWebOrDesktopAttachment(
Attachment attachment, {
ProgressCallback? onReceiveProgress,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
Options? options,
}) async {
final type = attachment.type;
String? downloadUrl;
String? fileName;
/* ---IMAGES/GIFS--- */
if (type == 'image') {
downloadUrl = attachment.imageUrl ?? attachment.assetUrl;
fileName = attachment.title;
fileName ??= 'attachment.${attachment.mimeType ?? 'png'}';
}
/* ---GIPHY's--- */
else if (type == 'giphy') {
downloadUrl = attachment.thumbUrl;
fileName = '${attachment.title}.gif';
}
/* ---FILES AND VIDEOS--- */
else if (type == 'file' || type == 'video') {
downloadUrl = attachment.assetUrl;
fileName = attachment.title;
}
assert(
downloadUrl != null,
'Attachment must have an assetUrl or imageUrl or thumbUrl',
);
final response = await Dio().get<List<int>>(
downloadUrl!,
onReceiveProgress: onReceiveProgress,
queryParameters: queryParameters,
cancelToken: cancelToken,
// set responseType to `bytes`
options: options?.copyWith(responseType: ResponseType.bytes) ??
Options(responseType: ResponseType.bytes),
);
// Open the native file browser so the user can select the download path.
final path = await getSavePath(suggestedName: fileName);
if (path == null) {
// Operation was canceled by the user.
return null;
}
// Create an XFile for proper file saving
final file = XFile.fromData(
Uint8List.fromList(response.data!),
mimeType: attachment.mimeType,
name: fileName,
path: path,
);
// Save the file to the user's selected path.
await file.saveTo(path);
return path;
}
@@ -0,0 +1,4 @@
export 'stream_attachment_handler_base.dart'
if (dart.library.html) 'stream_attachment_handler_html.dart'
if (dart.library.io) 'stream_attachment_handler_io.dart'
show StreamAttachmentHandler;
@@ -0,0 +1,80 @@
import 'package:file_picker/file_picker.dart';
import 'package:image_picker/image_picker.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Base class for handling attachment related functionality.
abstract class StreamAttachmentHandlerBase {
/// Pick an image from the device.
Future<Attachment?> pickImage({
required ImageSource source,
double? maxWidth,
double? maxHeight,
int? imageQuality,
CameraDevice preferredCameraDevice = CameraDevice.rear,
}) {
throw UnimplementedError('pickImage is not implemented');
}
/// Pick a video from the device.
Future<Attachment?> pickVideo({
required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
Duration? maxDuration,
}) {
throw UnimplementedError('pickVideo is not implemented');
}
/// Pick a file from the device.
Future<Attachment?> pickFile({
String? dialogTitle,
String? initialDirectory,
FileType type = FileType.any,
List<String>? allowedExtensions,
Function(FilePickerStatus)? onFileLoading,
bool allowCompression = true,
bool withData = true,
bool withReadStream = false,
bool lockParentWindow = true,
}) {
throw UnimplementedError('pickFile is not implemented');
}
/// Pick an audio from the device.
Future<Attachment?> pickAudio() {
throw UnimplementedError('pickAudio is not implemented');
}
/// Saves the [attachmentFile] to the temporary directory.
Future<String> saveAttachmentFile({
required AttachmentFile attachmentFile,
}) {
throw UnimplementedError('saveFile is not implemented');
}
/// Deletes the [attachmentFile] from the temporary directory.
Future<void> deleteAttachmentFile({
required AttachmentFile attachmentFile,
}) {
throw UnimplementedError('deleteAttachmentFile is not implemented');
}
/// Downloads the [attachment] to the device and returns
/// the path to the file.
Future<String?> downloadAttachment(
Attachment attachment, {
ProgressCallback? onReceiveProgress,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
Options? options,
}) {
throw UnimplementedError('downloadAttachment is not implemented');
}
}
/// Stub implementation of [StreamAttachmentHandlerBase].
class StreamAttachmentHandler extends StreamAttachmentHandlerBase {
/// Returns an instance of [StreamAttachmentHandler].
static StreamAttachmentHandler get instance {
throw UnimplementedError('instance is not implemented');
}
}
@@ -0,0 +1,63 @@
import 'package:file_picker/file_picker.dart';
import 'package:stream_chat_flutter/src/attachment/handler/common.dart';
import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler_base.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// StreamAttachmentHandler implementation for html.
class StreamAttachmentHandler extends StreamAttachmentHandlerBase {
StreamAttachmentHandler._();
static StreamAttachmentHandler? _instance;
/// Returns the singleton instance of [StreamAttachmentHandler].
// ignore: prefer_constructors_over_static_methods
static StreamAttachmentHandler get instance =>
_instance ??= StreamAttachmentHandler._();
late final _filePicker = FilePicker.platform;
@override
Future<Attachment?> pickFile({
String? dialogTitle,
String? initialDirectory,
FileType type = FileType.any,
List<String>? allowedExtensions,
Function(FilePickerStatus)? onFileLoading,
bool allowCompression = true,
bool withData = true,
bool withReadStream = false,
bool lockParentWindow = true,
}) async {
final result = await _filePicker.pickFiles(
dialogTitle: dialogTitle,
initialDirectory: initialDirectory,
type: type,
allowedExtensions: allowedExtensions,
onFileLoading: onFileLoading,
allowCompression: allowCompression,
withData: withData,
withReadStream: withReadStream,
lockParentWindow: lockParentWindow,
);
return result?.files.first.toAttachment(type: type.toAttachmentType());
}
@override
Future<String?> downloadAttachment(
Attachment attachment, {
ProgressCallback? onReceiveProgress,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
Options? options,
}) {
return downloadWebOrDesktopAttachment(
attachment,
onReceiveProgress: onReceiveProgress,
queryParameters: queryParameters,
cancelToken: cancelToken,
options: options,
);
}
}
@@ -0,0 +1,216 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:file_picker/file_picker.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:stream_chat_flutter/src/attachment/handler/common.dart';
import 'package:stream_chat_flutter/src/attachment/handler/stream_attachment_handler_base.dart';
import 'package:stream_chat_flutter/src/utils/device_segmentation.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// StreamAttachmentHandler implementation for desktop.
class StreamAttachmentHandlerDesktop extends StreamAttachmentHandler {
/// Returns the singleton instance of [StreamAttachmentHandler].
StreamAttachmentHandlerDesktop() : super.__();
@override
Future<String?> downloadAttachment(
Attachment attachment, {
ProgressCallback? onReceiveProgress,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
Options? options,
}) {
return downloadWebOrDesktopAttachment(
attachment,
onReceiveProgress: onReceiveProgress,
queryParameters: queryParameters,
cancelToken: cancelToken,
options: options,
);
}
}
/// StreamAttachmentHandler implementation for io.
class StreamAttachmentHandler extends StreamAttachmentHandlerBase {
StreamAttachmentHandler.__();
factory StreamAttachmentHandler._() {
if (isDesktopDevice) {
return StreamAttachmentHandlerDesktop();
}
return StreamAttachmentHandler.__();
}
static StreamAttachmentHandler? _instance;
/// Returns the singleton instance of [StreamAttachmentHandler].
// ignore: prefer_constructors_over_static_methods
static StreamAttachmentHandler get instance =>
_instance ??= StreamAttachmentHandler._();
late final _imagePicker = ImagePicker();
late final _filePicker = FilePicker.platform;
@override
Future<Attachment?> pickImage({
required ImageSource source,
double? maxWidth,
double? maxHeight,
int? imageQuality,
CameraDevice preferredCameraDevice = CameraDevice.rear,
}) async {
final image = await _imagePicker.pickImage(
source: source,
maxWidth: maxWidth,
maxHeight: maxHeight,
imageQuality: imageQuality,
preferredCameraDevice: preferredCameraDevice,
);
return image?.toAttachment(type: 'image');
}
@override
Future<Attachment?> pickVideo({
required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
Duration? maxDuration,
}) async {
final video = await _imagePicker.pickVideo(
source: source,
preferredCameraDevice: preferredCameraDevice,
maxDuration: maxDuration,
);
return video?.toAttachment(type: 'video');
}
@override
Future<Attachment?> pickFile({
String? dialogTitle,
String? initialDirectory,
FileType type = FileType.any,
List<String>? allowedExtensions,
Function(FilePickerStatus)? onFileLoading,
bool allowCompression = true,
bool withData = true,
bool withReadStream = false,
bool lockParentWindow = true,
}) async {
final result = await _filePicker.pickFiles(
dialogTitle: dialogTitle,
initialDirectory: initialDirectory,
type: type,
allowedExtensions: allowedExtensions,
onFileLoading: onFileLoading,
allowCompression: allowCompression,
withData: withData,
withReadStream: withReadStream,
lockParentWindow: lockParentWindow,
);
return result?.files.first.toAttachment(type: type.toAttachmentType());
}
@override
Future<String> saveAttachmentFile({
required AttachmentFile attachmentFile,
}) async {
final fileName = attachmentFile.name;
assert(fileName != null, 'Attachment file name is required');
final tempDir = await getTemporaryDirectory();
final tempPath = Uri.file(tempDir.path, windows: CurrentPlatform.isWindows);
final tempFilePath = tempPath.resolve(fileName!).path;
print(tempFilePath);
final attachmentFileBytes = attachmentFile.bytes;
if (attachmentFileBytes == null) {
final attachmentFilePath = attachmentFile.path!;
final file = File(attachmentFilePath);
return file.copy(tempFilePath).then((it) => it.path);
} else {
final file = File(tempFilePath);
return file.writeAsBytes(attachmentFileBytes).then((it) => it.path);
}
}
@override
Future<void> deleteAttachmentFile({
required AttachmentFile attachmentFile,
}) async {
final attachmentFilePath = attachmentFile.path;
if (attachmentFilePath != null) {
final file = File(attachmentFilePath);
if (file.existsSync()) {
await file.delete();
}
}
}
@override
Future<String?> downloadAttachment(
Attachment attachment, {
ProgressCallback? onReceiveProgress,
Map<String, dynamic>? queryParameters,
CancelToken? cancelToken,
Options? options,
}) async {
final type = attachment.type;
String? downloadUrl;
String? fileName;
/* ---IMAGES/GIFS--- */
if (type == 'image') {
downloadUrl = attachment.imageUrl ?? attachment.assetUrl;
fileName = attachment.title;
fileName ??= 'attachment.${attachment.mimeType ?? 'png'}';
}
/* ---GIPHY's--- */
else if (type == 'giphy') {
downloadUrl = attachment.thumbUrl;
fileName = '${attachment.title}.gif';
}
/* ---FILES AND VIDEOS--- */
else if (type == 'file' || type == 'video') {
downloadUrl = attachment.assetUrl;
fileName = attachment.title;
}
assert(
downloadUrl != null,
'Attachment must have an assetUrl or imageUrl or thumbUrl',
);
final response = await Dio().get<List<int>>(
downloadUrl!,
onReceiveProgress: onReceiveProgress,
queryParameters: queryParameters,
cancelToken: cancelToken,
// set responseType to `bytes`
options: options?.copyWith(responseType: ResponseType.bytes) ??
Options(responseType: ResponseType.bytes),
);
final appDir = await getTemporaryDirectory();
final ext = Uri.parse(downloadUrl).pathSegments.last;
final path = '${appDir.path}/${attachment.id}.$ext';
// Create an XFile for proper file saving
final file = XFile.fromData(
Uint8List.fromList(response.data!),
mimeType: attachment.mimeType,
name: fileName,
path: path,
);
// Save the file to the user's selected path.
await file.saveTo(path);
return path;
}
}
@@ -1,49 +1,44 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_title.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro image_attachment}
@Deprecated("use 'StreamImageAttachment' instead")
typedef ImageAttachment = StreamImageAttachment;
/// {@template image_attachment}
/// Widget for showing an image attachment
/// {@template streamImageAttachment}
/// Shows an image attachment in a [StreamMessageWidget].
/// {@endtemplate}
class StreamImageAttachment extends StreamAttachmentWidget {
/// Constructor for creating a [StreamImageAttachment] widget
/// {@macro streamImageAttachment}
const StreamImageAttachment({
super.key,
required super.message,
required super.attachment,
required this.messageTheme,
super.size,
super.constraints,
this.showTitle = false,
this.onShowMessage,
this.onReturnAction,
this.onReplyMessage,
this.onAttachmentTap,
this.imageThumbnailSize = const Size(400, 400),
this.imageThumbnailResizeType = 'crop',
this.imageThumbnailResizeType = 'clip',
this.imageThumbnailCropType = 'center',
});
/// [StreamMessageThemeData] for showing image title
/// The [StreamMessageThemeData] to use for the image title
final StreamMessageThemeData messageTheme;
/// Flag for showing title
/// Flag for whether the title should be shown or not
final bool showTitle;
/// Callback when show message is tapped
/// {@macro showMessageCallback}
final ShowMessageCallback? onShowMessage;
/// Callback when attachment is returned to from other screens
final ValueChanged<ReturnActionType>? onReturnAction;
/// {@macro replyMessageCallback}
final ReplyMessageCallback? onReplyMessage;
/// Callback when attachment is tapped
final VoidCallback? onAttachmentTap;
/// {@macro onAttachmentTap}
final OnAttachmentTap? onAttachmentTap;
/// Size of the attachment image thumbnail.
final Size imageThumbnailSize;
@@ -59,118 +54,126 @@ class StreamImageAttachment extends StreamAttachmentWidget {
final String /*center|top|bottom|left|right*/ imageThumbnailCropType;
@override
Widget build(BuildContext context) => source.when(
local: () {
if (attachment.localUri == null || attachment.file?.bytes == null) {
return AttachmentError(size: size);
}
return _buildImageAttachment(
context,
Image.memory(
attachment.file!.bytes!,
height: size?.height,
width: size?.width,
fit: BoxFit.cover,
errorBuilder: (context, _, __) => Image.asset(
Widget build(BuildContext context) {
return source.when(
local: () {
if (attachment.localUri == null || attachment.file?.bytes == null) {
return AttachmentError(constraints: constraints);
}
return _buildImageAttachment(
context,
Image.memory(
attachment.file!.bytes!,
height: constraints?.maxHeight,
width: constraints?.maxWidth,
fit: BoxFit.cover,
errorBuilder: (context, _, __) => Image.asset(
'images/placeholder.png',
package: 'stream_chat_flutter',
),
),
);
},
network: () {
var imageUrl =
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
if (imageUrl == null) {
return AttachmentError(constraints: constraints);
}
imageUrl = imageUrl.getResizedImageUrl(
width: imageThumbnailSize.width,
height: imageThumbnailSize.height,
resize: imageThumbnailResizeType,
crop: imageThumbnailCropType,
);
return _buildImageAttachment(
context,
CachedNetworkImage(
imageUrl: imageUrl,
height: constraints?.maxHeight,
width: constraints?.maxWidth,
fit: BoxFit.cover,
placeholder: (context, __) {
final image = Image.asset(
'images/placeholder.png',
fit: BoxFit.cover,
package: 'stream_chat_flutter',
),
),
);
},
network: () {
var imageUrl =
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
);
final colorTheme = StreamChatTheme.of(context).colorTheme;
return Shimmer.fromColors(
baseColor: colorTheme.disabled,
highlightColor: colorTheme.inputBg,
child: image,
);
},
errorWidget: (context, url, error) =>
AttachmentError(constraints: constraints),
),
);
},
);
}
if (imageUrl == null) return AttachmentError(size: size);
imageUrl = imageUrl.getResizedImageUrl(
width: imageThumbnailSize.width,
height: imageThumbnailSize.height,
resize: imageThumbnailResizeType,
crop: imageThumbnailCropType,
);
return _buildImageAttachment(
context,
CachedNetworkImage(
imageUrl: imageUrl,
height: size?.height,
width: size?.width,
fit: BoxFit.cover,
placeholder: (context, __) {
final image = Image.asset(
'images/placeholder.png',
fit: BoxFit.cover,
package: 'stream_chat_flutter',
);
final colorTheme = StreamChatTheme.of(context).colorTheme;
return Shimmer.fromColors(
baseColor: colorTheme.disabled,
highlightColor: colorTheme.inputBg,
child: image,
);
},
errorWidget: (context, url, error) => AttachmentError(size: size),
),
);
},
);
Widget _buildImageAttachment(BuildContext context, Widget imageWidget) =>
ConstrainedBox(
constraints: BoxConstraints.loose(size!),
child: Column(
children: <Widget>[
Expanded(
child: Stack(
children: [
GestureDetector(
Widget _buildImageAttachment(BuildContext context, Widget imageWidget) {
return Container(
constraints: constraints,
child: Column(
children: <Widget>[
Expanded(
child: Stack(
children: [
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onAttachmentTap ??
() async {
final result = await Navigator.push(
context,
() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) {
final channel =
StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: StreamFullScreenMedia(
child: StreamFullScreenMediaBuilder(
mediaAttachmentPackages:
message.getAttachmentPackageList(),
startIndex:
message.attachments.indexOf(attachment),
userName: message.user?.name,
userName: message.user!.name,
onShowMessage: onShowMessage,
onReplyMessage: onReplyMessage,
),
);
},
),
);
if (result != null) onReturnAction?.call(result);
},
child: imageWidget,
),
Padding(
padding: const EdgeInsets.all(8),
child: StreamAttachmentUploadStateBuilder(
message: message,
attachment: attachment,
),
),
Padding(
padding: const EdgeInsets.all(8),
child: StreamAttachmentUploadStateBuilder(
message: message,
attachment: attachment,
),
],
),
],
),
),
if (showTitle && attachment.title != null)
Material(
color: messageTheme.messageBackgroundColor,
child: StreamAttachmentTitle(
messageTheme: messageTheme,
attachment: attachment,
),
),
if (showTitle && attachment.title != null)
Material(
color: messageTheme.messageBackgroundColor,
child: StreamAttachmentTitle(
messageTheme: messageTheme,
attachment: attachment,
),
),
],
),
);
],
),
);
}
}
@@ -0,0 +1,175 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template streamImageGroup}
/// Constructs a group of image attachments in a [StreamMessageWidget].
/// {@endtemplate}
class StreamImageGroup extends StatelessWidget {
/// {@macro streamImageGroup}
const StreamImageGroup({
super.key,
required this.images,
required this.message,
required this.messageTheme,
required this.constraints,
this.onShowMessage,
this.onReplyMessage,
this.onAttachmentTap,
this.imageThumbnailSize = const Size(400, 400),
this.imageThumbnailResizeType = 'clip',
this.imageThumbnailCropType = 'center',
});
/// List of attachments to show
final List<Attachment> images;
/// {@macro onImageGroupAttachmentTap}
final OnImageGroupAttachmentTap? onAttachmentTap;
/// The [Message] that the images are attached to
final Message message;
/// The [StreamMessageThemeData] to apply to this [message]
final StreamMessageThemeData messageTheme;
/// The constraints of the [images]
final BoxConstraints constraints;
/// {@macro showMessageCallback}
final ShowMessageCallback? onShowMessage;
/// {@macro replyMessageCallback}
final ReplyMessageCallback? onReplyMessage;
/// Size of the attachment image thumbnail.
final Size imageThumbnailSize;
/// Resize type of the image attachment thumbnail.
///
/// Defaults to [crop]
final String /*clip|crop|scale|fill*/ imageThumbnailResizeType;
/// Crop type of the image attachment thumbnail.
///
/// Defaults to [center]
final String /*center|top|bottom|left|right*/ imageThumbnailCropType;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: constraints,
child: Flex(
direction: Axis.vertical,
children: <Widget>[
Flexible(
fit: FlexFit.tight,
child: Flex(
crossAxisAlignment: CrossAxisAlignment.stretch,
direction: Axis.horizontal,
children: [
Flexible(
fit: FlexFit.tight,
child: _buildImage(context, 0),
),
Flexible(
fit: FlexFit.tight,
child: Padding(
padding: const EdgeInsets.only(left: 2),
child: _buildImage(context, 1),
),
),
],
),
),
if (images.length >= 3)
Flexible(
fit: FlexFit.tight,
child: Padding(
padding: const EdgeInsets.only(top: 2),
child: Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Flexible(
fit: FlexFit.tight,
child: _buildImage(context, 2),
),
if (images.length >= 4)
Flexible(
fit: FlexFit.tight,
child: Padding(
padding: const EdgeInsets.only(left: 2),
child: Stack(
fit: StackFit.expand,
children: <Widget>[
_buildImage(context, 3),
if (images.length > 4)
Positioned.fill(
child: GestureDetector(
onTap: () => _onTap(context, 3),
child: Material(
color: Colors.black38,
child: Center(
child: Text(
'+ ${images.length - 4}',
style: const TextStyle(
color: Colors.white,
fontSize: 26,
),
),
),
),
),
),
],
),
),
),
],
),
),
),
],
),
);
}
Future<void> _onTap(
BuildContext context,
int index,
) async {
if (onAttachmentTap != null) {
return onAttachmentTap!(message, images[index]);
}
final channel = StreamChannel.of(context).channel;
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: StreamFullScreenMediaBuilder(
mediaAttachmentPackages: message.getAttachmentPackageList(),
startIndex: index,
userName: message.user!.name,
onShowMessage: onShowMessage,
onReplyMessage: onReplyMessage,
),
),
),
);
}
Widget _buildImage(BuildContext context, int index) {
return StreamImageAttachment(
attachment: images[index],
constraints: constraints,
message: message,
messageTheme: messageTheme,
onAttachmentTap: () => _onTap(context, index),
imageThumbnailSize: imageThumbnailSize,
imageThumbnailResizeType: imageThumbnailResizeType,
imageThumbnailCropType: imageThumbnailCropType,
);
}
}
@@ -0,0 +1,18 @@
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// The [StreamAttachmentPackage] class is basically meant to wrap
/// individual attachments with their corresponding message
class StreamAttachmentPackage {
/// Default constructor to prepare an [StreamAttachmentPackage] object
StreamAttachmentPackage({
required this.attachment,
required this.message,
});
/// This is the individual attachment
final Attachment attachment;
/// This is the message that the attachment belongs to
/// The message object may have attachment(s) other than the one packaged
final Message message;
}
@@ -2,15 +2,11 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro url_attachment}
@Deprecated("Use 'StreamUrlAttachment' instead")
typedef UrlAttachment = StreamUrlAttachment;
/// {@template url_attachment}
/// Widget to display URL attachment
/// {@template streamUrlAttachment}
/// Displays a URL attachment in a [StreamMessageWidget].
/// {@endtemplate}
class StreamUrlAttachment extends StatelessWidget {
/// Constructor for creating a [StreamUrlAttachment]
/// {@macro streamUrlAttachment}
const StreamUrlAttachment({
super.key,
required this.urlAttachment,
@@ -32,7 +28,7 @@ class StreamUrlAttachment extends StatelessWidget {
/// Padding for text
final EdgeInsets textPadding;
/// [StreamMessageThemeData] for showing image title
/// The [StreamMessageThemeData] to use for the image title
final StreamMessageThemeData messageTheme;
/// The function called when tapping on a link
@@ -41,83 +37,93 @@ class StreamUrlAttachment extends StatelessWidget {
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
return GestureDetector(
onTap: () {
final ogScrapeUrl = urlAttachment.ogScrapeUrl;
if (ogScrapeUrl != null) {
onLinkTap != null
? onLinkTap!(ogScrapeUrl)
: launchURL(context, ogScrapeUrl);
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (urlAttachment.imageUrl != null)
Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
CachedNetworkImage(
width: double.infinity,
imageUrl: urlAttachment.imageUrl!,
fit: BoxFit.cover,
return ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 400,
minWidth: 400,
),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
final ogScrapeUrl = urlAttachment.ogScrapeUrl;
if (ogScrapeUrl != null) {
onLinkTap != null
? onLinkTap!(ogScrapeUrl)
: launchURL(context, ogScrapeUrl);
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (urlAttachment.imageUrl != null)
Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
Positioned(
left: 0,
bottom: -1,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(16),
),
color: messageTheme.linkBackgroundColor,
child: Stack(
children: [
CachedNetworkImage(
width: double.infinity,
imageUrl: urlAttachment.imageUrl!,
fit: BoxFit.cover,
),
child: Padding(
padding: const EdgeInsets.only(
top: 8,
left: 8,
right: 8,
),
child: Text(
hostDisplayName,
style: chatThemeData.textTheme.bodyBold.copyWith(
color: chatThemeData.colorTheme.accentPrimary,
Positioned(
left: 0,
bottom: -1,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
topRight: Radius.circular(16),
),
color: messageTheme.linkBackgroundColor,
),
child: Padding(
padding: const EdgeInsets.only(
top: 8,
left: 8,
right: 8,
),
child: Text(
hostDisplayName,
style: chatThemeData.textTheme.bodyBold.copyWith(
color: chatThemeData.colorTheme.accentPrimary,
),
),
),
),
),
),
],
),
],
),
Padding(
padding: textPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (urlAttachment.title != null)
Text(
urlAttachment.title!.trim(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: chatThemeData.textTheme.body
.copyWith(fontWeight: FontWeight.w700),
),
if (urlAttachment.text != null)
Text(
urlAttachment.text!,
style: chatThemeData.textTheme.body
.copyWith(fontWeight: FontWeight.w400),
),
],
),
),
),
Padding(
padding: textPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (urlAttachment.title != null)
Text(
urlAttachment.title!.trim(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: chatThemeData.textTheme.body
.copyWith(fontWeight: FontWeight.w700),
),
if (urlAttachment.text != null)
Text(
urlAttachment.text!,
style: chatThemeData.textTheme.body
.copyWith(fontWeight: FontWeight.w400),
),
],
),
],
),
],
),
),
);
}
@@ -1,135 +1,124 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_title.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/src/video/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro video_attachment}
@Deprecated("Use 'StreamVideoAttachment' instead")
typedef VideoAttachment = StreamVideoAttachment;
/// {@template video_attachment}
/// Widget for showing a video attachment
/// {@template streamVideoAttachment}
/// Shows a video attachment in a [StreamMessageWidget].
/// {@endtemplate}
class StreamVideoAttachment extends StreamAttachmentWidget {
/// Constructor for creating a [StreamVideoAttachment] widget
/// {@macro streamVideoAttachment}
const StreamVideoAttachment({
super.key,
required super.message,
required super.attachment,
required this.messageTheme,
super.size,
super.constraints,
this.onShowMessage,
this.onReturnAction,
this.onReplyMessage,
this.onAttachmentTap,
});
/// [StreamMessageThemeData] for showing title
/// The [StreamMessageThemeData] to use for the title
final StreamMessageThemeData messageTheme;
/// Callback when show message is tapped
/// {@macro showMessageCallback}
final ShowMessageCallback? onShowMessage;
/// Callback when attachment is returned to from other screens
final ValueChanged<ReturnActionType>? onReturnAction;
/// {@macro replyMessageCallback}
final ReplyMessageCallback? onReplyMessage;
/// Callback when attachment is tapped
final VoidCallback? onAttachmentTap;
/// {@macro onAttachmentTap}
final OnAttachmentTap? onAttachmentTap;
@override
Widget build(BuildContext context) => source.when(
local: () {
if (attachment.file == null) {
return AttachmentError(size: size);
}
return _buildVideoAttachment(
context,
StreamVideoThumbnailImage(
video: attachment.file!.path!,
height: size?.height,
width: size?.width,
fit: BoxFit.cover,
errorBuilder: (_, __) => AttachmentError(size: size),
),
);
},
network: () {
if (attachment.assetUrl == null) {
return AttachmentError(size: size);
}
return _buildVideoAttachment(
context,
StreamVideoThumbnailImage(
video: attachment.assetUrl!,
height: size?.height,
width: size?.width,
fit: BoxFit.cover,
errorBuilder: (_, __) => AttachmentError(size: size),
),
);
},
);
Widget build(BuildContext context) {
return source.when(
local: () {
if (attachment.file == null) {
return AttachmentError(constraints: constraints);
}
return _buildVideoAttachment(
context,
StreamVideoThumbnailImage(
video: attachment.file!.path!,
constraints: constraints,
fit: BoxFit.cover,
errorBuilder: (_, __) => AttachmentError(constraints: constraints),
),
);
},
network: () {
if (attachment.assetUrl == null) {
return AttachmentError(constraints: constraints);
}
return _buildVideoAttachment(
context,
StreamVideoThumbnailImage(
video: attachment.assetUrl!,
constraints: constraints,
fit: BoxFit.cover,
errorBuilder: (_, __) => AttachmentError(constraints: constraints),
),
);
},
);
}
Widget _buildVideoAttachment(BuildContext context, Widget videoWidget) =>
ConstrainedBox(
constraints: BoxConstraints.loose(size ?? Size.infinite),
child: Column(
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: onAttachmentTap ??
() async {
Widget _buildVideoAttachment(BuildContext context, Widget videoWidget) {
return ConstrainedBox(
constraints: constraints ?? const BoxConstraints.expand(),
child: Column(
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: onAttachmentTap ??
() async {
if (attachment.uploadState == const UploadState.success()) {
final channel = StreamChannel.of(context).channel;
final res = await Navigator.push(
context,
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => StreamChannel(
channel: channel,
child: StreamFullScreenMedia(
child: StreamFullScreenMediaBuilder(
mediaAttachmentPackages:
message.getAttachmentPackageList(),
startIndex:
message.attachments.indexOf(attachment),
userName: message.user?.name,
userName: message.user!.name,
onShowMessage: onShowMessage,
onReplyMessage: onReplyMessage,
),
),
),
);
if (res != null) onReturnAction?.call(res);
},
child: Stack(
children: [
videoWidget,
const Center(
child: Material(
shape: CircleBorder(),
child: Padding(
padding: EdgeInsets.all(16),
child: Icon(Icons.play_arrow),
),
}
},
child: Stack(
children: [
videoWidget,
const Center(
child: Material(
shape: CircleBorder(),
child: Padding(
padding: EdgeInsets.all(16),
child: Icon(Icons.play_arrow),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: StreamAttachmentUploadStateBuilder(
message: message,
attachment: attachment,
),
),
Padding(
padding: const EdgeInsets.all(8),
child: StreamAttachmentUploadStateBuilder(
message: message,
attachment: attachment,
),
],
),
),
],
),
),
if (attachment.title != null)
Material(
color: messageTheme.messageBackgroundColor,
child: StreamAttachmentTitle(
messageTheme: messageTheme,
attachment: attachment,
),
),
],
),
);
),
],
),
);
}
}