* 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
+109 -1
View File
@@ -1,8 +1,116 @@
## 5.0.0
- Included the changes from version [4.5.0](#450).
🐞 Fixed
- [[#1326]](https://github.com/GetStream/stream-chat-flutter/issues/1326) Fixed hitting "enter" on
the android keyboard sends the message instead of going to a new line.
✅ Added
- Added `StreamMemberGridView` and `StreamMemberListView`.
- Added support for additional text field params in `StreamMessageInput`
* `maxLines`
* `minLines`
* `textInputAction`
* `keyboardType`
* `textCapitalization`
- Added `showStreamAttachmentPickerModalBottomSheet` to show the attachment picker modal bottom sheet.
🔄 Changed
- Removed Emoji picker from `StreamMessageInput`.
## 5.0.0-beta.2
- Included the changes from version [4.4.0](#440) and [4.4.1](#441).
🐞 Fixed
- Fixed the unread message header in the message list view.
- Show dialog after clicking on the camera button and permission is denied.
- Fix Jiffy initialization.
- Fix loading to unread position in `StreamMessageListView`.
- Minor fixes and improvements.
🔄 Changed
- [[#1125]](https://github.com/GetStream/stream-chat-flutter/issues/1125) `defaultUserImage`
, `placeholderUserImage`, `reactionIcons`, and `enforceUniqueReactions` have been refactored out
of `StreamChatThemeData` and into the new `StreamChatConfigurationData` class.
✅ Added
- Added `StreamAutocomplete` widget for autocomplete triggers in `StreamMessageInput`.
- Added `StreamMessageInput.customAutocompleteTriggers` to allow users to define their custom
triggers.
## 5.0.0-beta.1
- 🎉 Initial support for desktop 🖥️ and web 🧑‍💻
- Right-click context menus for messages and full-screen attachments
- Upload and download attachments using the native desktop file system
- Press the "enter" key to send a message
- If you are quoting a message and have not yet typed any text, you can press the "esc" key to
remove the quoted message.
- A dedicated "X" button for removing a quoted message with your mouse
- Drag and drop attachment files to `StreamMessageInput`
- New `StreamMessageInput.draggingBorder` property to customize the border color of the
message input when dropping a file.
- Message reactions bubbles
- Hovering over a message reaction will show the users that have reacted to the message
- Desktop attachment sharing UI
- Selectable message text
- Gallery navigation controls with keyboard shortcuts (left and right arrow keys)
- Appropriate message sizing for large screens
- Right-click context menu for `StreamMessageListView` items
- `StreamMessageListView` items not swipeable on desktop & web
- Video support for Windows & Linux through `dart_vlc`
- Video support for macOS through `video_player_macos`
- Replace bottom sheets with dialogs where appropriate
- Other Additions ✅
- `onQuotedMessageCleared` to `StreamMessageInput`
- `selected` and `selectedTileColor` to `StreamChannelListTile`
- `AttachmentUploadStateBuilder.inProgressBuilder` to `AttachmentUploadStateBuilder`
- `AttachmentUploadStateBuilder.successBuilder` to `AttachmentUploadStateBuilder`
- `AttachmentUploadStateBuilder.failedBuilder` to `AttachmentUploadStateBuilder`
- Translations:
- `couldNotReadBytesFromFileError`
- `downloadLabel`
- `toggleMuteUnmuteAction`
- `toggleMuteUnmuteGroupQuestion`
- `toggleMuteUnmuteGroupText`
- `toggleMuteUnmuteUserQuestion`
- `toggleMuteUnmuteUserText`
- Deprecated `showConfirmationDialog` in favor of `showConfirmationBottomSheet`
- Deprecated `showInfoDialog` in favor of `showInfoBottomSheet`
- Deprecated `wrapAttachmentWidget` in favor of the `WrapAttachmentWidget` class
- Breaking changes 🚧
- `StreamImageAttachment.size` has been converted from type `Size` to type `BoxConstraints`
- `StreamFileAttachment.size` has been converted from type `Size` to type `BoxConstraints`
- `StreamGiphyAttachment.size` has been converted from type `Size` to type `BoxConstraints`
- `StreamVideoAttachment.size` has been converted from type `Size` to type `BoxConstraints`
- `StreamVideoThumbnailImage.width` and `StreamVideoThumbnailImage.height` have been removed in
favor of
`StreamVideoThumbnailImage.constraints`
- Dependency updates ⬆️
- `chewie: ^1.3.0` -> `chewie: ^1.3.4`
- `path_provider: ^2.0.1` -> `path_provider: ^2.0.9`
- `video_player: ^2.1.0` -> `video_player: ^2.4.5`
- Code Improvements 🔧
- Extracted many widgets to classes to improve readability, maintainability, and devtools usage.
- Organized internal directory structure
- Extracted typedefs to their own file
- Updated dartdoc documentation
- Various code readability improvements
## 4.6.0
🐞 Fixed
- [[#1323]](https://github.com/GetStream/stream-chat-flutter/issues/1323): Fix message text hiding because of a [flutter bug](https://github.com/flutter/flutter/issues/110628).
- [[#1323]](https://github.com/GetStream/stream-chat-flutter/issues/1323): Fix message text hiding
because of a [flutter bug](https://github.com/flutter/flutter/issues/110628).
## 4.5.0
+20
View File
@@ -60,6 +60,26 @@ We also use [video_player](https://pub.dev/packages/video_player) to reproduce v
To pick images from the camera, we use the [image_picker](https://pub.dev/packages/image_picker) plugin.
Follow [these instructions](https://pub.dev/packages/image_picker#ios) to check the requirements.
### Web
For the web, edit your `index.html` and add the following in the `<body>` tag in order to allow the SDK to override the right-click behaviour:
```html
<body oncontextmenu="return false;">
```
### MacOS
For MacOS use the [file_selector](https://pub.dev/packages/file_selector#macos) package.
Follow [these instructions](https://pub.dev/packages/file_selector#macos) to check the requirements.
You also need to add the following [entitlement](https://docs.flutter.dev/development/platform-integration/desktop#entitlements-and-the-app-sandbox):
```xml
<key>com.apple.security.network.client</key>
<true/>
```
### Troubleshooting
It may happen that you have some problems building the app.
@@ -32,7 +32,6 @@
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
@@ -40,7 +40,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example"
minSdkVersion 22
minSdkVersion 23
targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@@ -6,6 +6,9 @@
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<application
android:name=".Application"
android:label="example"
@@ -1,15 +1,18 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart';
import 'package:responsive_builder/responsive_builder.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_localizations/stream_chat_localizations.dart';
void main() async {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
/// Create a new instance of [StreamChatClient] passing the apikey obtained
/// from your project dashboard.
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
logLevel: Level.OFF,
);
/// Set the current user and connect the websocket. In a production
@@ -63,50 +66,272 @@ class MyApp extends StatelessWidget {
final Channel channel;
@override
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
supportedLocales: const [
Locale('en'),
Locale('hi'),
Locale('fr'),
Locale('it'),
Locale('es'),
],
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
builder: (context, widget) => StreamChat(
client: client,
child: widget,
),
home: StreamChannel(
channel: channel,
child: const ChannelPage(),
),
);
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
// themeMode: ThemeMode.dark,
supportedLocales: const [
Locale('en'),
Locale('hi'),
Locale('fr'),
Locale('it'),
Locale('es'),
],
localizationsDelegates: GlobalStreamChatLocalizations.delegates,
builder: (context, widget) => StreamChat(
client: client,
child: widget,
),
home: StreamChannel(
channel: channel,
child: const ResponsiveChat(),
),
);
}
}
/// A list of messages sent in the current channel.
///
/// This is implemented using [StreamMessageListView],
/// a widget that provides query
/// functionalities fetching the messages from the api and showing them in a
/// listView.
class ChannelPage extends StatelessWidget {
/// Creates the page that shows the list of messages
const ChannelPage({
class ResponsiveChat extends StatelessWidget {
const ResponsiveChat({
super.key,
});
@override
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: StreamMessageListView(),
Widget build(BuildContext context) {
return ResponsiveBuilder(
builder: (context, sizingInformation) {
if (sizingInformation.isMobile) {
return ChannelListPage(
onTap: (c) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return StreamChannel(
channel: c,
child: ChannelPage(
onBackPressed: (context) {
Navigator.of(
context,
rootNavigator: true,
).pop();
},
),
);
},
),
);
},
);
}
return const SplitView();
},
breakpoints: const ScreenBreakpoints(
desktop: 550,
tablet: 550,
watch: 300,
),
);
}
}
class SplitView extends StatefulWidget {
const SplitView({
super.key,
});
@override
_SplitViewState createState() => _SplitViewState();
}
class _SplitViewState extends State<SplitView> {
Channel? selectedChannel;
@override
Widget build(BuildContext context) {
return Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
child: ChannelListPage(
onTap: (channel) {
setState(() {
selectedChannel = channel;
});
},
selectedChannel: selectedChannel,
),
),
Flexible(
flex: 2,
child: ClipPath(
child: Scaffold(
body: selectedChannel != null
? StreamChannel(
key: ValueKey(selectedChannel!.cid),
channel: selectedChannel!,
child: const ChannelPage(showBackButton: false),
)
: Center(
child: Text(
'Pick a channel to show the messages 💬',
style: Theme.of(context).textTheme.headline5,
),
),
),
StreamMessageInput(attachmentLimit: 3),
],
),
),
],
);
}
}
class ChannelListPage extends StatefulWidget {
const ChannelListPage({
super.key,
this.onTap,
this.selectedChannel,
});
final void Function(Channel)? onTap;
final Channel? selectedChannel;
@override
State<ChannelListPage> createState() => _ChannelListPageState();
}
class _ChannelListPageState extends State<ChannelListPage> {
late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
sort: const [SortOption('last_message_at')],
limit: 20,
);
@override
Widget build(BuildContext context) => Scaffold(
body: StreamChannelListView(
onChannelTap: widget.onTap,
controller: _listController,
itemBuilder: (context, channels, index, defaultWidget) {
return defaultWidget.copyWith(
selected: channels[index] == widget.selectedChannel,
);
},
),
);
}
class ChannelPage extends StatefulWidget {
const ChannelPage({
super.key,
this.showBackButton = true,
this.onBackPressed,
});
final bool showBackButton;
final void Function(BuildContext)? onBackPressed;
@override
State<ChannelPage> createState() => _ChannelPageState();
}
class _ChannelPageState extends State<ChannelPage> {
late final messageInputController = StreamMessageInputController();
final focusNode = FocusNode();
@override
Widget build(BuildContext context) => Navigator(
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => Scaffold(
appBar: StreamChannelHeader(
onBackPressed: widget.onBackPressed != null
? () {
widget.onBackPressed!(context);
}
: null,
showBackButton: widget.showBackButton,
),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
onMessageSwiped:
(CurrentPlatform.isAndroid || CurrentPlatform.isIos)
? reply
: null,
threadBuilder: (context, parent) {
return ThreadPage(
parent: parent!,
);
},
messageBuilder:
(context, details, messages, defaultWidget) {
return defaultWidget.copyWith(
onReplyTap: reply,
);
},
),
),
StreamMessageInput(
onQuotedMessageCleared:
messageInputController.clearQuotedMessage,
focusNode: focusNode,
messageInputController: messageInputController,
),
],
),
),
),
);
void reply(Message message) {
messageInputController.quotedMessage = message;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
focusNode.requestFocus();
});
}
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
}
class ThreadPage extends StatelessWidget {
const ThreadPage({
super.key,
required this.parent,
});
final Message parent;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: StreamThreadHeader(
parent: parent,
),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
parentMessage: parent,
),
),
StreamMessageInput(
messageInputController: StreamMessageInputController(
message: Message(parentId: parent.id),
),
),
],
),
);
}
}
@@ -2,7 +2,7 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
void main() async {
Future<void> main() async {
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
@@ -29,13 +29,15 @@ class MyApp extends StatelessWidget {
final StreamChatClient client;
@override
Widget build(BuildContext context) => MaterialApp(
builder: (context, child) => StreamChat(
client: client,
child: child,
),
home: const SplitView(),
);
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) => StreamChat(
client: client,
child: child,
),
home: const SplitView(),
);
}
}
class SplitView extends StatefulWidget {
@@ -51,37 +53,39 @@ class _SplitViewState extends State<SplitView> {
Channel? selectedChannel;
@override
Widget build(BuildContext context) => Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
child: ChannelListPage(
onTap: (channel) {
setState(() {
selectedChannel = channel;
});
},
),
Widget build(BuildContext context) {
return Flex(
direction: Axis.horizontal,
children: <Widget>[
Flexible(
child: ChannelListPage(
onTap: (channel) {
setState(() {
selectedChannel = channel;
});
},
),
Flexible(
flex: 2,
child: Scaffold(
body: selectedChannel != null
? StreamChannel(
key: ValueKey(selectedChannel!.cid),
channel: selectedChannel!,
child: const ChannelPage(),
)
: Center(
child: Text(
'Pick a channel to show the messages 💬',
style: Theme.of(context).textTheme.headline5,
),
),
Flexible(
flex: 2,
child: Scaffold(
body: selectedChannel != null
? StreamChannel(
key: ValueKey(selectedChannel!.cid),
channel: selectedChannel!,
child: const ChannelPage(),
)
: Center(
child: Text(
'Pick a channel to show the messages 💬',
style: Theme.of(context).textTheme.headline5,
),
),
),
),
],
);
),
],
);
}
}
class ChannelListPage extends StatefulWidget {
@@ -1,5 +1,4 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: prefer_expression_function_bodies
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -32,7 +31,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// and [StreamMessageInput]
///
/// If you now run the simulator you will see a single channel UI.
void main() async {
Future<void> main() async {
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
@@ -90,15 +89,18 @@ class ChannelPage extends StatelessWidget {
});
@override
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: StreamMessageListView(),
),
StreamMessageInput(),
],
),
);
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: StreamMessageListView(),
),
StreamMessageInput(),
],
),
);
}
}
@@ -1,6 +1,4 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: prefer_expression_function_bodies
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -31,7 +29,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// [StreamChannelListView] handles pagination
/// and updates automatically when new channels are created or when a new
/// message is added to a channel.
void main() async {
Future<void> main() async {
final client = StreamChatClient(
's2dxdhpxd94g',
logLevel: Level.INFO,
@@ -100,23 +98,25 @@ class _ChannelListPageState extends State<ChannelListPage> {
}
@override
Widget build(BuildContext context) => Scaffold(
body: RefreshIndicator(
onRefresh: _controller.refresh,
child: StreamChannelListView(
controller: _controller,
onChannelTap: (channel) => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
onRefresh: _controller.refresh,
child: StreamChannelListView(
controller: _controller,
onChannelTap: (channel) => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
),
),
),
);
),
);
}
}
class ChannelPage extends StatelessWidget {
@@ -125,15 +125,17 @@ class ChannelPage extends StatelessWidget {
});
@override
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: StreamMessageListView(),
),
StreamMessageInput(),
],
),
);
Widget build(BuildContext context) {
return Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: StreamMessageListView(),
),
StreamMessageInput(),
],
),
);
}
}
@@ -1,5 +1,4 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: prefer_expression_function_bodies
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -1,6 +1,4 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: prefer_expression_function_bodies
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -106,21 +104,23 @@ class ChannelPage extends StatelessWidget {
});
@override
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
),
Widget build(BuildContext context) {
return Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
),
),
const StreamMessageInput(),
],
),
);
),
const StreamMessageInput(),
],
),
);
}
}
class ThreadPage extends StatelessWidget {
@@ -1,5 +1,4 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: prefer_expression_function_bodies
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -86,21 +85,23 @@ class _ChannelListPageState extends State<ChannelListPage> {
}
@override
Widget build(BuildContext context) => Scaffold(
body: StreamChannelListView(
controller: _listController,
onChannelTap: (channel) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
Widget build(BuildContext context) {
return Scaffold(
body: StreamChannelListView(
controller: _listController,
onChannelTap: (channel) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
);
},
),
);
),
);
},
),
);
}
}
class ChannelPage extends StatelessWidget {
@@ -1,6 +1,4 @@
// ignore_for_file: public_member_api_docs
// ignore_for_file: prefer_expression_function_bodies
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -126,21 +124,23 @@ class _ChannelListPageState extends State<ChannelListPage> {
}
@override
Widget build(BuildContext context) => Scaffold(
body: StreamChannelListView(
controller: _listController,
onChannelTap: (channel) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
Widget build(BuildContext context) {
return Scaffold(
body: StreamChannelListView(
controller: _listController,
onChannelTap: (channel) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
);
},
),
);
),
);
},
),
);
}
}
class ChannelPage extends StatelessWidget {
@@ -149,21 +149,23 @@ class ChannelPage extends StatelessWidget {
});
@override
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
),
Widget build(BuildContext context) {
return Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
),
),
const StreamMessageInput(),
],
),
);
),
const StreamMessageInput(),
],
),
);
}
}
class ThreadPage extends StatelessWidget {
@@ -0,0 +1 @@
flutter/ephemeral
@@ -0,0 +1,116 @@
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
set(BINARY_NAME "example")
set(APPLICATION_ID "com.example.example")
cmake_policy(SET CMP0063 NEW)
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Configure build options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
# Flutter library and tool build rules.
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Application build
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
apply_standard_settings(${BINARY_NAME})
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
@@ -0,0 +1,87 @@
cmake_minimum_required(VERSION 3.10)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
# which isn't available in 3.10.
function(list_prepend LIST_NAME PREFIX)
set(NEW_LIST "")
foreach(element ${${LIST_NAME}})
list(APPEND NEW_LIST "${PREFIX}${element}")
endforeach(element)
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
endfunction()
# === Flutter Library ===
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"fl_basic_message_channel.h"
"fl_binary_codec.h"
"fl_binary_messenger.h"
"fl_dart_project.h"
"fl_engine.h"
"fl_json_message_codec.h"
"fl_json_method_codec.h"
"fl_message_codec.h"
"fl_method_call.h"
"fl_method_channel.h"
"fl_method_codec.h"
"fl_method_response.h"
"fl_plugin_registrar.h"
"fl_plugin_registry.h"
"fl_standard_message_codec.h"
"fl_standard_method_codec.h"
"fl_string_codec.h"
"fl_value.h"
"fl_view.h"
"flutter_linux.h"
)
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
target_link_libraries(flutter INTERFACE
PkgConfig::GTK
PkgConfig::GLIB
PkgConfig::GIO
)
add_dependencies(flutter flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/_phony_
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
)
@@ -0,0 +1,35 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <dart_vlc/dart_vlc_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dart_vlc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DartVlcPlugin");
dart_vlc_plugin_register_with_registrar(dart_vlc_registrar);
g_autoptr(FlPluginRegistrar) desktop_drop_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin");
desktop_drop_plugin_register_with_registrar(desktop_drop_registrar);
g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
g_autoptr(FlPluginRegistrar) window_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
window_manager_plugin_register_with_registrar(window_manager_registrar);
}
@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_
@@ -0,0 +1,29 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
dart_vlc
desktop_drop
screen_retriever
sqlite3_flutter_libs
url_launcher_linux
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
@@ -0,0 +1,6 @@
#include "my_application.h"
int main(int argc, char** argv) {
g_autoptr(MyApplication) app = my_application_new();
return g_application_run(G_APPLICATION(app), argc, argv);
}
@@ -0,0 +1,104 @@
#include "my_application.h"
#include <flutter_linux/flutter_linux.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#include "flutter/generated_plugin_registrant.h"
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
// Use a header bar when running in GNOME as this is the common style used
// by applications and is the setup most users will be using (e.g. Ubuntu
// desktop).
// If running on X and not using GNOME then just use a traditional title bar
// in case the window manager does more exotic layout, e.g. tiling.
// If running on Wayland assume the header bar will work (may need changing
// if future cases occur).
gboolean use_header_bar = TRUE;
#ifdef GDK_WINDOWING_X11
GdkScreen* screen = gtk_window_get_screen(window);
if (GDK_IS_X11_SCREEN(screen)) {
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
use_header_bar = FALSE;
}
}
#endif
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "example");
gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "example");
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
}
// Implements GApplication::local_command_line.
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
MyApplication* self = MY_APPLICATION(application);
// Strip out the first argument as it is the binary name.
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
g_autoptr(GError) error = nullptr;
if (!g_application_register(application, nullptr, &error)) {
g_warning("Failed to register: %s", error->message);
*exit_status = 1;
return TRUE;
}
g_application_activate(application);
*exit_status = 0;
return TRUE;
}
// Implements GObject::dispose.
static void my_application_dispose(GObject* object) {
MyApplication* self = MY_APPLICATION(object);
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
}
static void my_application_class_init(MyApplicationClass* klass) {
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
}
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
nullptr));
}
@@ -0,0 +1,18 @@
#ifndef FLUTTER_MY_APPLICATION_H_
#define FLUTTER_MY_APPLICATION_H_
#include <gtk/gtk.h>
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
GtkApplication)
/**
* my_application_new:
*
* Creates a new Flutter-based application.
*
* Returns: a new #MyApplication.
*/
MyApplication* my_application_new();
#endif // FLUTTER_MY_APPLICATION_H_
@@ -10,5 +10,9 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
@@ -28,5 +28,7 @@
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>In order to access your photo library</string>
</dict>
</plist>
@@ -4,12 +4,12 @@ import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController.init()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}
}
@@ -24,9 +24,10 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
collection: ^1.15.0
cupertino_icons: ^1.0.3
cupertino_icons: ^1.0.4
flutter:
sdk: flutter
responsive_builder: ^0.4.2
stream_chat_flutter:
path: ../
stream_chat_localizations:
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -11,7 +11,6 @@
Fore more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
-->
<base href="/">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
@@ -31,7 +30,7 @@
<title>example</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<body oncontextmenu="return false;">
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
@@ -0,0 +1,17 @@
flutter/ephemeral/
# Visual Studio user-specific files.
*.suo
*.user
*.userosscache
*.sln.docstates
# Visual Studio build-related files.
x64/
x86/
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
@@ -0,0 +1,95 @@
cmake_minimum_required(VERSION 3.14)
project(example LANGUAGES CXX)
set(BINARY_NAME "example")
cmake_policy(SET CMP0063 NEW)
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Configure build options.
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(IS_MULTICONFIG)
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
CACHE STRING "" FORCE)
else()
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
endif()
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
# Use Unicode for all projects.
add_definitions(-DUNICODE -D_UNICODE)
# Compilation settings that should be applied to most targets.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_17)
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
target_compile_options(${TARGET} PRIVATE /EHsc)
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
endfunction()
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
# Flutter library and tool build rules.
add_subdirectory(${FLUTTER_MANAGED_DIR})
# Application build
add_subdirectory("runner")
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# Support files are copied into place next to the executable, so that it can
# run in place. This is done instead of making a separate bundle (as on Linux)
# so that building and running from within Visual Studio will work.
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# Make the "install" step default, as it's required to run.
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
if(PLUGIN_BUNDLED_LIBRARIES)
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
CONFIGURATIONS Profile;Release
COMPONENT Runtime)
@@ -0,0 +1,103 @@
cmake_minimum_required(VERSION 3.14)
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
# Configuration provided via flutter tool.
include(${EPHEMERAL_DIR}/generated_config.cmake)
# TODO: Move the rest of this into files in ephemeral. See
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
# Published to parent scope for install step.
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
list(APPEND FLUTTER_LIBRARY_HEADERS
"flutter_export.h"
"flutter_windows.h"
"flutter_messenger.h"
"flutter_plugin_registrar.h"
"flutter_texture_registrar.h"
)
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
add_library(flutter INTERFACE)
target_include_directories(flutter INTERFACE
"${EPHEMERAL_DIR}"
)
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
add_dependencies(flutter flutter_assemble)
# === Wrapper ===
list(APPEND CPP_WRAPPER_SOURCES_CORE
"core_implementations.cc"
"standard_codec.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
"plugin_registrar.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
list(APPEND CPP_WRAPPER_SOURCES_APP
"flutter_engine.cc"
"flutter_view_controller.cc"
)
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
# Wrapper sources needed for a plugin.
add_library(flutter_wrapper_plugin STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
)
apply_standard_settings(flutter_wrapper_plugin)
set_target_properties(flutter_wrapper_plugin PROPERTIES
POSITION_INDEPENDENT_CODE ON)
set_target_properties(flutter_wrapper_plugin PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
target_include_directories(flutter_wrapper_plugin PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_plugin flutter_assemble)
# Wrapper sources needed for the runner.
add_library(flutter_wrapper_app STATIC
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_APP}
)
apply_standard_settings(flutter_wrapper_app)
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
target_include_directories(flutter_wrapper_app PUBLIC
"${WRAPPER_ROOT}/include"
)
add_dependencies(flutter_wrapper_app flutter_assemble)
# === Flutter tool backend ===
# _phony_ is a non-existent file to force this command to run every time,
# since currently there's no way to get a full input/output list from the
# flutter tool.
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
add_custom_command(
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
${PHONY_OUTPUT}
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
windows-x64 $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)
@@ -0,0 +1,41 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <connectivity_plus_windows/connectivity_plus_windows_plugin.h>
#include <dart_vlc/dart_vlc_plugin.h>
#include <desktop_drop/desktop_drop_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_native_view/flutter_native_view_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <thumblr_windows/thumblr_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
DartVlcPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DartVlcPlugin"));
DesktopDropPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterNativeViewPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterNativeViewPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
ThumblrWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ThumblrWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WindowManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
}
@@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_
@@ -0,0 +1,33 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus_windows
dart_vlc
desktop_drop
file_selector_windows
flutter_native_view
screen_retriever
sqlite3_flutter_libs
thumblr_windows
url_launcher_windows
window_manager
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
foreach(plugin ${FLUTTER_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
endforeach(ffi_plugin)
@@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.14)
project(runner LANGUAGES CXX)
add_executable(${BINARY_NAME} WIN32
"flutter_window.cpp"
"main.cpp"
"utils.cpp"
"win32_window.cpp"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
"Runner.rc"
"runner.exe.manifest"
)
apply_standard_settings(${BINARY_NAME})
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
add_dependencies(${BINARY_NAME} flutter_assemble)
@@ -0,0 +1,121 @@
// Microsoft Visual C++ generated resource script.
//
#pragma code_page(65001)
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APP_ICON ICON "resources\\app_icon.ico"
/////////////////////////////////////////////////////////////////////////////
//
// Version
//
#ifdef FLUTTER_BUILD_NUMBER
#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
#else
#define VERSION_AS_NUMBER 1,0,0
#endif
#ifdef FLUTTER_BUILD_NAME
#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
#else
#define VERSION_AS_STRING "1.0.0"
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_AS_NUMBER
PRODUCTVERSION VERSION_AS_NUMBER
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904e4"
BEGIN
VALUE "CompanyName", "com.example" "\0"
VALUE "FileDescription", "example" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "example" "\0"
VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0"
VALUE "OriginalFilename", "example.exe" "\0"
VALUE "ProductName", "example" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED
@@ -0,0 +1,61 @@
#include "flutter_window.h"
#include <optional>
#include "flutter/generated_plugin_registrant.h"
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
: project_(project) {}
FlutterWindow::~FlutterWindow() {}
bool FlutterWindow::OnCreate() {
if (!Win32Window::OnCreate()) {
return false;
}
RECT frame = GetClientArea();
// The size here must match the window dimensions to avoid unnecessary surface
// creation / destruction in the startup path.
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
frame.right - frame.left, frame.bottom - frame.top, project_);
// Ensure that basic setup of the controller was successful.
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
return false;
}
RegisterPlugins(flutter_controller_->engine());
SetChildContent(flutter_controller_->view()->GetNativeWindow());
return true;
}
void FlutterWindow::OnDestroy() {
if (flutter_controller_) {
flutter_controller_ = nullptr;
}
Win32Window::OnDestroy();
}
LRESULT
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
// Give Flutter, including plugins, an opportunity to handle window messages.
if (flutter_controller_) {
std::optional<LRESULT> result =
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
lparam);
if (result) {
return *result;
}
}
switch (message) {
case WM_FONTCHANGE:
flutter_controller_->engine()->ReloadSystemFonts();
break;
}
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
}
@@ -0,0 +1,33 @@
#ifndef RUNNER_FLUTTER_WINDOW_H_
#define RUNNER_FLUTTER_WINDOW_H_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
#include "win32_window.h"
// A window that does nothing but host a Flutter view.
class FlutterWindow : public Win32Window {
public:
// Creates a new FlutterWindow hosting a Flutter view running |project|.
explicit FlutterWindow(const flutter::DartProject& project);
virtual ~FlutterWindow();
protected:
// Win32Window:
bool OnCreate() override;
void OnDestroy() override;
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
LPARAM const lparam) noexcept override;
private:
// The project to run.
flutter::DartProject project_;
// The Flutter instance hosted by this window.
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
};
#endif // RUNNER_FLUTTER_WINDOW_H_
@@ -0,0 +1,43 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#include "flutter_window.h"
#include "utils.h"
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
CreateAndAttachConsole();
}
// Initialize COM, so that it is available for use in the library and/or
// plugins.
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
flutter::DartProject project(L"data");
std::vector<std::string> command_line_arguments =
GetCommandLineArguments();
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project);
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.CreateAndShow(L"example", origin, size)) {
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
::MSG msg;
while (::GetMessage(&msg, nullptr, 0, 0)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
::CoUninitialize();
return EXIT_SUCCESS;
}
@@ -0,0 +1,16 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Runner.rc
//
#define IDI_APP_ICON 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
</application>
</compatibility>
</assembly>
@@ -0,0 +1,64 @@
#include "utils.h"
#include <flutter_windows.h>
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <iostream>
void CreateAndAttachConsole() {
if (::AllocConsole()) {
FILE *unused;
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
_dup2(_fileno(stdout), 1);
}
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
_dup2(_fileno(stdout), 2);
}
std::ios::sync_with_stdio();
FlutterDesktopResyncOutputStreams();
}
}
std::vector<std::string> GetCommandLineArguments() {
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
int argc;
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
if (argv == nullptr) {
return std::vector<std::string>();
}
std::vector<std::string> command_line_arguments;
// Skip the first argument as it's the binary name.
for (int i = 1; i < argc; i++) {
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
}
::LocalFree(argv);
return command_line_arguments;
}
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
if (utf16_string == nullptr) {
return std::string();
}
int target_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, nullptr, 0, nullptr, nullptr);
if (target_length == 0) {
return std::string();
}
std::string utf8_string;
utf8_string.resize(target_length);
int converted_length = ::WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
-1, utf8_string.data(),
target_length, nullptr, nullptr);
if (converted_length == 0) {
return std::string();
}
return utf8_string;
}
@@ -0,0 +1,19 @@
#ifndef RUNNER_UTILS_H_
#define RUNNER_UTILS_H_
#include <string>
#include <vector>
// Creates a console for the process, and redirects stdout and stderr to
// it for both the runner and the Flutter library.
void CreateAndAttachConsole();
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
// encoded in UTF-8. Returns an empty std::string on failure.
std::string Utf8FromUtf16(const wchar_t* utf16_string);
// Gets the command line arguments passed in as a std::vector<std::string>,
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments();
#endif // RUNNER_UTILS_H_
@@ -0,0 +1,245 @@
#include "win32_window.h"
#include <flutter_windows.h>
#include "resource.h"
namespace {
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
// The number of Win32Window objects that currently exist.
static int g_active_window_count = 0;
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
// Scale helper to convert logical scaler values to physical using passed in
// scale factor
int Scale(int source, double scale_factor) {
return static_cast<int>(source * scale_factor);
}
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
// This API is only needed for PerMonitor V1 awareness mode.
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
HMODULE user32_module = LoadLibraryA("User32.dll");
if (!user32_module) {
return;
}
auto enable_non_client_dpi_scaling =
reinterpret_cast<EnableNonClientDpiScaling*>(
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
if (enable_non_client_dpi_scaling != nullptr) {
enable_non_client_dpi_scaling(hwnd);
FreeLibrary(user32_module);
}
}
} // namespace
// Manages the Win32Window's window class registration.
class WindowClassRegistrar {
public:
~WindowClassRegistrar() = default;
// Returns the singleton registar instance.
static WindowClassRegistrar* GetInstance() {
if (!instance_) {
instance_ = new WindowClassRegistrar();
}
return instance_;
}
// Returns the name of the window class, registering the class if it hasn't
// previously been registered.
const wchar_t* GetWindowClass();
// Unregisters the window class. Should only be called if there are no
// instances of the window.
void UnregisterWindowClass();
private:
WindowClassRegistrar() = default;
static WindowClassRegistrar* instance_;
bool class_registered_ = false;
};
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
const wchar_t* WindowClassRegistrar::GetWindowClass() {
if (!class_registered_) {
WNDCLASS window_class{};
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = kWindowClassName;
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.hIcon =
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
window_class.hbrBackground = 0;
window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = Win32Window::WndProc;
RegisterClass(&window_class);
class_registered_ = true;
}
return kWindowClassName;
}
void WindowClassRegistrar::UnregisterWindowClass() {
UnregisterClass(kWindowClassName, nullptr);
class_registered_ = false;
}
Win32Window::Win32Window() {
++g_active_window_count;
}
Win32Window::~Win32Window() {
--g_active_window_count;
Destroy();
}
bool Win32Window::CreateAndShow(const std::wstring& title,
const Point& origin,
const Size& size) {
Destroy();
const wchar_t* window_class =
WindowClassRegistrar::GetInstance()->GetWindowClass();
const POINT target_point = {static_cast<LONG>(origin.x),
static_cast<LONG>(origin.y)};
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
double scale_factor = dpi / 96.0;
HWND window = CreateWindow(
window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
nullptr, nullptr, GetModuleHandle(nullptr), this);
if (!window) {
return false;
}
return OnCreate();
}
// static
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
if (message == WM_NCCREATE) {
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
EnableFullDpiSupportIfAvailable(window);
that->window_handle_ = window;
} else if (Win32Window* that = GetThisFromHandle(window)) {
return that->MessageHandler(window, message, wparam, lparam);
}
return DefWindowProc(window, message, wparam, lparam);
}
LRESULT
Win32Window::MessageHandler(HWND hwnd,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
switch (message) {
case WM_DESTROY:
window_handle_ = nullptr;
Destroy();
if (quit_on_close_) {
PostQuitMessage(0);
}
return 0;
case WM_DPICHANGED: {
auto newRectSize = reinterpret_cast<RECT*>(lparam);
LONG newWidth = newRectSize->right - newRectSize->left;
LONG newHeight = newRectSize->bottom - newRectSize->top;
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
return 0;
}
case WM_SIZE: {
RECT rect = GetClientArea();
if (child_content_ != nullptr) {
// Size and position the child window.
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, TRUE);
}
return 0;
}
case WM_ACTIVATE:
if (child_content_ != nullptr) {
SetFocus(child_content_);
}
return 0;
}
return DefWindowProc(window_handle_, message, wparam, lparam);
}
void Win32Window::Destroy() {
OnDestroy();
if (window_handle_) {
DestroyWindow(window_handle_);
window_handle_ = nullptr;
}
if (g_active_window_count == 0) {
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
}
}
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
return reinterpret_cast<Win32Window*>(
GetWindowLongPtr(window, GWLP_USERDATA));
}
void Win32Window::SetChildContent(HWND content) {
child_content_ = content;
SetParent(content, window_handle_);
RECT frame = GetClientArea();
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
frame.bottom - frame.top, true);
SetFocus(child_content_);
}
RECT Win32Window::GetClientArea() {
RECT frame;
GetClientRect(window_handle_, &frame);
return frame;
}
HWND Win32Window::GetHandle() {
return window_handle_;
}
void Win32Window::SetQuitOnClose(bool quit_on_close) {
quit_on_close_ = quit_on_close;
}
bool Win32Window::OnCreate() {
// No-op; provided for subclasses.
return true;
}
void Win32Window::OnDestroy() {
// No-op; provided for subclasses.
}
@@ -0,0 +1,98 @@
#ifndef RUNNER_WIN32_WINDOW_H_
#define RUNNER_WIN32_WINDOW_H_
#include <windows.h>
#include <functional>
#include <memory>
#include <string>
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
// inherited from by classes that wish to specialize with custom
// rendering and input handling
class Win32Window {
public:
struct Point {
unsigned int x;
unsigned int y;
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
};
struct Size {
unsigned int width;
unsigned int height;
Size(unsigned int width, unsigned int height)
: width(width), height(height) {}
};
Win32Window();
virtual ~Win32Window();
// Creates and shows a win32 window with |title| and position and size using
// |origin| and |size|. New windows are created on the default monitor. Window
// sizes are specified to the OS in physical pixels, hence to ensure a
// consistent size to will treat the width height passed in to this function
// as logical pixels and scale to appropriate for the default monitor. Returns
// true if the window was created successfully.
bool CreateAndShow(const std::wstring& title,
const Point& origin,
const Size& size);
// Release OS resources associated with window.
void Destroy();
// Inserts |content| into the window tree.
void SetChildContent(HWND content);
// Returns the backing Window handle to enable clients to set icon and other
// window properties. Returns nullptr if the window has been destroyed.
HWND GetHandle();
// If true, closing this window will quit the application.
void SetQuitOnClose(bool quit_on_close);
// Return a RECT representing the bounds of the current client area.
RECT GetClientArea();
protected:
// Processes and route salient window messages for mouse handling,
// size change and DPI. Delegates handling of these to member overloads that
// inheriting classes can handle.
virtual LRESULT MessageHandler(HWND window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Called when CreateAndShow is called, allowing subclass window-related
// setup. Subclasses should return false if setup fails.
virtual bool OnCreate();
// Called when Destroy is called.
virtual void OnDestroy();
private:
friend class WindowClassRegistrar;
// OS callback called by message pump. Handles the WM_NCCREATE message which
// is passed when the non-client area is being created and enables automatic
// non-client DPI scaling so that the non-client area automatically
// responsponds to changes in DPI. All other messages are handled by
// MessageHandler.
static LRESULT CALLBACK WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept;
// Retrieves a class instance pointer for |window|
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
bool quit_on_close_ = false;
// window handle for top level window.
HWND window_handle_ = nullptr;
// window handle for hosted content.
HWND child_content_ = nullptr;
};
#endif // RUNNER_WIN32_WINDOW_H_
@@ -0,0 +1,25 @@
# conditional_parent_builder
A widget that allows developers to conditionally wrap a child widget with a parent widget.
This is useful in situations where a child widget should always be built, but should be wrapped
by another widget if certain conditions are met.
In the following real-world example, we conditionally wrap the child widget
tree with a context menu if the message is not deleted:
```dart
ConditionalParentBuilder(
builder: (context, child) {
if (!widget.message.isDeleted) {
return ContextMenuArea(
builder: (context) => buildContextMenu(),
child: child,
);
} else {
return child;
}
},
child: Material(...),
),
```
This example can be found in the `stream_chat_flutter` source code under
`src/message_widget/message_widget.dart`.
@@ -0,0 +1,55 @@
library conditional_parent_builder;
import 'package:flutter/material.dart';
/// {@template parentBuilder}
/// A function that provides the [BuildContext] and the [child] widget.
/// {@endtemplate}
typedef ParentBuilder = Widget Function(
BuildContext context,
Widget child,
);
/// {@template conditionalParentBuilder}
/// A widget that allows developers to conditionally wrap the [child] widget
/// with a parent widget.
///
/// In the following real-world example, we conditionally wrap the child widget
/// tree with a context menu if the message is not deleted:
/// ```dart
/// ConditionalParentBuilder(
/// builder: (context, child) {
/// if (!widget.message.isDeleted) {
/// return ContextMenuArea(
/// builder: (context) => buildContextMenu(),
/// child: child,
/// );
/// } else {
/// return child;
/// }
/// },
/// child: Material(...),
/// ),
/// ```
/// This example can be found in the `stream_chat_flutter` source code under
/// `src/message_widget/message_widget.dart`.
/// {@endtemplate}
class ConditionalParentBuilder extends StatelessWidget {
/// {@macro conditionalParentBuilder}
const ConditionalParentBuilder({
super.key,
required this.builder,
required this.child,
});
/// {@macro parentBuilder}
final ParentBuilder builder;
/// The child widget to build.
final Widget child;
@override
Widget build(BuildContext context) {
return builder.call(context, child);
}
}
@@ -0,0 +1,28 @@
# platform_widget_builder
This package is a more specialized version of the [flutter_platform_widgets](https://pub.dev/packages/flutter_platform_widgets) package.
It provides two specialized widget builders:
* `PlatformWidgetBuilder`
* `DesktopWidgetBuilder`
### `PlatformWidgetBuilder`
This widget differs from the `PlatformWidgetBuilder` found in the `flutter_platform_widgets` package in that it
provides three builders:
* `mobile`
* `desktop`
* `web`
This allows developers to build different widgets for their generalized platform targets
(Android + iOS = mobile, macOS + Windows + Linux = desktop). This is advantageous when requirements call for one set
of widgets that are identical across mobile, a different set of widgets that are identical across desktop, and another
that are required for web.
### `DesktopWidgetBuilder`
This widget is more specialized than `PlatformWidgetBuilder` in that it allows for more targeted widget-building for
desktop platforms. It provides three builders:
* `macOS`
* `windows`
* `linux`
This allows developers to build different widgets for the various desktop platforms. This is advantageous when building
native-looking desktop applications using the `macos_ui`, `fluent_ui`, and `yaru_widgets` packages.
@@ -0,0 +1,4 @@
library platform_widget_builder;
export 'src/desktop_widget_builder.dart';
export 'src/platform_widget_builder.dart';
@@ -0,0 +1,38 @@
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/platform_widget_builder/src/desktop_widget_base.dart';
/// A widget that will only be built for the specified desktop Platforms.
///
/// See [DesktopWidgetBuilder] and [DesktopWidgetBase] for more.
///
/// Also see: [PlatformWidget] and [PlatformWidgetBase].
class DesktopWidget extends DesktopWidgetBase<Widget, Widget, Widget> {
/// Builds a [DesktopWidget].
const DesktopWidget({
super.key,
this.macOS,
this.windows,
this.linux,
});
/// The widget to build for macOS.
final PlatformBuilder<Widget?>? macOS;
/// The widget to build for Windows.
final PlatformBuilder<Widget?>? windows;
/// The widget to build for Linux.
final PlatformBuilder<Widget?>? linux;
@override
Widget createMacosWidget(BuildContext context) =>
macOS?.call(context) ?? const SizedBox.shrink();
@override
Widget createWindowsWidget(BuildContext context) =>
windows?.call(context) ?? const SizedBox.shrink();
@override
Widget createLinuxWidget(BuildContext context) =>
linux?.call(context) ?? const SizedBox.shrink();
}
@@ -0,0 +1,53 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show Theme;
import 'package:flutter/widgets.dart';
/// A generic widget builder function.
typedef PlatformBuilder<T> = T Function(
BuildContext context,
);
/// An abstract class used as a building block for creating
/// [DesktopPlatformWidget]s.
///
/// This class is similar to [PlatformWidgetBase], which broadly covers
/// the platform categories; it combines the desktop platforms into a single
/// "desktop" target, for the purpose of returning the same widget one time
/// for all platforms in that category. [DesktopWidgetBase] differs in that
/// it more specifically targets each platform in the "desktop" category.
///
/// This class utilizes generics to define the types of widgets it expects to
/// build:
/// * M = macOS
/// * W = Windows
/// * L = Linux
abstract class DesktopWidgetBase<M extends Widget, W extends Widget,
L extends Widget> extends StatelessWidget {
/// Builds a [DesktopWidgetBase].
const DesktopWidgetBase({super.key});
@override
Widget build(BuildContext context) {
final platform = Theme.of(context).platform;
if (platform == TargetPlatform.macOS) {
return createMacosWidget(context);
} else if (platform == TargetPlatform.windows) {
return createWindowsWidget(context);
} else if (platform == TargetPlatform.linux) {
return createLinuxWidget(context);
}
return throw UnsupportedError(
'This platform is not supported: $defaultTargetPlatform',
);
}
/// Builds a `M` macOS widget.
M createMacosWidget(BuildContext context);
/// Builds a `W` Windows widget.
W createWindowsWidget(BuildContext context);
/// Builds a `L` Linux widget.
L createLinuxWidget(BuildContext context);
}
@@ -0,0 +1,51 @@
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/platform_widget_builder/src/desktop_widget.dart';
/// A widget-building function that includes the child widget.
typedef DesktopTargetBuilder = Widget? Function(
BuildContext context,
Widget? child,
)?;
/// A widget that utilizes [DesktopWidgetBuilder]s to build different widgets
/// for each specified desktop platform.
///
/// Usage:
/// ```
/// DesktopWidgetBuilder(
/// macOS: (context, child) => MacosWidget(),
/// windows: (context, child) => WindowsWidget(),
/// linux: (context, child) => LinuxWidget(),
/// ),
/// ```
class DesktopWidgetBuilder extends StatelessWidget {
/// Builds a [DesktopWidgetBuilder].
const DesktopWidgetBuilder({
super.key,
this.child,
this.macOS,
this.windows,
this.linux,
});
/// The child widget.
final Widget? child;
/// The widget to build for macOS.
final DesktopTargetBuilder? macOS;
/// The widget to build for windows.
final DesktopTargetBuilder? windows;
/// The widget to build for linux.
final DesktopTargetBuilder? linux;
@override
Widget build(BuildContext context) {
return DesktopWidget(
macOS: (context) => macOS?.call(context, child),
windows: (context) => windows?.call(context, child),
linux: (context) => linux?.call(context, child),
);
}
}
@@ -0,0 +1,38 @@
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget_base.dart';
/// A widget that will only be built for the specific Platforms:
///
/// See [PlatformWidgetBuilder] and [PlatformWidgetBase] for more.
///
/// Also see: [DesktopWidget] and [DesktopWidgetBase].
class PlatformWidget extends PlatformWidgetBase<Widget, Widget, Widget> {
/// Builds a [PlatformWidget].
const PlatformWidget({
super.key,
this.desktop,
this.mobile,
this.web,
});
/// The mobile widget to build.
final PlatformBuilder<Widget?>? mobile;
/// The desktop widget to build.
final PlatformBuilder<Widget?>? desktop;
/// The web widget to build.
final PlatformBuilder<Widget?>? web;
@override
Widget createDesktopWidget(BuildContext context) =>
desktop?.call(context) ?? const SizedBox.shrink();
@override
Widget createMobileWidget(BuildContext context) =>
mobile?.call(context) ?? const SizedBox.shrink();
@override
Widget createWebWidget(BuildContext context) =>
web?.call(context) ?? const SizedBox.shrink();
}
@@ -0,0 +1,53 @@
import 'package:flutter/material.dart' show Theme;
import 'package:flutter/widgets.dart';
/// A generic widget builder function.
typedef PlatformBuilder<T> = T Function(
BuildContext context,
);
/// An abstract class used as a building block for creating [PlatformWidget]s.
///
/// This class broadly covers the platforms by combining Android and iOS
/// together as a "mobile" category, macOS, Windows, and Linux together as a
/// "desktop" category. This is useful is cases where a widget is expected to
/// be the same for the various mobile and desktop categories, and would
/// therefore be tedious to return the same widget more than once for the
/// specified category. This is unlike [DesktopWidgetBase], which more
/// specifically targets the various platforms in the "desktop" category.
///
/// This class utilizes generics to define the types of widgets it expects to
/// build:
/// * M = Mobile
/// * D = Desktop
/// * W = Web
abstract class PlatformWidgetBase<M extends Widget, D extends Widget,
W extends Widget> extends StatelessWidget {
/// Builds a [PlatformWidgetBase].
const PlatformWidgetBase({
super.key,
});
@override
Widget build(BuildContext context) {
final platform = Theme.of(context).platform;
if (platform == TargetPlatform.android || platform == TargetPlatform.iOS) {
return createMobileWidget(context);
} else if (platform == TargetPlatform.macOS ||
platform == TargetPlatform.windows ||
platform == TargetPlatform.linux) {
return createDesktopWidget(context);
} else {
return createWebWidget(context);
}
}
/// Builds a `M` mobile widget.
M createMobileWidget(BuildContext context);
/// Builds a `D` desktop widget.
D createDesktopWidget(BuildContext context);
/// Builds a `W` web widget.
W createWebWidget(BuildContext context);
}
@@ -0,0 +1,50 @@
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/platform_widget_builder/src/platform_widget.dart';
/// A widget-building function that includes the child widget.
typedef PlatformTargetBuilder = Widget? Function(
BuildContext context,
Widget? child,
)?;
/// A widget that utilizes [PlatformTargetBuilder]s to build different widgets
/// for each specified platform.
///
/// In the following real-world example, only the `mobile` builder is used
/// to ensure the child widget is only built for Android and iOS:
/// ```
/// PlatformWidgetBuilder(
/// mobile: (context, child) => _buildFilePickerSection(),
/// ),
/// ```
class PlatformWidgetBuilder extends StatelessWidget {
/// Builds a [PlatformWidgetBuilder].
const PlatformWidgetBuilder({
super.key,
this.child,
this.mobile,
this.desktop,
this.web,
});
/// The child widget.
final Widget? child;
/// The widget to build for mobile platforms.
final PlatformTargetBuilder? mobile;
/// The widget to build for desktop platforms.
final PlatformTargetBuilder? desktop;
/// The widget to build for web platforms.
final PlatformTargetBuilder? web;
@override
Widget build(BuildContext context) {
return PlatformWidget(
desktop: (context) => desktop?.call(context, child),
mobile: (context) => mobile?.call(context, child),
web: (context) => web?.call(context, child),
);
}
}
@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scrollable_positioned_list.dart';
@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart';
/// Internal implementation of [ItemPositionsListener].
@@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/element_registry.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/indexed_key.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart';
@@ -5,7 +5,6 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/viewport.dart';
/// {@template custom_scroll_view}
@@ -8,7 +8,6 @@ import 'dart:math';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/positioned_list.dart';
@@ -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,
);
}
}
@@ -13,6 +13,6 @@ class StreamAttachmentPackage {
final Attachment attachment;
/// This is the message that the attachment belongs to
/// The message object may have attachemnt(s) other than the one packaged
/// 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,
),
),
],
),
);
),
],
),
);
}
}
@@ -1,20 +1,7 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.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';
/// Callback to download an attachment asset
typedef AttachmentDownloader = Future<String> Function(
Attachment attachment, {
ProgressCallback? progressCallback,
DownloadedPathCallback? downloadedPathCallback,
});
/// Callback to receive the path once the attachment asset is downloaded
typedef DownloadedPathCallback = void Function(String? path);
/// Widget that shows the options in the gallery view
class AttachmentActionsModal extends StatelessWidget {
/// Returns a new [AttachmentActionsModal]
@@ -23,8 +10,8 @@ class AttachmentActionsModal extends StatelessWidget {
required this.attachment,
required this.message,
this.onShowMessage,
this.imageDownloader,
this.fileDownloader,
this.onReply,
this.attachmentDownloader,
this.showReply = true,
this.showShowInChat = true,
this.showSave = true,
@@ -41,11 +28,11 @@ class AttachmentActionsModal extends StatelessWidget {
/// Callback to show the message
final VoidCallback? onShowMessage;
/// Callback to download images
final AttachmentDownloader? imageDownloader;
/// Callback to reply the message
final VoidCallback? onReply;
/// Callback to provide download files
final AttachmentDownloader? fileDownloader;
/// Callback to download [attachment].
final AttachmentDownloader? attachmentDownloader;
/// Show reply option
final bool showReply;
@@ -69,34 +56,35 @@ class AttachmentActionsModal extends StatelessWidget {
Attachment? attachment,
Message? message,
VoidCallback? onShowMessage,
AttachmentDownloader? imageDownloader,
AttachmentDownloader? fileDownloader,
AttachmentDownloader? attachmentDownloader,
bool? showReply,
bool? showShowInChat,
bool? showSave,
bool? showDelete,
List<AttachmentAction>? customActions,
}) =>
AttachmentActionsModal(
key: key ?? this.key,
attachment: attachment ?? this.attachment,
message: message ?? this.message,
onShowMessage: onShowMessage ?? this.onShowMessage,
imageDownloader: imageDownloader ?? this.imageDownloader,
fileDownloader: fileDownloader ?? this.fileDownloader,
showReply: showReply ?? this.showReply,
showShowInChat: showShowInChat ?? this.showShowInChat,
showSave: showSave ?? this.showSave,
showDelete: showDelete ?? this.showDelete,
customActions: customActions ?? this.customActions,
);
}) {
return AttachmentActionsModal(
key: key ?? this.key,
attachment: attachment ?? this.attachment,
message: message ?? this.message,
onShowMessage: onShowMessage ?? this.onShowMessage,
attachmentDownloader: attachmentDownloader ?? this.attachmentDownloader,
showReply: showReply ?? this.showReply,
showShowInChat: showShowInChat ?? this.showShowInChat,
showSave: showSave ?? this.showSave,
showDelete: showDelete ?? this.showDelete,
customActions: customActions ?? this.customActions,
);
}
@override
Widget build(BuildContext context) => GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => Navigator.maybePop(context),
child: _buildPage(context),
);
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => Navigator.of(context).maybePop(),
child: _buildPage(context),
);
}
Widget _buildPage(BuildContext context) {
final theme = StreamChatTheme.of(context);
@@ -125,9 +113,7 @@ class AttachmentActionsModal extends StatelessWidget {
size: 24,
color: theme.colorTheme.textLowEmphasis,
),
() {
Navigator.pop(context, ReturnActionType.reply);
},
() => Navigator.of(context).pop(ReturnActionType.reply),
),
if (showShowInChat)
_buildButton(
@@ -150,46 +136,44 @@ class AttachmentActionsModal extends StatelessWidget {
color: theme.colorTheme.textLowEmphasis,
),
() {
final isImage = attachment.type == 'image';
final Future<String?> Function(
Attachment, {
void Function(int, int) progressCallback,
DownloadedPathCallback downloadedPathCallback,
}) saveFile = fileDownloader ?? _downloadAttachment;
final Future<String?> Function(
Attachment, {
void Function(int, int) progressCallback,
DownloadedPathCallback downloadedPathCallback,
}) saveImage = imageDownloader ?? _downloadAttachment;
final downloader = isImage ? saveImage : saveFile;
// Closing attachment actions modal before opening
// attachment download dialog
Navigator.of(context).pop();
final downloader = attachmentDownloader ??
StreamAttachmentHandler.instance.downloadAttachment;
// No need to show progress dialog in case of
// web or desktop.
if (isDesktopDeviceOrWeb) {
downloader(attachment);
return;
}
final progressNotifier =
ValueNotifier<_DownloadProgress?>(
_DownloadProgress.initial(),
);
final downloadedPathNotifier = ValueNotifier<String?>(
null,
);
final downloadedPathNotifier =
ValueNotifier<String?>(null);
downloader(
attachment,
progressCallback: (received, total) {
onReceiveProgress: (received, total) {
progressNotifier.value = _DownloadProgress(
total,
received,
);
},
downloadedPathCallback: (String? path) {
downloadedPathNotifier.value = path;
},
).catchError((e, stk) {
).then((path) {
downloadedPathNotifier.value = path;
}).catchError((e, stk) {
print(e);
print(stk);
progressNotifier.value = null;
});
// Closing attachment actions modal before opening
// attachment download dialog
Navigator.pop(context);
showDialog(
barrierDismissible: false,
context: context,
@@ -273,30 +257,31 @@ class AttachmentActionsModal extends StatelessWidget {
VoidCallback? onTap, {
Color? color,
Key? key,
}) =>
Material(
key: key,
color: StreamChatTheme.of(context).colorTheme.barsBg,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
children: [
icon,
const SizedBox(width: 16),
Text(
title,
style: StreamChatTheme.of(context)
.textTheme
.body
.copyWith(color: color),
),
],
),
}) {
return Material(
key: key,
color: StreamChatTheme.of(context).colorTheme.barsBg,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
children: [
icon,
const SizedBox(width: 16),
Text(
title,
style: StreamChatTheme.of(context)
.textTheme
.body
.copyWith(color: color),
),
],
),
),
);
),
);
}
Widget _buildDownloadProgressDialog(
BuildContext context,
@@ -385,29 +370,6 @@ class AttachmentActionsModal extends StatelessWidget {
},
);
}
Future<String?> _downloadAttachment(
Attachment attachment, {
ProgressCallback? progressCallback,
DownloadedPathCallback? downloadedPathCallback,
}) async {
String? filePath;
final appDocDir = await getTemporaryDirectory();
final url =
attachment.assetUrl ?? attachment.imageUrl ?? attachment.thumbUrl!;
await Dio().download(
url,
(Headers responseHeaders) {
final ext = Uri.parse(url).pathSegments.last;
filePath ??= '${appDocDir.path}/${attachment.id}.$ext';
return filePath!;
},
onReceiveProgress: progressCallback,
);
final result = await ImageGallerySaver.saveFile(filePath!);
downloadedPathCallback?.call((result as Map)['filePath'] ?? filePath);
return (result as Map)['filePath'];
}
}
class _DownloadProgress {
@@ -426,9 +388,11 @@ class _DownloadProgress {
int get toPercentage => (received * 100) ~/ total;
}
/// Class for custom attachment action
/// {@template attachmentAction}
/// Defines a custom attachment action.
/// {@endtemplate}
class AttachmentAction {
/// Constructor for custom attachment action
/// {@macro attachmentAction}
AttachmentAction({
required this.actionTitle,
required this.icon,
@@ -0,0 +1,642 @@
// ignore_for_file: no-empty-block
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
export 'stream_command_autocomplete_options.dart';
export 'stream_mention_autocomplete_options.dart';
/// {@macro stream_chat_flutter.StreamMessageInputController}
typedef StreamMessageEditingController = StreamMessageInputController;
/// Positions the [AutocompleteTrigger] options around the [TextField] or
/// [TextFormField] that triggered the autocomplete.
enum OptionsAlignment {
/// The options are displayed below the field.
below,
/// The options are displayed above the field.
///
/// This is the default.
above;
Anchor _toAnchor() {
switch (this) {
case OptionsAlignment.below:
return const Aligned(
widthFactor: 1,
follower: Alignment.topCenter,
target: Alignment.bottomCenter,
);
case OptionsAlignment.above:
return const Aligned(
widthFactor: 1,
follower: Alignment.bottomCenter,
target: Alignment.topCenter,
);
}
}
}
/// The type of the Autocomplete callback which returns the widget that
/// contains the input [TextField] or [TextFormField].
///
/// See also:
///
/// * [StreamAutocomplete.fieldViewBuilder], which is of this type.
typedef StreamAutocompleteFieldViewBuilder = Widget Function(
BuildContext context,
StreamMessageEditingController messageEditingController,
FocusNode focusNode,
);
/// The type of the [StreamAutocompleteTrigger] callback which returns a
/// [Widget] that displays the specified [options].
///
/// See also:
///
/// * [StreamAutocompleteTrigger.optionsViewBuilder], which is of this type.
typedef StreamAutocompleteOptionsViewBuilder = Widget Function(
BuildContext context,
StreamAutocompleteQuery autocompleteQuery,
StreamMessageEditingController messageEditingController,
);
/// The query to determine the autocomplete options.
class StreamAutocompleteQuery {
/// Creates a [StreamAutocompleteQuery] with the specified [query] and
/// [selection].
const StreamAutocompleteQuery({
required this.query,
required this.selection,
});
/// The query string.
final String query;
/// The selection in the text field.
final TextSelection selection;
}
class _StreamAutocompleteInvokedTriggerWithQuery {
const _StreamAutocompleteInvokedTriggerWithQuery(this.trigger, this.query);
final StreamAutocompleteTrigger trigger;
final StreamAutocompleteQuery query;
}
/// The class responsible for triggering autocomplete suggestions and
/// displaying the options.
class StreamAutocompleteTrigger {
/// Creates a [StreamAutocompleteTrigger] which can be used to trigger
/// autocomplete suggestions.
const StreamAutocompleteTrigger({
required this.trigger,
required this.optionsViewBuilder,
this.triggerOnlyAfterSpace = false,
this.triggerOnlyAtStart = false,
this.minimumRequiredCharacters = 0,
});
/// The trigger character.
///
/// eg. '@', '#', ':'
final String trigger;
/// Whether the [trigger] should only be recognised at the start of the input.
final bool triggerOnlyAtStart;
/// Whether the [trigger] should only be recognised after a space.
final bool triggerOnlyAfterSpace;
/// The minimum required characters for the [trigger] to start recognising
/// a autocomplete options.
final int minimumRequiredCharacters;
/// Builds the widget responsible for querying and displaying the
/// autocomplete options.
///
/// See also:
/// * [StreamAutocompleteOptions], which helps in displaying the options.
final StreamAutocompleteOptionsViewBuilder optionsViewBuilder;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is StreamAutocompleteTrigger &&
runtimeType == other.runtimeType &&
trigger == other.trigger &&
triggerOnlyAtStart == other.triggerOnlyAtStart &&
triggerOnlyAfterSpace == other.triggerOnlyAfterSpace &&
minimumRequiredCharacters == other.minimumRequiredCharacters;
@override
int get hashCode =>
trigger.hashCode ^
triggerOnlyAtStart.hashCode ^
triggerOnlyAfterSpace.hashCode ^
minimumRequiredCharacters.hashCode;
/// Checks if the user is invoking the recognising [trigger] and returns
/// the autocomplete query if so.
StreamAutocompleteQuery? invokingTrigger(
Message message,
TextEditingValue textEditingValue,
) {
final text = textEditingValue.text;
final cursorPosition = textEditingValue.selection.baseOffset;
// Find the first [trigger] location before the input cursor.
final firstTriggerIndexBeforeCursor =
text.substring(0, cursorPosition).lastIndexOf(trigger);
// If the [trigger] is not found before the cursor, then it's not a trigger.
if (firstTriggerIndexBeforeCursor == -1) return null;
// If the [trigger] is found before the cursor, but the [trigger] is only
// recognised at the start of the input, then it's not a trigger.
if (triggerOnlyAtStart && firstTriggerIndexBeforeCursor != 0) {
return null;
}
// Only show typing suggestions after a space, or at the start of the input
// valid examples: "@user", "Hello @user"
// invalid examples: "Hello@user"
final textBeforeTrigger = text.substring(0, firstTriggerIndexBeforeCursor);
if (triggerOnlyAfterSpace &&
textBeforeTrigger.isNotEmpty &&
!textBeforeTrigger.endsWith(' ')) {
return null;
}
// The suggestion range. Protect against invalid ranges.
final suggestionStart = firstTriggerIndexBeforeCursor + trigger.length;
final suggestionEnd = cursorPosition;
if (suggestionStart > suggestionEnd) return null;
// Fetch the suggestion text. The suggestions can't have spaces.
// valid example: "@luke_skywa..."
// invalid example: "@luke skywa..."
final suggestionText = text.substring(suggestionStart, suggestionEnd);
if (suggestionText.contains(' ')) return null;
// A minimum number of characters can be provided to only show
// suggestions after the customer has input enough characters.
if (suggestionText.length < minimumRequiredCharacters) return null;
return StreamAutocompleteQuery(
query: suggestionText,
selection: TextSelection(
baseOffset: suggestionStart,
extentOffset: suggestionEnd,
),
);
}
}
/// A widget that provides a text field with autocomplete functionality.
class StreamAutocomplete extends StatefulWidget {
/// Create an instance of StreamAutocomplete.
///
/// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must
/// not be null.
const StreamAutocomplete({
super.key,
this.focusNode,
this.messageEditingController,
required this.autocompleteTriggers,
this.fieldViewBuilder = _defaultFieldViewBuilder,
this.optionsAlignment = OptionsAlignment.above,
this.debounceDuration = const Duration(milliseconds: 300),
}) : assert((focusNode == null) == (messageEditingController == null), '');
/// The triggers that trigger autocomplete.
final Iterable<StreamAutocompleteTrigger> autocompleteTriggers;
/// Builds the field whose input is used to get the options.
///
/// Pass the provided [StreamMessageEditingController] to the field built
/// here so that StreamAutocomplete can listen for changes.
final StreamAutocompleteFieldViewBuilder fieldViewBuilder;
/// The [FocusNode] that is used for the text field.
///
/// The main purpose of this parameter is to allow the use of a separate text
/// field located in another part of the widget tree instead of the text
/// field built by [fieldViewBuilder]. For example, it may be desirable to
/// place the text field in the AppBar and the options below in the main body.
///
/// When following this pattern, [fieldViewBuilder] can return
/// `SizedBox.shrink()` so that nothing is drawn where the text field would
/// normally be. A separate text field can be created elsewhere, and a
/// FocusNode and StreamMessageEditingController can be passed both to that
/// text field and to StreamAutocomplete.
///
/// If this parameter is not null, then [messageEditingController] must also
/// be not null.
final FocusNode? focusNode;
/// The [StreamMessageEditingController] that is used for the text field.
///
/// If this parameter is not null, then [focusNode] must also be not null.
final StreamMessageEditingController? messageEditingController;
/// The alignment of the options.
///
/// The default value is [OptionsAlignment.above].
final OptionsAlignment optionsAlignment;
/// The duration of the debounce period for the
/// [StreamMessageEditingController].
///
/// The default value is [300ms].
final Duration debounceDuration;
static Widget _defaultFieldViewBuilder(
BuildContext context,
StreamMessageEditingController messageEditingController,
FocusNode focusNode,
) {
return _StreamAutocompleteField(
focusNode: focusNode,
messageEditingController: messageEditingController,
);
}
/// Returns the nearest [StreamAutocomplete] ancestor of the given context.
static _StreamAutocompleteState of(BuildContext context) {
final state = context.findAncestorStateOfType<_StreamAutocompleteState>();
assert(state != null, 'StreamAutocomplete not found in the widget tree');
return state!;
}
@override
_StreamAutocompleteState createState() => _StreamAutocompleteState();
}
class _StreamAutocompleteState extends State<StreamAutocomplete> {
late StreamMessageEditingController _messageEditingController;
late FocusNode _focusNode;
StreamAutocompleteQuery? _currentQuery;
StreamAutocompleteTrigger? _currentTrigger;
bool _hideOptions = false;
String _lastFieldText = '';
// True if the state indicates that the options should be visible.
bool get _shouldShowOptions {
return !_hideOptions &&
_focusNode.hasFocus &&
_currentQuery != null &&
_currentTrigger != null;
}
/// Accepts and replaces the current query with the given [option] and closes
/// the suggested options.
///
/// Optionally, pass [keepTrigger] false to remove the trigger from the text.
void acceptAutocompleteOption(
String option, {
bool keepTrigger = true,
}) {
if (option.isEmpty) return;
final query = _currentQuery;
final trigger = _currentTrigger;
if (query == null || trigger == null) return;
final querySelection = query.selection;
final text = _messageEditingController.text;
var start = querySelection.baseOffset;
if (!keepTrigger) start -= 1;
final end = querySelection.extentOffset;
final alreadyContainsSpace = text.substring(end).startsWith(' ');
// Having extra space helps dismissing the auto-completion view.
// ignore: parameter_assignments
if (!alreadyContainsSpace) option += ' ';
var selectionOffset = start + option.length;
// In case the extra space is already there, we need to move the cursor
// after the space.
if (alreadyContainsSpace) selectionOffset += 1;
final newText = text.replaceRange(start, end, option);
final newSelection = TextSelection.collapsed(offset: selectionOffset);
_messageEditingController.textEditingValue = TextEditingValue(
text: newText,
selection: newSelection,
);
return closeSuggestions();
}
/// Closes the suggestions and resets the current query.
void closeSuggestions() {
final prev = _currentQuery;
if (prev == null) return;
_currentQuery = null;
if (mounted) setState(() {});
}
/// Starts showing the suggestions for the given [query].
void showSuggestions(
StreamAutocompleteQuery query,
StreamAutocompleteTrigger trigger,
) {
final prevQuery = _currentQuery;
final prevTrigger = _currentTrigger;
if (prevQuery == query && prevTrigger == trigger) return;
_currentQuery = query;
_currentTrigger = trigger;
if (mounted) setState(() {});
}
// Checks if there is any invoked autocomplete trigger and returns the first
// one along with the query that matches the current input.
_StreamAutocompleteInvokedTriggerWithQuery? _getInvokedTriggerWithQuery(
Message messageValue,
TextEditingValue textEditingValue,
) {
final autocompleteTriggers = widget.autocompleteTriggers.toSet();
for (final trigger in autocompleteTriggers) {
final query = trigger.invokingTrigger(messageValue, textEditingValue);
if (query != null) {
return _StreamAutocompleteInvokedTriggerWithQuery(trigger, query);
}
}
return null;
}
// Called when _textEditingController changes.
late final _onChangedField = debounce(
() {
final messageValue = _messageEditingController.message;
final textEditingValue = _messageEditingController.textEditingValue;
// If the content has not changed, then there is nothing to do.
if (textEditingValue.text == _lastFieldText) return;
// Make sure the options are no longer hidden if the content of the
// field changes.
_hideOptions = false;
_lastFieldText = textEditingValue.text;
// If the text field is empty, then there is no need to do anything.
if (textEditingValue.text.isEmpty) return closeSuggestions();
// If the text field is not empty, then we need to check if the
// text field contains a trigger.
final _triggerWithQuery = _getInvokedTriggerWithQuery(
messageValue,
textEditingValue,
);
// If the text field does not contain a trigger, then there is no need
// to do anything.
if (_triggerWithQuery == null) return closeSuggestions();
// If the text field contains a trigger, then we need to open the
// portal.
final trigger = _triggerWithQuery.trigger;
final query = _triggerWithQuery.query;
return showSuggestions(query, trigger);
},
widget.debounceDuration,
);
// Called when the field's FocusNode changes.
void _onChangedFocus() {
// Options should no longer be hidden when the field is re-focused.
_hideOptions = !_focusNode.hasFocus;
if (mounted) setState(() {});
}
// Handle a potential change in textEditingController by properly disposing of
// the old one and setting up the new one, if needed.
void _updateTextEditingController(
StreamMessageEditingController? old,
StreamMessageEditingController? current,
) {
if ((old == null && current == null) || old == current) {
return;
}
if (old == null) {
_messageEditingController
..removeListener(_onChangedField)
..dispose();
_messageEditingController = current!;
} else if (current == null) {
_messageEditingController.removeListener(_onChangedField);
_messageEditingController = StreamMessageEditingController();
} else {
_messageEditingController.removeListener(_onChangedField);
_messageEditingController = current;
}
_messageEditingController.addListener(_onChangedField);
}
// Handle a potential change in focusNode by properly disposing of the old one
// and setting up the new one, if needed.
void _updateFocusNode(FocusNode? old, FocusNode? current) {
if ((old == null && current == null) || old == current) {
return;
}
if (old == null) {
_focusNode
..removeListener(_onChangedFocus)
..dispose();
_focusNode = current!;
} else if (current == null) {
_focusNode.removeListener(_onChangedFocus);
_focusNode = FocusNode();
} else {
_focusNode.removeListener(_onChangedFocus);
_focusNode = current;
}
_focusNode.addListener(_onChangedFocus);
}
@override
void initState() {
super.initState();
_messageEditingController =
widget.messageEditingController ?? StreamMessageEditingController();
_messageEditingController.addListener(_onChangedField);
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.addListener(_onChangedFocus);
}
@override
void didUpdateWidget(StreamAutocomplete oldWidget) {
super.didUpdateWidget(oldWidget);
_updateTextEditingController(
oldWidget.messageEditingController,
widget.messageEditingController,
);
_updateFocusNode(oldWidget.focusNode, widget.focusNode);
}
@override
void dispose() {
_messageEditingController.removeListener(_onChangedField);
if (widget.messageEditingController == null) {
_messageEditingController.dispose();
}
_focusNode.removeListener(_onChangedFocus);
if (widget.focusNode == null) {
_focusNode.dispose();
}
_onChangedField.cancel();
closeSuggestions();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Adding additional builder so that [.of] works.
return Builder(
builder: (context) {
final anchor = widget.optionsAlignment._toAnchor();
final shouldShowOptions = _shouldShowOptions;
final optionViewBuilder = shouldShowOptions
? _currentTrigger!.optionsViewBuilder(
context,
_currentQuery!,
_messageEditingController,
)
: null;
return PortalTarget(
anchor: anchor,
visible: shouldShowOptions,
portalFollower: optionViewBuilder,
child: widget.fieldViewBuilder(
context,
_messageEditingController,
_focusNode,
),
);
},
);
}
}
// The default Material-style Autocomplete text field.
class _StreamAutocompleteField extends StatelessWidget {
const _StreamAutocompleteField({
required this.focusNode,
required this.messageEditingController,
});
final FocusNode focusNode;
final StreamMessageEditingController messageEditingController;
@override
Widget build(BuildContext context) {
return StreamMessageTextField(
controller: messageEditingController,
focusNode: focusNode,
);
}
}
const _kDefaultStreamAutocompleteOptionsShape = RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
);
/// A helper widget used to show the options of a [StreamAutocomplete].
class StreamAutocompleteOptions<T extends Object> extends StatelessWidget {
/// Creates a [StreamAutocompleteOptions] widget.
const StreamAutocompleteOptions({
super.key,
this.color,
this.elevation = 2,
this.margin = const EdgeInsets.all(8),
this.clipBehavior = Clip.hardEdge,
required this.options,
this.maxHeight,
required this.optionBuilder,
this.headerBuilder,
this.shape = _kDefaultStreamAutocompleteOptionsShape,
});
/// The background color of the options card.
///
/// Defaults to [StreamColorTheme.barsBg].
final Color? color;
/// The elevation of the options card.
///
/// The default value is 2.
final double elevation;
/// The margin of the options card.
///
/// The default value is [EdgeInsets.all(8)].
final EdgeInsetsGeometry margin;
/// The clip behavior of the options card.
///
/// The default value is [Clip.hardEdge].
final Clip clipBehavior;
/// The shape of the options card.
final ShapeBorder shape;
/// The options to display.
final Iterable<T> options;
/// The maximum height of the options card.
///
/// Defaults to half the height of the screen.
final double? maxHeight;
/// The builder for the options.
final Widget Function(BuildContext context, T option) optionBuilder;
/// The builder for the header of the options.
final WidgetBuilder? headerBuilder;
@override
Widget build(BuildContext context) {
final height = MediaQuery.of(context).size.height;
final colorTheme = StreamChatTheme.of(context).colorTheme;
return Card(
margin: margin,
elevation: elevation,
color: color ?? colorTheme.barsBg,
shape: shape,
clipBehavior: clipBehavior,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (headerBuilder != null) ...[
headerBuilder!(context),
const Divider(height: 0),
],
LimitedBox(
maxHeight: maxHeight ?? height * 0.5,
child: ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: options.length,
itemBuilder: (context, index) {
final option = options.elementAt(index);
return optionBuilder(context, option);
},
),
),
],
),
);
}
}
@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/autocomplete/stream_autocomplete.dart';
import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// {@template commands_overlay}
/// Overlay for displaying commands that can be used
/// to interact with the channel.
/// {@endtemplate}
class StreamCommandAutocompleteOptions extends StatelessWidget {
/// Constructor for creating a [StreamCommandAutocompleteOptions]
const StreamCommandAutocompleteOptions({
required this.query,
required this.channel,
this.onCommandSelected,
super.key,
});
/// Query for searching commands.
final String query;
/// The channel to search for users.
final Channel channel;
/// Callback called when a command is selected.
final ValueSetter<Command>? onCommandSelected;
@override
Widget build(BuildContext context) {
final commands = channel.config?.commands.where((it) {
final normalizedQuery = query.toUpperCase();
final normalizedName = it.name.toUpperCase();
return normalizedName.contains(normalizedQuery);
});
if (commands == null || commands.isEmpty) return const SizedBox.shrink();
final streamChatTheme = StreamChatTheme.of(context);
final colorTheme = streamChatTheme.colorTheme;
final textTheme = streamChatTheme.textTheme;
return StreamAutocompleteOptions<Command>(
options: commands,
headerBuilder: (context) {
return ListTile(
dense: true,
horizontalTitleGap: 0,
leading: StreamSvgIcon.lightning(
color: colorTheme.accentPrimary,
size: 28,
),
title: Text(
context.translations.instantCommandsLabel,
style: TextStyle(
color: colorTheme.textHighEmphasis.withOpacity(0.5),
),
),
);
},
optionBuilder: (context, command) {
return ListTile(
dense: true,
horizontalTitleGap: 0,
leading: _CommandIcon(command: command),
title: Row(
children: [
Text(
command.name.capitalize(),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Text(
'/${command.name} ${command.args}',
style: textTheme.body.copyWith(
color: colorTheme.textLowEmphasis,
),
),
],
),
onTap: onCommandSelected == null
? null
: () => onCommandSelected!(command),
);
},
);
}
}
class _CommandIcon extends StatelessWidget {
const _CommandIcon({required this.command});
final Command command;
@override
Widget build(BuildContext context) {
final _streamChatTheme = StreamChatTheme.of(context);
switch (command.name) {
case 'giphy':
return CircleAvatar(
radius: 12,
child: StreamSvgIcon.giphyIcon(
size: 24,
),
);
case 'ban':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: StreamSvgIcon.iconUserDelete(
size: 16,
color: Colors.white,
),
);
case 'flag':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: StreamSvgIcon.flag(
size: 14,
color: Colors.white,
),
);
case 'imgur':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: ClipOval(
child: StreamSvgIcon.imgur(
size: 24,
),
),
);
case 'mute':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: StreamSvgIcon.mute(
size: 16,
color: Colors.white,
),
);
case 'unban':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: StreamSvgIcon.userAdd(
size: 16,
color: Colors.white,
),
);
case 'unmute':
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: StreamSvgIcon.volumeUp(
size: 16,
color: Colors.white,
),
);
default:
return CircleAvatar(
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
radius: 12,
child: StreamSvgIcon.lightning(
size: 16,
color: Colors.white,
),
);
}
}
}
@@ -1,31 +1,20 @@
import 'package:flutter/material.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/user_mention_tile.dart';
import 'package:stream_chat_flutter/src/autocomplete/stream_autocomplete.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/user/user_mention_tile.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/src/utils/typedefs.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Builder function for building a mention tile.
///
/// Use [StreamUserMentionTile] for the default implementation.
typedef MentionTileBuilder = Widget Function(
BuildContext context,
User user,
);
/// {@macro user_mention_tile}
@Deprecated("Use 'StreamUserMentionsOverlay' instead")
typedef UserMentionsOverlay = StreamUserMentionsOverlay;
/// {@template user_mentions_overlay}
/// Overlay for displaying users that can be mentioned.
/// {@endtemplate}
class StreamUserMentionsOverlay extends StatefulWidget {
/// Constructor for creating a [StreamUserMentionsOverlay].
StreamUserMentionsOverlay({
class StreamMentionAutocompleteOptions extends StatefulWidget {
/// Constructor for creating a [StreamMentionAutocompleteOptions].
StreamMentionAutocompleteOptions({
super.key,
required this.query,
required this.channel,
required this.size,
this.client,
this.limit = 10,
this.mentionAllAppUsers = false,
@@ -46,9 +35,6 @@ class StreamUserMentionsOverlay extends StatefulWidget {
/// Limit applied on user search results.
final int limit;
/// The size of the overlay.
final Size size;
/// The channel to search for users.
final Channel channel;
@@ -61,17 +47,18 @@ class StreamUserMentionsOverlay extends StatefulWidget {
final bool mentionAllAppUsers;
/// Customize the tile for the mentions overlay.
final MentionTileBuilder? mentionsTileBuilder;
final UserMentionTileBuilder? mentionsTileBuilder;
/// Callback called when a user is selected.
final void Function(User user)? onMentionUserTap;
final ValueSetter<User>? onMentionUserTap;
@override
_StreamUserMentionsOverlayState createState() =>
_StreamUserMentionsOverlayState();
_StreamMentionAutocompleteOptionsState createState() =>
_StreamMentionAutocompleteOptionsState();
}
class _StreamUserMentionsOverlayState extends State<StreamUserMentionsOverlay> {
class _StreamMentionAutocompleteOptionsState
extends State<StreamMentionAutocompleteOptions> {
late Future<List<User>> userMentionsFuture;
@override
@@ -81,7 +68,7 @@ class _StreamUserMentionsOverlayState extends State<StreamUserMentionsOverlay> {
}
@override
void didUpdateWidget(covariant StreamUserMentionsOverlay oldWidget) {
void didUpdateWidget(covariant StreamMentionAutocompleteOptions oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.channel != oldWidget.channel ||
widget.query != oldWidget.query ||
@@ -93,43 +80,30 @@ class _StreamUserMentionsOverlayState extends State<StreamUserMentionsOverlay> {
@override
Widget build(BuildContext context) {
final theme = StreamChatTheme.of(context);
return Card(
margin: const EdgeInsets.all(8),
elevation: 2,
color: theme.colorTheme.barsBg,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
clipBehavior: Clip.hardEdge,
child: Container(
constraints: BoxConstraints.loose(widget.size),
decoration: BoxDecoration(color: theme.colorTheme.barsBg),
child: FutureBuilder<List<User>>(
future: userMentionsFuture,
builder: (context, snapshot) {
if (snapshot.hasError) return const Offstage();
if (!snapshot.hasData) return const Offstage();
final users = snapshot.data!;
return ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return Material(
color: theme.colorTheme.barsBg,
child: InkWell(
onTap: () => widget.onMentionUserTap?.call(user),
child: widget.mentionsTileBuilder?.call(context, user) ??
StreamUserMentionTile(user),
),
);
},
return FutureBuilder<List<User>>(
future: userMentionsFuture,
builder: (context, snapshot) {
if (snapshot.hasError) return const SizedBox.shrink();
if (!snapshot.hasData) return const SizedBox.shrink();
final users = snapshot.data!;
return StreamAutocompleteOptions<User>(
options: users,
optionBuilder: (context, user) {
final colorTheme = StreamChatTheme.of(context).colorTheme;
return Material(
color: colorTheme.barsBg,
child: InkWell(
onTap: widget.onMentionUserTap == null
? null
: () => widget.onMentionUserTap!(user),
child: widget.mentionsTileBuilder?.call(context, user) ??
StreamUserMentionTile(user),
),
);
},
),
),
);
},
);
}
@@ -3,15 +3,11 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
/// {@macro gradient_avatar}
@Deprecated("Use 'StreamGradientAvatar' instead")
typedef GradientAvatar = StreamGradientAvatar;
/// {@template gradient_avatar}
/// Fallback user avatar with a polygon gradient overlayed with text
/// {@template streamGradientAvatar}
/// Fallback user avatar with a polygon gradient overlaid with text
/// {@endtemplate}
class StreamGradientAvatar extends StatefulWidget {
/// Constructor for [StreamGradientAvatar]
/// {@macro streamGradientAvatar}
const StreamGradientAvatar({
super.key,
required this.name,
@@ -30,18 +26,20 @@ class StreamGradientAvatar extends StatefulWidget {
class _StreamGradientAvatarState extends State<StreamGradientAvatar> {
@override
Widget build(BuildContext context) => Center(
child: RepaintBoundary(
child: CustomPaint(
painter: DemoPainter(
widget.userId,
getShortenedName(widget.name),
DefaultTextStyle.of(context).style.fontFamily ?? 'Roboto',
),
child: const SizedBox.expand(),
Widget build(BuildContext context) {
return Center(
child: RepaintBoundary(
child: CustomPaint(
painter: PolygonGradientPainter(
widget.userId,
getShortenedName(widget.name),
DefaultTextStyle.of(context).style.fontFamily ?? 'Roboto',
),
child: const SizedBox.expand(),
),
);
),
);
}
String getShortenedName(String name) {
var parts = name.split(' ')..removeWhere((e) => e == '');
@@ -60,19 +58,21 @@ class _StreamGradientAvatarState extends State<StreamGradientAvatar> {
}
}
/// {@template polygonGradientPainter}
/// Painter for bg polygon gradient
class DemoPainter extends CustomPainter {
/// Constructor for [DemoPainter]
DemoPainter(
/// {@endtemplate}
class PolygonGradientPainter extends CustomPainter {
/// {@macro polygonGradientPainter}
PolygonGradientPainter(
this.userId,
this.username,
this.fontFamily,
);
/// Init grid row count
/// Initial grid row count
static const int rowCount = 5;
/// Init grid column count
/// Initial grid column count
static const int columnCount = 5;
/// User ID used for key
@@ -154,7 +154,7 @@ class DemoPainter extends CustomPainter {
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
/// Transforms initial grid into a polygon grid
/// Transforms initial grid into a polygon grid.
List<Offset> transformPoints(Set<Offset> points, Size size) {
final transformedList = <Offset>[];
final orgList = points.toList();
@@ -185,9 +185,11 @@ class DemoPainter extends CustomPainter {
}
}
/// Class for storing and drawing four points of a polygon
/// {@template offset4}
/// Class for storing and drawing four points of a polygon.
/// {@endtemplate}
class Offset4 {
/// Constructor for [Offset4]
/// {@macro offset4}
Offset4(
this.p1,
this.p2,
@@ -1,15 +1,11 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro group_avatar}
@Deprecated("Use 'StreamGroupAvatar' instead")
typedef GroupAvatar = StreamGroupAvatar;
/// {@template group_avatar}
/// {@template streamGroupAvatar}
/// Widget for constructing a group of images
/// {@endtemplate}
class StreamGroupAvatar extends StatelessWidget {
/// Constructor for creating a [StreamGroupAvatar]
/// {@macro streamGroupAvatar}
const StreamGroupAvatar({
super.key,
this.channel,
@@ -25,25 +21,28 @@ class StreamGroupAvatar extends StatelessWidget {
/// The channel of the avatar
final Channel? channel;
/// List of images to display
/// The list of members in the group whose avatars should be displayed.
final List<Member> members;
/// Constraints on the widget
final BoxConstraints? constraints;
/// Callback when widget is tapped
/// The action to perform when the widget is tapped
final VoidCallback? onTap;
/// Highlights if selected
/// If `true`, this widget should be highlighted.
///
/// Defaults to `false`.
final bool selected;
/// [BorderRadius] to pass to the widget
final BorderRadius? borderRadius;
/// Color of selection if selected
/// The color to highlight the widget with if [selected] is `true`
final Color? selectionColor;
/// Thickness with which color of selection is shown
/// The value to use for the border thickness and padding of the
/// selected image
final double selectionThickness;
@override
@@ -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 user_avatar}
@Deprecated("Use 'StreamUserAvatar' instead")
typedef UserAvatar = StreamUserAvatar;
/// {@template user_avatar}
/// Widget that displays a user avatar
/// {@template streamUserAvatar}
/// Displays a user's avatar.
/// {@endtemplate}
class StreamUserAvatar extends StatelessWidget {
/// Constructor to create a [StreamUserAvatar]
/// {@macro streamUserAvatar}
const StreamUserAvatar({
super.key,
required this.user,
@@ -27,54 +23,63 @@ class StreamUserAvatar extends StatelessWidget {
this.placeholder,
});
/// User whose avatar is to displayed
/// User whose avatar is to be displayed
final User user;
/// Alignment of the online indicator
///
/// Defaults to `Alignment.topRight`
final Alignment onlineIndicatorAlignment;
/// Size of the avatar
/// Sizing constraints of the avatar
final BoxConstraints? constraints;
/// [BorderRadius] of the image
final BorderRadius? borderRadius;
/// Size of the online indicator
/// Sizing constraints of the online indicator
final BoxConstraints? onlineIndicatorConstraints;
/// Callback when avatar is tapped
final void Function(User)? onTap;
/// {@macro onUserAvatarTap}
final OnUserAvatarPress? onTap;
/// Callback when avatar is long pressed
final void Function(User)? onLongPress;
/// {@macro onUserAvatarTap}
final OnUserAvatarPress? onLongPress;
/// Flag for showing online status
///
/// Defaults to `true`
final bool showOnlineStatus;
/// Flag for if avatar is selected
///
/// Defaults to `false`
final bool selected;
/// Color of selection
final Color? selectionColor;
/// Selection thickness around the avatar
///
/// Defaults to `4`
final double selectionThickness;
/// The widget that will be built when the user image is loading
final Widget Function(BuildContext, User)? placeholder;
/// {@macro placeholderUserImage}
final PlaceholderUserImage? placeholder;
@override
Widget build(BuildContext context) {
final hasImage = user.image != null && user.image!.isNotEmpty;
final streamChatTheme = StreamChatTheme.of(context);
final streamChatConfig = StreamChatConfiguration.of(context);
final placeholder =
this.placeholder ?? streamChatTheme.placeholderUserImage;
this.placeholder ?? streamChatConfig.placeholderUserImage;
final backupGradientAvatar = ClipRRect(
borderRadius: borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius,
child: streamChatTheme.defaultUserImage(context, user),
child: streamChatConfig.defaultUserImage(context, user),
);
Widget avatar = FittedBox(
@@ -1,59 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Back button implementation
// ignore: prefer-match-file-name
class StreamBackButton extends StatelessWidget {
/// Constructor for creating back button
const StreamBackButton({
super.key,
this.onPressed,
this.showUnreads = false,
this.cid,
});
/// Callback for when button is pressed
final VoidCallback? onPressed;
/// Show unread count
final bool showUnreads;
/// Channel cid used to retrieve unread count
final String? cid;
@override
Widget build(BuildContext context) => Stack(
alignment: Alignment.center,
children: [
RawMaterialButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
hoverElevation: 0,
onPressed: () {
if (onPressed != null) {
onPressed!();
} else {
Navigator.maybePop(context);
}
},
padding: const EdgeInsets.all(14),
child: StreamSvgIcon.left(
size: 24,
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
),
),
if (showUnreads)
Positioned(
top: 7,
right: 7,
child: StreamUnreadIndicator(
cid: cid,
),
),
],
);
}
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
/// {@template attachmentModalSheet}
/// The modalBottomSheet that appears when a mobile user attempts to add
/// attachments to a chat.
///
/// Should not be used on desktop or web.
/// {@endtemplate}
class AttachmentModalSheet extends StatelessWidget {
/// {@macro attachmentModalSheet}
const AttachmentModalSheet({
super.key,
required this.onFileTap,
required this.onPhotoTap,
required this.onVideoTap,
});
/// The action to perform when the "file" button is tapped.
final VoidCallback onFileTap;
/// The action to perform when the "photo" button is tapped.
final VoidCallback onPhotoTap;
/// The action to perform when the "video" button is tapped.
final VoidCallback onVideoTap;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(
context.translations.addAFileLabel,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
ListTile(
leading: const Icon(Icons.image),
title: Text(context.translations.uploadAPhotoLabel),
onTap: () {
onPhotoTap.call();
Navigator.of(context).pop();
},
),
ListTile(
leading: const Icon(Icons.video_library),
title: Text(context.translations.uploadAVideoLabel),
onTap: () {
onVideoTap.call();
Navigator.of(context).pop();
},
),
ListTile(
leading: const Icon(Icons.insert_drive_file),
title: Text(context.translations.uploadAFileLabel),
onTap: () {
onFileTap.call();
Navigator.of(context).pop();
},
),
],
);
}
}
@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template editMessageSheet}
/// Allows a user to edit the selected message.
/// {@endtemplate}
class EditMessageSheet extends StatefulWidget {
/// {@macro editMessageSheet}
const EditMessageSheet({
super.key,
required this.message,
required this.channel,
this.editMessageInputBuilder,
});
/// {@macro editMessageInputBuilder}
final EditMessageInputBuilder? editMessageInputBuilder;
/// The message to edit.
final Message message;
/// The [StreamChannel] above this widget.
final Channel channel;
@override
State<EditMessageSheet> createState() => _EditMessageSheetState();
}
class _EditMessageSheetState extends State<EditMessageSheet> {
late final controller = StreamMessageInputController(
message: widget.message,
);
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final streamChatThemeData = StreamChatTheme.of(context);
return KeyboardShortcutRunner(
onEscapeKeypress: () => Navigator.of(context).pop(),
child: Padding(
padding: MediaQuery.of(context).viewInsets,
child: StreamChannel(
channel: widget.channel,
child: Flex(
direction: Axis.vertical,
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.all(8),
child: StreamSvgIcon.edit(
color: streamChatThemeData.colorTheme.disabled,
),
),
Text(
context.translations.editMessageLabel,
style: const TextStyle(fontWeight: FontWeight.bold),
),
IconButton(
visualDensity: VisualDensity.compact,
icon: StreamSvgIcon.closeSmall(),
onPressed: Navigator.of(context).pop,
),
],
),
),
if (widget.editMessageInputBuilder != null)
widget.editMessageInputBuilder!(context, widget.message)
else
StreamMessageInput(
messageInputController: controller,
preMessageSending: (m) {
FocusScope.of(context).unfocus();
Navigator.of(context).pop();
return m;
},
),
],
),
),
),
);
}
}
@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template errorAlertSheet}
/// A bottom sheet that displays when an error occurs.
///
/// Should only be used on mobile platforms.
/// {@endtemplate}
class ErrorAlertSheet extends StatelessWidget {
/// {@macro errorAlertSheet}
const ErrorAlertSheet({
super.key,
required this.errorDescription,
});
/// The description of the error.
final String errorDescription;
@override
Widget build(BuildContext context) {
final _streamChatTheme = StreamChatTheme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 26,
),
StreamSvgIcon.error(
color: _streamChatTheme.colorTheme.accentError,
size: 24,
),
const SizedBox(
height: 26,
),
Text(
context.translations.somethingWentWrongError,
style: _streamChatTheme.textTheme.headlineBold,
),
const SizedBox(
height: 7,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
errorDescription,
textAlign: TextAlign.center,
),
),
const SizedBox(
height: 36,
),
Container(
color: _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.08),
height: 1,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
context.translations.okLabel,
style: _streamChatTheme.textTheme.bodyBold.copyWith(
color: _streamChatTheme.colorTheme.accentPrimary,
),
),
),
],
),
],
);
}
}
@@ -1,13 +1,6 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/channel_info.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/option_list_tile.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/theme/themes.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart';
import 'package:stream_chat_flutter/src/v4/stream_channel_name.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A [BottomSheet] that shows information about a [Channel].
class StreamChannelInfoBottomSheet extends StatelessWidget {
@@ -1,18 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stream_chat_flutter/src/channel_info.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
///{@macro template_name}
@Deprecated("Use 'StreamChannelHeader' instead")
typedef ChannelHeader = StreamChannelHeader;
/// {@template channel_header}
/// {@template streamChannelHeader}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png)
///
/// It shows the current [Channel] information.
/// Shows information about the current [Channel].
///
/// ```dart
/// class MyApp extends StatelessWidget {
@@ -39,26 +34,26 @@ typedef ChannelHeader = StreamChannelHeader;
/// ```
///
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
/// However you can also use it as a normal widget.
/// However, you can also use it as a normal widget.
///
/// Make sure to have a [StreamChannel] ancestor in order to provide the
/// information about the channel.
///
/// Every part of the widget uses a [StreamBuilder] to render the channel
/// information as soon as it updates.
///
/// By default the widget shows a backButton that calls [Navigator.pop].
/// You can disable this button using the [showBackButton] property of just
/// override the behaviour
/// with [onBackPressed].
/// You can disable this button using the [showBackButton] property.
/// Alternatively, you can override this behaviour via the [onBackPressed]
/// callback.
///
/// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme] and on its [StreamChatThemeData.channelHeaderTheme]
/// property.
/// Modify it to change the widget appearance.
/// The UI is rendered based on the first ancestor of type [StreamChatTheme]
/// and the [StreamChatThemeData.channelHeaderTheme] property. Modify it to
/// change the widget's appearance.
/// {@endtemplate}
class StreamChannelHeader extends StatelessWidget
implements PreferredSizeWidget {
/// Creates a channel header
/// {@macro streamChannelHeader}
const StreamChannelHeader({
super.key,
this.showBackButton = true,
@@ -76,23 +71,28 @@ class StreamChannelHeader extends StatelessWidget
this.elevation = 1,
}) : preferredSize = const Size.fromHeight(kToolbarHeight);
/// True if this header shows the leading back button
/// Whether to show the leading back button
///
/// Defaults to `true`
final bool showBackButton;
/// Callback to call when pressing the back button.
/// The action to perform when the back button is pressed.
///
/// By default it calls [Navigator.pop]
final VoidCallback? onBackPressed;
/// Callback to call when the header is tapped.
/// The action to perform when the header is tapped.
final VoidCallback? onTitleTap;
/// Callback to call when the image is tapped.
/// The action to perform when the image is tapped.
final VoidCallback? onImageTap;
/// If true the typing indicator will be rendered if a user is typing
/// Whether to show the typing indicator
///
/// Defaults to `true`
final bool showTypingIndicator;
/// Show connection tile on header
/// Whether to show the connection state tile
final bool showConnectionStateTile;
/// Title widget
@@ -107,8 +107,9 @@ class StreamChannelHeader extends StatelessWidget
/// Leading widget
final Widget? leading;
/// AppBar actions
/// By default it shows the [StreamChannelAvatar]
/// {@macro flutter.material.appbar.actions}
///
/// The [StreamChannelAvatar] is shown by default
final List<Widget>? actions;
/// The background color for this [StreamChannelHeader].
@@ -117,6 +118,9 @@ class StreamChannelHeader extends StatelessWidget
/// The elevation for this [StreamChannelHeader].
final double elevation;
@override
final Size preferredSize;
@override
Widget build(BuildContext context) {
final effectiveCenterTitle = getEffectiveCenterTitle(
@@ -131,7 +135,7 @@ class StreamChannelHeader extends StatelessWidget
(showBackButton
? StreamBackButton(
onPressed: onBackPressed,
showUnreads: true,
showUnreadCount: true,
)
: const SizedBox());
@@ -215,7 +219,4 @@ class StreamChannelHeader extends StatelessWidget
},
);
}
@override
final Size preferredSize;
}
@@ -0,0 +1,200 @@
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template streamChannelInfo}
/// Displays information about the current [Channel].
/// {@endtemplate}
class StreamChannelInfo extends StatelessWidget {
/// {@macro streamChannelInfo}
const StreamChannelInfo({
super.key,
required this.channel,
this.textStyle,
this.showTypingIndicator = true,
this.parentId,
});
/// The channel to display information about
final Channel channel;
/// The style of the text displayed
final TextStyle? textStyle;
/// Whether to show the typing indicator
///
/// Defaults to `true`
final bool showTypingIndicator;
/// The ID of the parent message (in the case of a thread)
final String? parentId;
@override
Widget build(BuildContext context) {
final client = StreamChat.of(context).client;
return BetterStreamBuilder<List<Member>>(
stream: channel.state!.membersStream,
initialData: channel.state!.members,
builder: (context, data) => StreamConnectionStatusBuilder(
statusBuilder: (context, status) {
switch (status) {
case ConnectionStatus.connected:
return _ConnectedTitleState(
channel: channel,
showTypingIndicator: showTypingIndicator,
textStyle: textStyle,
members: data,
parentId: parentId,
);
case ConnectionStatus.connecting:
return _ConnectingTitleState(textStyle: textStyle);
case ConnectionStatus.disconnected:
return _DisconnectedTitleState(
client: client,
textStyle: textStyle,
);
default:
return const Offstage();
}
},
),
);
}
}
class _ConnectedTitleState extends StatelessWidget {
const _ConnectedTitleState({
required this.channel,
required this.showTypingIndicator,
this.members,
this.textStyle,
this.parentId,
});
final Channel channel;
final List<Member>? members;
final TextStyle? textStyle;
final bool showTypingIndicator;
final String? parentId;
@override
Widget build(BuildContext context) {
Widget? alternativeWidget;
final memberCount = channel.memberCount;
if (memberCount != null && memberCount > 2) {
var text = context.translations.membersCountText(memberCount);
final onlineCount =
members?.where((m) => m.user?.online == true).length ?? 0;
if (onlineCount > 0) {
text += ', ${context.translations.watchersCountText(onlineCount)}';
}
alternativeWidget = Text(
text,
style: StreamChannelHeaderTheme.of(context).subtitleStyle,
);
} else {
final userId = StreamChat.of(context).currentUser?.id;
final otherMember = members?.firstWhereOrNull(
(element) => element.userId != userId,
);
if (otherMember != null) {
if (otherMember.user?.online == true) {
alternativeWidget = Text(
context.translations.userOnlineText,
style: textStyle,
);
} else {
alternativeWidget = Text(
'${context.translations.userLastOnlineText} '
'${Jiffy(otherMember.user?.lastActive).fromNow()}',
style: textStyle,
);
}
}
}
if (!showTypingIndicator) {
return alternativeWidget ?? const Offstage();
}
return StreamTypingIndicator(
parentId: parentId,
alternativeWidget: alternativeWidget,
style: textStyle,
);
}
}
class _ConnectingTitleState extends StatelessWidget {
const _ConnectingTitleState({
this.textStyle,
});
final TextStyle? textStyle;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 16,
width: 16,
child: Center(
child: CircularProgressIndicator(),
),
),
const SizedBox(width: 10),
Text(
context.translations.searchingForNetworkText,
style: textStyle,
),
],
);
}
}
class _DisconnectedTitleState extends StatelessWidget {
const _DisconnectedTitleState({
required this.client,
this.textStyle,
});
final StreamChatClient client;
final TextStyle? textStyle;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
context.translations.offlineLabel,
style: textStyle,
),
TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity,
),
),
onPressed: () => client
..closeConnection()
..openConnection(),
child: Text(
context.translations.tryAgainLabel,
style: textStyle?.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
),
),
),
],
);
}
}
@@ -1,22 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.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';
/// Widget builder for title
typedef TitleBuilder = Widget Function(
BuildContext context,
ConnectionStatus status,
StreamChatClient client,
);
/// {@macro channel_list_header}
@Deprecated("Use 'StreamChannelListHeader' instead")
typedef ChannelListHeader = StreamChannelListHeader;
/// {@template channel_list_header}
/// It shows the current [StreamChatClient] status.
/// {@template streamChannelListHeader}
/// Shows the current [StreamChatClient] status.
///
/// ```dart
/// class MyApp extends StatelessWidget {
@@ -39,20 +28,19 @@ typedef ChannelListHeader = StreamChannelListHeader;
/// ```
///
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
/// However you can also use it as a normal widget.
/// However, you can also use it as a normal widget.
///
/// The widget by default uses the inherited [StreamChatClient]
/// to fetch information about the status.
/// However you can also pass your own [StreamChatClient]
/// if you don't have it in the widget tree.
/// Uses the inherited [StreamChatClient], by default, to fetch information
/// about the status of the [client]. You can also pass your own
/// [StreamChatClient] if you don't have it in the widget tree.
///
/// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme] and on its [StreamChannelListHeaderThemeData] property.
/// Modify it to change the widget appearance.
/// Renders the UI based on the first ancestor of type [StreamChatTheme] and
/// the [StreamChannelListHeaderThemeData] property. Modify it to change the
/// widget's appearance.
/// {@endtemplate}
class StreamChannelListHeader extends StatelessWidget
implements PreferredSizeWidget {
/// Instantiates a ChannelListHeader
/// {@macro streamChannelListHeader}
const StreamChannelListHeader({
super.key,
this.client,
@@ -69,23 +57,24 @@ class StreamChannelListHeader extends StatelessWidget
this.elevation = 1,
});
/// Pass this if you don't have a [StreamChatClient] in your widget tree.
/// Use this if you don't have a [StreamChatClient] in your widget tree.
final StreamChatClient? client;
/// Use this to build your own title as per different [ConnectionStatus]
final TitleBuilder? titleBuilder;
/// {@macro channelListHeaderTitleBuilder}
final ChannelListHeaderTitleBuilder? titleBuilder;
/// Callback to call when pressing the user avatar button.
/// By default it calls Scaffold.of(context).openDrawer()
/// The action to perform when pressing the user avatar button.
///
/// By default it calls `Scaffold.of(context).openDrawer()`.
final Function(User)? onUserAvatarTap;
/// Callback to call when pressing the new chat button.
/// The action to perform when pressing the "new chat" button.
final VoidCallback? onNewChatButtonTap;
/// Show connection state tile
/// Whether to show the connection state tile
final bool showConnectionStateTile;
/// Callback before navigation is performed
/// The function to execute before navigation is performed
final VoidCallback? preNavigationCallback;
/// Subtitle widget
@@ -95,11 +84,13 @@ class StreamChannelListHeader extends StatelessWidget
final bool? centerTitle;
/// Leading widget
/// By default it shows the logged in user avatar
///
/// By default it shows the logged in user's avatar
final Widget? leading;
/// AppBar actions
/// By default it shows the new chat button
/// {@macro flutter.material.appbar.actions}
///
/// The "new chat" button is shown by default.
final List<Widget>? actions;
/// The background color for this [StreamChannelListHeader].
@@ -108,6 +99,9 @@ class StreamChannelListHeader extends StatelessWidget
/// The elevation for this [StreamChannelListHeader].
final double elevation;
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@override
Widget build(BuildContext context) {
final _client = client ?? StreamChat.of(context).client;
@@ -205,11 +199,11 @@ class StreamChannelListHeader extends StatelessWidget
}
switch (status) {
case ConnectionStatus.connected:
return _buildConnectedTitleState(context);
return _ConnectedTitleState();
case ConnectionStatus.connecting:
return _buildConnectingTitleState(context);
return _ConnectingTitleState();
case ConnectionStatus.disconnected:
return _buildDisconnectedTitleState(context, _client);
return _DisconnectedTitleState(client: _client);
default:
return const Offstage();
}
@@ -223,8 +217,11 @@ class StreamChannelListHeader extends StatelessWidget
},
);
}
}
Widget _buildConnectedTitleState(BuildContext context) {
class _ConnectedTitleState extends StatelessWidget {
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
return Text(
context.translations.streamChatLabel,
@@ -233,33 +230,43 @@ class StreamChannelListHeader extends StatelessWidget
),
);
}
}
Widget _buildConnectingTitleState(BuildContext context) => Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 16,
width: 16,
child: Center(
child: CircularProgressIndicator(),
),
class _ConnectingTitleState extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
height: 16,
width: 16,
child: Center(
child: CircularProgressIndicator(),
),
const SizedBox(width: 10),
Text(
context.translations.searchingForNetworkText,
style:
StreamChannelListHeaderTheme.of(context).titleStyle?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
);
),
const SizedBox(width: 10),
Text(
context.translations.searchingForNetworkText,
style: StreamChannelListHeaderTheme.of(context).titleStyle?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
);
}
}
Widget _buildDisconnectedTitleState(
BuildContext context,
StreamChatClient client,
) {
class _DisconnectedTitleState extends StatelessWidget {
const _DisconnectedTitleState({
required this.client,
});
final StreamChatClient client;
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
final channelListHeaderTheme = StreamChannelListHeaderTheme.of(context);
return Row(
@@ -288,7 +295,4 @@ class StreamChannelListHeader extends StatelessWidget
],
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}
@@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template channelName}
/// Displays the current [Channel] name using a [Text] widget.
///
/// The widget uses a [StreamBuilder] to render the channel information
/// image as soon as it updates.
/// {@endtemplate}
class ChannelName extends StatelessWidget {
/// {@macro channelName}
const ChannelName({
super.key,
this.textStyle,
this.textOverflow = TextOverflow.ellipsis,
});
/// The style of the text displayed
final TextStyle? textStyle;
/// How visual overflow should be handled.
final TextOverflow textOverflow;
@override
Widget build(BuildContext context) {
final client = StreamChat.of(context);
final channel = StreamChannel.of(context).channel;
assert(channel.state != null, 'Channel ${channel.id} is not initialized');
return BetterStreamBuilder<String>(
stream: channel.nameStream,
initialData: channel.name,
builder: (context, channelName) => Text(
channelName,
style: textStyle,
overflow: textOverflow,
),
noDataBuilder: (context) => _NameGenerator(
currentUser: client.currentUser!,
members: channel.state!.members,
textStyle: textStyle,
textOverflow: textOverflow,
),
);
}
}
class _NameGenerator extends StatelessWidget {
const _NameGenerator({
required this.currentUser,
required this.members,
this.textStyle,
this.textOverflow,
});
final User currentUser;
final List<Member> members;
final TextStyle? textStyle;
final TextOverflow? textOverflow;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
var channelName = context.translations.noTitleText;
final otherMembers = members.where(
(member) => member.userId != currentUser.id,
);
if (otherMembers.isNotEmpty) {
if (otherMembers.length == 1) {
final user = otherMembers.first.user;
if (user != null) {
channelName = user.name;
}
} else {
final maxWidth = constraints.maxWidth;
final maxChars = maxWidth / (textStyle?.fontSize ?? 1);
var currentChars = 0;
final currentMembers = <Member>[];
otherMembers.forEach((element) {
final newLength = currentChars + (element.user?.name.length ?? 0);
if (newLength < maxChars) {
currentChars = newLength;
currentMembers.add(element);
}
});
final exceedingMembers =
otherMembers.length - currentMembers.length;
channelName =
'${currentMembers.map((e) => e.user?.name).join(', ')} '
'${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
}
}
return Text(
channelName,
style: textStyle,
overflow: textOverflow,
);
},
);
}
}
@@ -0,0 +1,508 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:collection/collection.dart'
show IterableExtension, ListEquality;
import 'package:contextmenu/contextmenu.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/context_menu_items/stream_chat_context_menu_item.dart';
import 'package:stream_chat_flutter/src/dialogs/dialogs.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template channelPreview}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview_paint.png)
///
/// Shows a preview for the current [Channel].
///
/// Uses a [StreamBuilder] to render the channel information image as soon as
/// it updates.
///
/// It is not recommended to use this widget directly as it is the
/// default channel preview widget used by [ChannelListView].
///
/// The UI is rendered based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget's appearance.
/// {@endtemplate}
class ChannelPreview extends StatelessWidget {
/// {@macro channelPreview}
const ChannelPreview({
required this.channel,
super.key,
this.onTap,
this.onLongPress,
this.onViewInfoTap,
this.onImageTap,
this.title,
this.subtitle,
this.leading,
this.sendingIndicator,
this.trailing,
});
/// The action to perform when this widget is tapped or clicked.
final void Function(Channel)? onTap;
/// The action to perform when this widget is long pressed.
final void Function(Channel)? onLongPress;
/// The action to perform when 'View Info' is tapped or clicked.
final ViewInfoCallback? onViewInfoTap;
/// The [Channel] being previewed.
final Channel channel;
/// The action to perform when the image is tapped
final VoidCallback? onImageTap;
/// Widget rendering the title
final Widget? title;
/// Widget rendering the subtitle
final Widget? subtitle;
/// Widget rendering the leading element. By default it shows the
/// [StreamChannelAvatar].
final Widget? leading;
/// Widget rendering the trailing element. By default it shows the date of
/// the last message.
final Widget? trailing;
/// Widget rendering the sending indicator. By default it uses the
/// [StreamSendingIndicator] widget.
final Widget? sendingIndicator;
@override
Widget build(BuildContext context) {
final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
final streamChatState = StreamChat.of(context);
return BetterStreamBuilder<bool>(
stream: channel.isMutedStream,
initialData: channel.isMuted,
builder: (context, data) => AnimatedOpacity(
opacity: data ? 0.5 : 1,
duration: const Duration(milliseconds: 300),
child: ContextMenuArea(
verticalPadding: 0,
builder: (context) => [
StreamChatContextMenuItem(
leading: StreamSvgIcon.user(
color: Colors.grey,
),
title: Text(context.translations.viewInfoLabel),
onClick: () {
Navigator.of(context, rootNavigator: true).pop();
if (onViewInfoTap != null) {
onViewInfoTap?.call(channel);
} else {
showDialog(
context: context,
builder: (_) => ChannelInfoDialog(
channel: channel,
),
);
}
},
),
StreamChatContextMenuItem(
leading: StreamSvgIcon.mute(
color: Colors.grey,
),
title: channel.isGroup
? Text(
context.translations
.toggleMuteUnmuteGroupText(isMuted: channel.isMuted),
)
: Text(
context.translations
.toggleMuteUnmuteUserText(isMuted: channel.isMuted),
),
onClick: () async {
Navigator.of(context, rootNavigator: true).pop();
showDialog(
context: context,
builder: (_) => ConfirmationDialog(
titleText: channel.isGroup
? context.translations
.toggleMuteUnmuteGroupText(isMuted: channel.isMuted)
: context.translations
.toggleMuteUnmuteUserText(isMuted: channel.isMuted),
promptText: channel.isGroup
? context.translations.toggleMuteUnmuteGroupQuestion(
isMuted: channel.isMuted,
)
: context.translations.toggleMuteUnmuteUserQuestion(
isMuted: channel.isMuted,
),
affirmativeText: context.translations
.toggleMuteUnmuteAction(isMuted: channel.isMuted),
onConfirmation: () async {
try {
if (channel.isMuted) {
await channel.unmute();
} else {
await channel.mute();
}
} catch (e) {
showDialog(
context: context,
builder: (_) => MessageDialog(
messageText: e.toString(),
),
);
}
},
),
);
},
),
if (channel.isGroup)
StreamChatContextMenuItem(
leading: StreamSvgIcon.userRemove(
color: Colors.red,
),
title: Text(
context.translations.leaveGroupLabel,
style: const TextStyle(
color: Colors.red,
),
),
onClick: () {
Navigator.of(context, rootNavigator: true).pop();
showDialog(
context: context,
builder: (_) => ConfirmationDialog(
titleText: context.translations.leaveGroupLabel,
promptText:
context.translations.leaveConversationQuestion,
affirmativeText: context.translations.leaveLabel,
onConfirmation: () async {
final userAsMember = channel.state?.members.firstWhere(
(e) =>
e.user?.id ==
StreamChat.of(context).currentUser?.id,
);
try {
await channel.removeMembers([userAsMember!.user!.id]);
} catch (e) {
showDialog(
context: context,
builder: (_) => MessageDialog(
messageText: e.toString(),
),
);
}
},
),
);
},
),
if (!channel.isGroup)
StreamChatContextMenuItem(
leading: StreamSvgIcon.delete(
color: Colors.red,
),
title: Text(
context.translations.deleteConversationLabel,
style: const TextStyle(
color: Colors.red,
),
),
onClick: () {
Navigator.of(context, rootNavigator: true).pop();
showDialog(
context: context,
builder: (_) => ConfirmationDialog(
titleText: context.translations.deleteConversationLabel,
promptText:
context.translations.deleteConversationQuestion,
affirmativeText: context.translations.deleteLabel,
onConfirmation: () async {
try {
await channel.delete();
} catch (e) {
showDialog(
context: context,
builder: (_) => MessageDialog(
messageText: e.toString(),
),
);
}
},
),
);
},
),
],
child: ListTile(
visualDensity: VisualDensity.compact,
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
),
onTap: () => onTap?.call(channel),
onLongPress: () => onLongPress?.call(channel),
leading: leading ??
StreamChannelAvatar(
onTap: onImageTap,
channel: channel,
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: title ??
ChannelName(
textStyle: channelPreviewTheme.titleStyle,
),
),
BetterStreamBuilder<List<Member>>(
stream: channel.state?.membersStream,
initialData: channel.state?.members,
comparator: const ListEquality().equals,
builder: (context, members) {
if (members.isEmpty ||
!members.any((Member e) =>
e.user!.id ==
channel.client.state.currentUser?.id)) {
return const SizedBox();
}
return StreamUnreadIndicator(
cid: channel.cid,
);
},
),
],
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(child: subtitle ?? _Subtitle(channel: channel)),
sendingIndicator ??
Builder(
builder: (context) {
final lastMessage =
channel.state?.messages.lastWhereOrNull(
(m) => !m.isDeleted && !m.shadowed,
);
if (lastMessage?.user?.id ==
streamChatState.currentUser?.id) {
return Padding(
padding: const EdgeInsets.only(right: 4),
child: BetterStreamBuilder<List<Read>>(
stream: channel.state?.readStream,
initialData: channel.state?.read,
builder: (context, data) {
final readList = data.where((it) =>
it.user.id !=
channel.client.state.currentUser?.id &&
(it.lastRead
.isAfter(lastMessage!.createdAt) ||
it.lastRead.isAtSameMomentAs(
lastMessage.createdAt,
)));
final isMessageRead = readList.length >=
(channel.memberCount ?? 0) - 1;
return StreamSendingIndicator(
message: lastMessage!,
size: channelPreviewTheme.indicatorIconSize,
isMessageRead: isMessageRead,
);
},
),
);
}
return const SizedBox();
},
),
trailing ?? _Date(channel: channel),
],
),
),
),
),
);
}
}
class _Date extends StatelessWidget {
const _Date({
required this.channel,
});
final Channel channel;
@override
Widget build(BuildContext context) {
return BetterStreamBuilder<DateTime>(
stream: channel.lastMessageAtStream,
initialData: channel.lastMessageAt,
builder: (context, data) {
final lastMessageAt = data.toLocal();
String stringDate;
final now = DateTime.now();
final startOfDay = DateTime(now.year, now.month, now.day);
if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.millisecondsSinceEpoch) {
stringDate = Jiffy(lastMessageAt.toLocal()).jm;
} else if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay
.subtract(const Duration(days: 1))
.millisecondsSinceEpoch) {
stringDate = context.translations.yesterdayLabel;
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
} else {
stringDate = Jiffy(lastMessageAt.toLocal()).yMd;
}
return Text(
stringDate,
style: StreamChannelPreviewTheme.of(context).lastMessageAtStyle,
);
},
);
}
}
class _Subtitle extends StatelessWidget {
const _Subtitle({
required this.channel,
});
final Channel channel;
@override
Widget build(BuildContext context) {
final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
if (channel.isMuted) {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
StreamSvgIcon.mute(
size: 16,
),
Text(
' ${context.translations.channelIsMutedText}',
style: channelPreviewTheme.subtitleStyle,
),
],
);
}
return StreamTypingIndicator(
channel: channel,
alternativeWidget: _LastMessage(
channel: channel,
),
style: channelPreviewTheme.subtitleStyle,
);
}
}
class _LastMessage extends StatelessWidget {
const _LastMessage({
required this.channel,
});
final Channel channel;
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.centerLeft,
child: BetterStreamBuilder<List<Message>>(
stream: channel.state!.messagesStream,
initialData: channel.state!.messages,
builder: (context, data) {
final lastMessage =
data.lastWhereOrNull((m) => !m.shadowed && !m.isDeleted);
if (lastMessage == null) {
return const SizedBox();
}
var text = lastMessage.text;
final parts = <String>[
...lastMessage.attachments.map((e) {
if (e.type == 'image') {
return '📷';
} else if (e.type == 'video') {
return '🎬';
} else if (e.type == 'giphy') {
return '[GIF]';
}
return e == lastMessage.attachments.last
? (e.title ?? 'File')
: '${e.title ?? 'File'} , ';
}),
lastMessage.text ?? '',
];
text = parts.join(' ');
final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
return Text.rich(
_getDisplayText(
text,
lastMessage.mentionedUsers,
lastMessage.attachments,
channelPreviewTheme.subtitleStyle?.copyWith(
color: channelPreviewTheme.subtitleStyle?.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic
: FontStyle.normal,
),
channelPreviewTheme.subtitleStyle?.copyWith(
color: channelPreviewTheme.subtitleStyle?.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic
: FontStyle.normal,
fontWeight: FontWeight.bold,
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,
);
},
),
);
}
TextSpan _getDisplayText(
String text,
List<User> mentions,
List<Attachment> attachments,
TextStyle? normalTextStyle,
TextStyle? mentionsTextStyle,
) {
final textList = text.split(' ');
final resList = <TextSpan>[];
for (final e in textList) {
if (mentions.isNotEmpty &&
mentions.any((element) => '@${element.name}' == e)) {
resList.add(TextSpan(
text: '$e ',
style: mentionsTextStyle,
));
} else if (attachments.isNotEmpty &&
attachments
.where((e) => e.title != null)
.any((element) => element.title == e)) {
resList.add(TextSpan(
text: '$e ',
style: normalTextStyle?.copyWith(fontStyle: FontStyle.italic),
));
} else {
resList.add(TextSpan(
text: e == textList.last ? e : '$e ',
style: normalTextStyle,
));
}
}
return TextSpan(children: resList);
}
}
@@ -1,6 +1,5 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/group_avatar.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image.png)
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that renders a preview of the message text.
@@ -1,203 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/group_avatar.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image_paint.png)
///
/// It shows the current [Channel] image.
///
/// ```dart
/// class MyApp extends StatelessWidget {
/// final StreamChatClient client;
/// final Channel channel;
///
/// MyApp(this.client, this.channel);
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// debugShowCheckedModeBanner: false,
/// home: StreamChat(
/// client: client,
/// child: StreamChannel(
/// channel: channel,
/// child: Center(
/// child: ChannelAvatar(
/// channel: channel,
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// }
/// ```
///
/// The widget uses a [StreamBuilder] to render the channel information
/// image as soon as it updates.
///
/// By default the widget radius size is 40x40 pixels.
/// Set the property [constraints] to set a custom dimension.
///
/// The widget renders the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance.
@Deprecated(
"'ChannelAvatar' is deprecated and shouldn't be used. "
"Please use 'StreamChannelAvatar' instead.",
)
class ChannelAvatar extends StatelessWidget {
/// Instantiate a new ChannelImage
const ChannelAvatar({
super.key,
this.channel,
this.constraints,
this.onTap,
this.borderRadius,
this.selected = false,
this.selectionColor,
this.selectionThickness = 4,
});
/// [BorderRadius] to display the widget
final BorderRadius? borderRadius;
/// The channel to show the image of
final Channel? channel;
/// The diameter of the image
final BoxConstraints? constraints;
/// The function called when the image is tapped
final VoidCallback? onTap;
/// If image is selected
final bool selected;
/// Selection color for image
final Color? selectionColor;
/// Thickness of selection image
final double selectionThickness;
@override
Widget build(BuildContext context) {
final streamChat = StreamChat.of(context);
final channel = this.channel ?? StreamChannel.of(context).channel;
assert(channel.state != null, 'Channel ${channel.id} is not initialized');
final chatThemeData = StreamChatTheme.of(context);
final colorTheme = chatThemeData.colorTheme;
final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme;
return BetterStreamBuilder<String>(
stream: channel.imageStream,
initialData: channel.image,
builder: (context, channelImage) {
Widget child = ClipRRect(
borderRadius: borderRadius ?? previewTheme?.borderRadius,
child: Container(
constraints: constraints ?? previewTheme?.constraints,
decoration: BoxDecoration(color: colorTheme.accentPrimary),
child: InkWell(
onTap: onTap,
child: CachedNetworkImage(
imageUrl: channelImage,
errorWidget: (_, __, ___) => Center(
child: Text(
channel.name?[0] ?? '',
style: TextStyle(
color: colorTheme.barsBg,
fontWeight: FontWeight.bold,
),
),
),
fit: BoxFit.cover,
),
),
),
);
if (selected) {
child = ClipRRect(
key: const Key('selectedImage'),
borderRadius: BorderRadius.circular(selectionThickness) +
(borderRadius ??
previewTheme?.borderRadius ??
BorderRadius.zero),
child: Container(
constraints: constraints ?? previewTheme?.constraints,
color: selectionColor ?? colorTheme.accentPrimary,
child: Padding(
padding: EdgeInsets.all(selectionThickness),
child: child,
),
),
);
}
return child;
},
noDataBuilder: (context) {
final currentUser = streamChat.currentUser!;
final otherMembers = channel.state!.members
.where((it) => it.userId != currentUser.id)
.toList(growable: false);
// our own space, no other members
if (otherMembers.isEmpty) {
return BetterStreamBuilder<User>(
stream: streamChat.client.state.currentUserStream.map((it) => it!),
initialData: currentUser,
builder: (context, user) => StreamUserAvatar(
borderRadius: borderRadius ?? previewTheme?.borderRadius,
user: user,
constraints: constraints ?? previewTheme?.constraints,
onTap: onTap != null ? (_) => onTap!() : null,
selected: selected,
selectionColor: selectionColor ?? colorTheme.accentPrimary,
selectionThickness: selectionThickness,
),
);
}
// 1-1 Conversation
if (otherMembers.length == 1) {
final member = otherMembers.first;
return BetterStreamBuilder<Member>(
stream: channel.state!.membersStream.map(
(members) => members.firstWhere(
(it) => it.userId == member.userId,
orElse: () => member,
),
),
initialData: member,
builder: (context, member) => StreamUserAvatar(
borderRadius: borderRadius ?? previewTheme?.borderRadius,
user: member.user!,
constraints: constraints ?? previewTheme?.constraints,
onTap: onTap != null ? (_) => onTap!() : null,
selected: selected,
selectionColor: selectionColor ?? colorTheme.accentPrimary,
selectionThickness: selectionThickness,
),
);
}
// Group conversation
return StreamGroupAvatar(
members: otherMembers,
borderRadius: borderRadius ?? previewTheme?.borderRadius,
constraints: constraints ?? previewTheme?.constraints,
onTap: onTap,
selected: selected,
selectionColor: selectionColor ?? colorTheme.accentPrimary,
selectionThickness: selectionThickness,
);
},
);
}
}

Some files were not shown because too many files have changed in this diff Show More