* chore(flutter): change BoxFit of VLC player

* chore(flutter): remove unused imports

* chore(flutter sample): tweak breakpoints, ignore some lints

* chore(flutter): minor code readability improvements

* chore(flutter): extract out platform_widget_builder to its own "package"

* fix(flutter): fix a theme bug with PlatformDialog

* chore(persistence): test plugin on macOS

* Generated the macos runner for the example
* Re-generated the android runner because it was using the v1 embedding

* chore(persistence): test plugin on web

* chore(persistence): add Windows runner to example

* chore(persistence): add linux runner to example

* feat(flutter): branded context menus

* chore(flutter): fix bug where lines would be shown in between menu items

* chore(flutter): import/export fixes

* chore(flutter): move file

* chore(flutter): /src/channel

Also refactors several /channel widgets to use class widgets instead of methods. Some methods, however, have been left as those cases do actually produce cleaner code.

chore(flutter): fix bad directory placement

chore(flutter): fix bad directory placement

* chore(flutter): more directory organization

* chore(flutter): navigator improvements

* chore(flutter): void async improvements

* chore(flutter): fix imports/exports

* docs(flutter): documentation updates for /attachment

* docs(flutter): doc updates

For:
* /avatars
* /bottom_sheets
* /channel

* feat(flutter): selectable message text on web & desktop

* Squashed commit of the following:

commit 50b249edb6b2ee2d744e4e00e97f81979026aab4
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 14:55:05 2022 -0400

    chore(flutter): wrapping up docs

commit 7d289ae45e20c0109bcdb4496f7b693e2adc4ef9
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 14:42:09 2022 -0400

    chore(flutter): more docs & typedefs

commit 6b36f6f3864f15b52e441d58ee7e62577ff8f843
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 14:22:34 2022 -0400

    chore(flutter): more doc fixes

commit 1a9a6de3c836e9ee5d6373033f1d9a7da830bddc
Merge: 59188f5d 88abf99f
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 13:17:52 2022 -0400

    Merge branch 'desk_web_dev' into utils

commit 59188f5d9c1dd68edd531086cdfd9d42faf93fc8
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 12:48:16 2022 -0400

    chore(flutter): add todo

commit 9b4a986ca5757f284677eb5335637d11ccb1d642
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 12:43:12 2022 -0400

    chore(flutter): more docs and typedefs updates

commit 8786d2dd82b94f673b36371fb2b33a1ebcc9840a
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 12:14:14 2022 -0400

    chore(flutter): more typedef and doc template updates

commit 9e906beb0e7a054c13a32c8c8c5b0e2a9e27bb21
Author: GroovinChip <[email protected]>
Date:   Wed Mar 16 10:46:04 2022 -0400

    chore(flutter): create /uitils and move files

    Fixes subsequent imports/exports

* chore(flutter): update file_selector plugin

* chore(flutter): include utils/typedefs.dart in library export

* Squashed commit of the following:

commit 501228afa614b0d740858dec1ebedd3f671e2c96
Author: GroovinChip <[email protected]>
Date:   Thu Mar 17 16:18:12 2022 -0400

    feat(flutter): ensure fullscreen media feature works on ALL platforms

commit cd0aab92bc7d90af03258829a5aae66125578af5
Author: GroovinChip <[email protected]>
Date:   Thu Mar 17 14:26:48 2022 -0400

    feat(flutter): working conditional import for initializing VLC

* fix(flutter): remove stray import causing web to break

* chore(flutter): ensure context menus can show on web in sample

* feat(flutter) FullScreenMedia context menus for Web

* feat(flutter): branded context menus

* chore(flutter): no success dialog on file download (web)

* chore(flutter): remove platform UI packages

* chore(flutter): properly show/hide delete message menu item

Also deletes deprecated PlatformDialog

* feat(flutter): 'view info' channel preview context menu

* feat(flutter): more menu actions for ChannelPreview

Also new ConfirmationDialog and add missing docs

* feat(flutter/localizations): translations related to channel mutes

* chore(flutter): remove prints and fix super.didChangeDependencies order

* chore(flutter): rename some things in ChannelBottomSheet

* chore(flutter): more sensible name in TypingIndicator

* chore(flutter): remove unused code in MessageWidget

* feat(flutter): ChannelInfoDialog

* chore(flutter): organize files more

Also refactors out some internal MessageListView widgets

* fix(flutter): fix render overflow issue

* chore(flutter): move MediaListView

* chore(flutter): remove unused imports

* fix(flutter): fix weird share menu placement on macos

* chore(flutter): clean up fullscreenmedia conditional imports

* chore(flutter): adjust package exports

* chore(flutter): update dart_vlc

* chore(flutter): clean up platform checks

* chore(flutter): run flutter format on lib

* chore(flutter): minor sample cleanup

* chore(flutter): ensure vlc doesn't get initialized in non-desktop testing environments

* test(flutter): reformat existing tests

* fix(flutter): pass onThreadReplyTap down to ThreadReplyButton

* First round of new tests

commit a90bed427521123fdaa4c5e276744a224fac2b68
Author: GroovinChip <[email protected]>
Date:   Thu Apr 7 12:34:19 2022 -0400

    test(flutter): Username test

commit d7bc179c20eae2713c9248191518bf1d955104a9
Author: GroovinChip <[email protected]>
Date:   Thu Apr 7 12:30:47 2022 -0400

    chore(flutter): remove unused imports

commit 09c9517de2917dc5ccd7de019b50a7875386991c
Author: GroovinChip <[email protected]>
Date:   Thu Apr 7 12:29:58 2022 -0400

    test(flutter): ThreadSeparator test

commit 9422d1129211aaaac9a51ca4d29fe94bde06593c
Author: GroovinChip <[email protected]>
Date:   Thu Apr 7 12:19:43 2022 -0400

    chore(flutter): rename LoadingIndicator parameter

commit 63af943a1a16e78a1e0377d76176420bd7e1fbc7
Author: GroovinChip <[email protected]>
Date:   Thu Apr 7 11:47:46 2022 -0400

    test(flutter): FloatingDateDivider test

commit d8e67cf6fca2b76ac091a9047104c0910d5fdfef
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 15:19:39 2022 -0400

    test(flutter): InputAttachment tests

commit d2b7d5dd3304c8be6f74bf5b0281d7a95dd027ec
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 14:41:40 2022 -0400

    test(flutter): DmCheckbox tests

commit 01567f235a2f00880bb530b232489e3790159b25
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 14:16:08 2022 -0400

    chore(flutter): remove unused imports

commit c9625858e840df6082506ae9374f1d2710053cbd
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 14:15:44 2022 -0400

    test(flutter): CountdownButton tests

commit d770474bb11ee8b36a3b0e18db31695fd812a6b3
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 14:11:37 2022 -0400

    test(flutter): CommandButton tests

commit 95e63706d1dd971373d39d471894a38bb2a634f2
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 14:07:43 2022 -0400

    test(flutter): ClearInputItem tests

commit 4156586945c91408cb7943b6c57c2ae560a2857c
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 14:03:59 2022 -0400

    test(flutter): AttachmentButton tests

commit bba042806326999aabf338636c6e33900de86160
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 13:00:42 2022 -0400

    test(flutter): golden test for SendButton

commit e2cd0f00b9eeb8d81399f6316edb298234de12dc
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 12:56:52 2022 -0400

    chore(flutter): deal with lints

commit b4e045f9cbd54c4d42c3f30acfe72d5f8ccf9d82
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 12:56:00 2022 -0400

    test(flutter): SendButton test

commit 4e3d5db0bddb97e508fcdda2e7272fe31e70808f
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 12:38:21 2022 -0400

    test(flutter): KeyboardShortcutRunner tests

commit 0dfd7a0bd6fd6bc76902e87f62ac29da0b01bac6
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 12:17:30 2022 -0400

    test(flutter): UploadProgressIndicator

commit ec50c8aea716bdc19e4308e367c8331211c61ea3
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 11:47:53 2022 -0400

    test(flutter): SendingIndicator tests

commit e87bd1450fb5bfb713a867c7e831968f76428848
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 11:32:35 2022 -0400

    test(flutter): more gallery tests

commit 93d46ca649277fe6d289a70f0bdd23584fd0ab94
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 11:17:29 2022 -0400

    test(flutter): remove bad mock

commit c7b03a39f0f3ca7a334dfb908f3473b5e4deab38
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 10:45:45 2022 -0400

    chore(flutter): minor refactor

commit 33aeb7b5ed398287cb930f3b4702f47a82fce2c6
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 09:49:07 2022 -0400

    test(flutter): MessageDialog tests

commit 220b3ae591e135d9606254666e04d4771945d0ae
Author: GroovinChip <[email protected]>
Date:   Tue Mar 29 09:42:51 2022 -0400

    test(flutter): DeleteMessageDialog tests

commit db508478bb96749971f563de87b6c2a49e1e4e57
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 14:46:41 2022 -0400

    test(flutter): ConfirmationDialog tests

commit e8172dad6bfe18f40b65554ef2c6c16d7437cb56
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 14:35:50 2022 -0400

    test(flutter): ChannelInfoDialog test

commit 04c21458a601af3c3f4a5fc5ce9e8b61493c208c
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 13:57:02 2022 -0400

    test(flutter): DownloadMenuItem tests

commit afee1dace543e2fef8543f9b035f684b2ee00f3b
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 13:53:21 2022 -0400

    test(flutter): StreamChatContextMenuItem tests

commit 4616b1a973d0cf5224b5a71914c9908339c8462a
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 13:02:07 2022 -0400

    test(flutter): golden test for ErrorAlertSheet

commit 22fb8241ae333f57ade8e8550f87695929836b2b
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 12:59:01 2022 -0400

    test(flutter): golden test for EditMessageSheet

commit 0be79964827f344ba66e99cb9e5bd3d627aed01d
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 12:56:08 2022 -0400

    test(flutter): golden test for AttachmentModalSheet

commit 0ae842a98143478885bc211c788353c8bca57b40
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 12:53:26 2022 -0400

    test(flutter): GroupAvatar tests

commit 697d294c6c7e1bba1d9089a53d5c60d8609ea64e
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 12:30:25 2022 -0400

    test(flutter): UserAvatar tests

commit c03ba4124068c83d3a49c4db00ca18a659801c23
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 11:04:55 2022 -0400

    test(flutter): ErrorAlertSheet test

commit 3414f28530b0e22614b83af20361f21dd4967fae
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 10:45:37 2022 -0400

    test(flutter): EditMessageSheet test

commit b2962b641dfbf14c11d3da7f80a4be27f9980d90
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 10:37:18 2022 -0400

    test(flutter): AttachmentModalSheet test

    test(flutter): AttachmentModalSheet test

    chore(flutter): update exports

commit 7d7e278692c65dea84e7ef3b2d3c1311ebb28408
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 09:52:29 2022 -0400

    test(flutter): more organization

commit 4479b649835acc62d8a0553f9505a0af8077abfc
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 09:33:06 2022 -0400

    test(flutter): tweak AttachmentHandler tests

commit b4a41106567e3c08105ce8dc272b1ebf40b3b0bb
Author: GroovinChip <[email protected]>
Date:   Mon Mar 28 09:24:00 2022 -0400

    chore(flutter): streamline attachment handling

    * MobileAttachmentHandler for streamlining related code for mobile
    * Break out DesktopAttachmentHandler into its own file
    * Update minSdk version in Android runner

commit bfb06d31b79cd941d8b611c7e89e2d7728a394cb
Merge: a44b3b39 15c83d03
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 13:00:27 2022 -0400

    Merge branch 'desk_web_dev' into tests

commit a44b3b390adc25a00d1597bb1460973e8555fa9e
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:59:14 2022 -0400

    chore(flutter): start cleaning up test dir

commit de104a3491d942d12f72dd66152b62c6e8b10a5b
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:56:48 2022 -0400

    test(flutter): VideoAttachment test

commit 4748a1c081a3272f7fdbdeb69c501344e221e01e
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:52:30 2022 -0400

    test(flutter): UrlAttachment test

commit 54408ef70f07c60adf8350553fdf3173e5edd7c7
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:48:24 2022 -0400

    test(flutter): ImageGroup test

commit 811cff069e490c6bd03be0c0ac2f5ec86fb990e9
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:46:04 2022 -0400

    test(flutter): ImageAttachment test

commit a72677976c6524e76793432b0520e99cac4e7a62
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:34:06 2022 -0400

    test(flutter): GiphyAttachment test

commit 32ddac022e2c149726b4b6607f41850d556055b8
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:28:01 2022 -0400

    test(flutter): FileAttachment test

commit 6534958cab775be1fb9bda56e67caa4746aa1502
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:14:25 2022 -0400

    test(flutter): AttachmentError test

commit 718a83c10c4e883ce01c9c002ced24de671d30b9
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 12:06:43 2022 -0400

    test(flutter): AttachmentUploadStateBuilder test

commit a05108c460e36b2e425e4ec06f8bf408b0c20e97
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 11:44:34 2022 -0400

    chore(flutter): minor cleanup

commit 6e73038ac3714a2bd96f8c587af7f743bf2b2023
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 11:43:23 2022 -0400

    test(flutter): AttachmentTitle test

commit c6ee52ec951877feef31dc1427eec3bfc2bd8fcb
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 11:43:11 2022 -0400

    test(flutter): minor cleanup

commit 09845cd845aa8c546043f4ae04805a8e6f53f61c
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 11:42:52 2022 -0400

    chore(flutter): improve device segmentation & vlc init check

commit 172e6b25a064b5147b7641f29ebff3c920156f70
Author: GroovinChip <[email protected]>
Date:   Fri Mar 25 10:37:43 2022 -0400

    test(flutter): attachment_handler tests

commit 5f5a91c70cd7c069f39518f1f50bb74ab484c7e9
Author: GroovinChip <[email protected]>
Date:   Wed Mar 23 13:55:27 2022 -0400

    test(flutter): test PlatformWidgetBuilder & DesktopWidgetBuilder

commit 5a457b7557cb4127b74ee022389a24e5e9150601
Author: GroovinChip <[email protected]>
Date:   Wed Mar 23 13:55:05 2022 -0400

    chore(flutter): make base platform widgets testable

    Also fix environment check in stream_chat.dart

commit 88fb16018103c581236aeebd978e705b223b2243
Author: GroovinChip <[email protected]>
Date:   Wed Mar 23 11:59:19 2022 -0400

    chore(flutter): add readme to conditional_parent_widget library

commit e9307f8a6a5639d59358c245f033f9d9b209fe20
Author: GroovinChip <[email protected]>
Date:   Wed Mar 23 11:54:54 2022 -0400

    test(flutter): ConditionalParentBuilder tests

* chore(flutter): add thumblr dependency

* chore(flutter): fix typos

* chore(flutter): properly order super calls

* feat(flutter) video thumbnails on desktop

* chore(flutter): fix incorrect file name

* chore(flutter): fix docs for generateVideoThumbnail

* chore(flutter): remove incorrect comment

* chore(flutter): run flutter format

* chore(flutter): ignore lint

* chore(flutter): flutter format

* chore(flutter): fix tests

* fix(flutter): fix MessageListView bug caused by rebase

* chore(flutter): migrate desktop stuff to v4/stream_message_input.dart

* chore(flutter): migrate over desktop changes for message replies

* chore(flutter): fix message replies on desktop

* chore(flutter): address lints

* chore(flutter): update dart_vlc

* chore(flutter): melos clean & update photo_view

* fix(ui): port #1118

* fix(ui): use attachmentButtonBuilder

* fix(ui): fix file attachment

* format

* fix(ui): fix message bottom row

* chore(flutter sample): use modern window look

* chore(flutter): pass userAvatarBuilder through to MessageWidgetContent

Also pass onUserAvatarTap & userAvatarBuilder through to UserAvatarTransform

* chore: fix MessageWidget menu actions

* fix(flutter): fix "enter" sending empty messages in StreamMessageInput

* fix(flutter): fix Giphys not loading in quoted messages

* chore(flutter): update thumblr

* feat(flutter): keyboard navigation for gallery items

* fix(ui): fix double date divider

* fix(ui): add scroll to bottom back

* fix(ui): use mouse region

* fix(ui): remove backbutton in desktop example

* fix(ui): rename full screen media widget

* feat(flutter): elevation prop for gallery header

* chore(flutter): update dart_vlc

* fix(flutter): fix "show in chat" showing when it shouldn't

* fix(flutter): fix "show in chat" for mobile

* docs(flutter): minor doc update

* chore(flutter): add onMessageSwiped in example app

* chore(flutter): pass usernameBuilder through to BottomRow

* fix(flutter): hacky fix for chonky link previews

* fix(flutter): fix chonky text messages

* feat(flutter): press "esc" to close gallery

Also refactors KeyboardShortcutRunner API

* chore(flutter): fix innaccurate name

* fix(ui): fix attachment + long text sizing

* add key to channelpage

* fix(ui): better example

* fix format

* fix example

* chore(flutter): revert modern window look

* improvement(flutter): improve `GalleryNavigationItem`

* Make buttons more visible
* Spacing from the sides of the screen

* fix video thumbnail

* fix split view

* fix(ui): fix giphy in thread

* feat(flutter): desktop message reactions

commit 4b880ecb6d4dcbf315a7d41d863174817a9c530a
Author: Reuben Turner <[email protected]>
Date:   Thu Jun 9 12:25:21 2022 -0400

    fix(flutter): fixes for desktop reactions

    * Fix reactions for reverse messages
    * Ensure reactions near the edges of MessageListView show properly

commit f511513f8dbb272397e81d08bad33ee60108d738
Author: Salvatore Giordano <[email protected]>
Date:   Thu Jun 9 16:43:03 2022 +0200

    fix(ui): fix DesktopReactionBuilder and Portal

commit c4885648330552f5212c5ba6b771c0f07023f6ae
Merge: 2b710a2b 72b34c21
Author: Reuben Turner <[email protected]>
Date:   Wed Jun 8 14:12:44 2022 -0400

    Merge branch 'desk_web_dev' into desktop_reactions

commit 2b710a2bb610326123d5b7f75c26caa705114c9b
Author: Reuben Turner <[email protected]>
Date:   Mon Jun 6 12:19:12 2022 -0400

    chore(flutter): add debug props

commit e24dabc1de12da8dce697b6d4e1a3617e9835549
Author: Reuben Turner <[email protected]>
Date:   Mon Jun 6 12:00:03 2022 -0400

    feat(flutter): first pass at desktop reactions popup on mouse hover

* fix(flutter): potential fix for #1199

* fix(flutter): fix non-reversed reactions placement

* refactor(flutter): extract bottom reaction, stacked reaction

with debug props

* fix(flutter): fixes for desktop reactions

* Fix bottom row being rendered above reaction buttons
* Fix padding issues related to avatars

* chore(flutter): add back lost changes from 2b1fb6a

Also removed commented code

* chore(flutter): stacked reactions update

* Show "You" if userId matches the reaction username (per design)
* Ensure long names don't throw off alignment

* chore(flutter): format

* fix(ui): fix mime type generation

* chore(flutter): tweak desktop video

* chore(flutter): add some missing things from develop

* merge develop

* fix merge

* fix merge

* format

* fix tests

* increate test action timeout

* remove test

* refactor(ui,core): remove deprecated code and cleanup

* run builder

* update coverage threasholds

* refactor(ui): remove arrow function from countdown button

* Update packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart

* Update packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart

* update comment

* fix(ui): fix attachment + link solution

* fix(ui): fix attachment + link solution

* chore(flutter) update changelog and version

* chore(localizations): update changelog and versions

* chore(llc,core,ui,persistence,localizations): update changelog and pubspec

* chore(llc,core,ui,persistence,localizations): update changelog and pubspec

* fix example

* fix deps

* fix(ui): fix enter sending the message while in slowmode

* fix(ui): fix reaction list

* fix web

* fix(ui): fix image download when using assetUrl

* fix(ui): fix typo in changelog

* fix(ui): fix gallery navigation

* fix(ui): fix giphy sending size

* fix(ui): add keyboard shorcuts to gallery view

* fix(ui): fix gallery footer

* fix(core): allow scroll notification bubbling in lazy load scroll view

* fix(ui): fix lazy load scroll view

* fix(ui): fix loading to unread position

* fix(ui): fix last message preview

* chore(ui): update changelog

* fix tests

* fix(llc): fix connecting while connecting and disconneting (#1237)

* fix(llc): fix connecting while connecting and disconneting

* chore(llc): update changelog

* chore(llc): add test

* chore(llc): improve test

* fix(llc): _resetRequestFlags regardless of the connection status

* feat(llc): add support for AttachmentFileUploaderProvider. (#1246)

* feat(llc): add support for `AttachmentFileUploaderProvider`.

Signed-off-by: xsahil03x <[email protected]>

* chore(llc): update CHANGELOG.md

Signed-off-by: xsahil03x <[email protected]>

* refactor(llc): Remove deprecated attachmentFileUploader field.

Signed-off-by: xsahil03x <[email protected]>

* port no localization

* fix(ui): fix jiffy locale initialization (#1255)

* fix(ui): fix jiffy locale initialization

* chore(ui): update changelog

* fix(ui): cleanup

* chore(llc,core,ui,persistence,localizations): align changelog

* chore(llc,core,ui,persistence,localizations): add links to 4.x changelogs

* fix(ui): fix date divider not showing (#1259)

* fix(ui): fix date divider not showing

* chore(ui): update changelog

* chore(ui): minor changes

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): fix separator

Signed-off-by: xsahil03x <[email protected]>

Co-authored-by: Sahil Kumar <[email protected]>

* chore(ui): add contextmenu requirements for web (#1261)

* fix unread separator

* feat(ui): `StreamChatConfiguration` class (#1128)

* feat: StreamChatConfiguration class

See #1125 for more details

* chore(flutter): update changelog

* fix analysis

* cleanup

* feat(ui): add enforceUniqueReactions to `StreamChatConfiguration` widget

* add tests

* refactor(ui): rename StreamChatConfigurationProvider and StreamChatConfiguration

* refactor(ui): rename StreamChat.config to streamChatConfigData

* refactor(ui): make enforceUniqueReactions non nullable

* chore(ui): fix analysis errors

* refactor(ui): make config fields non nullable

* chore(ui): format

Co-authored-by: Salvatore Giordano <[email protected]>

* feat(ui, core): add StreamAutocomplete (#1263)

* feat(ui, core): add stream_autocomplete.dart

Signed-off-by: xsahil03x <[email protected]>

* refactor(core, ui): refactor message input controller

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): minor fixes

Signed-off-by: xsahil03x <[email protected]>

* feat(ui): add StreamAutocompleteOptions widget

Signed-off-by: xsahil03x <[email protected]>

* Update packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart

* fix(ui): cleanup

* chore(core, ui): cleanup

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): ignore no empty block

* chore(ui): more cleanup

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): fix command button for bigger screens, other fixes.

Signed-off-by: xsahil03x <[email protected]>

Co-authored-by: Salvatore Giordano <[email protected]>

* fix(ui): increase default file limit to 100MB (#1279)

* fix(ui): increase default file limit to 100MB

* Update packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart

* fix(ui): export kDefaultMaxAttachmentSize and use it in `StreamAttachmentPicker` default

* feat(ui): show dialog after clicking on the camera button and permission is denied (#1262)

* feat(ui): show dialog after clicking on the camera button and permission is denied

* chore(ui): update changelog

* Remove permission_handler plugin and refactor

* cleanup

* cleanup

* add tests

* fix(ui): reset the permission state on resume

* cleanup

* fix concurrent calls

* fix(ui): cleanup

* fix(ui): add try catch

* fix(llc, ui): copy file to tempdir before uploading a file and do not serialize bytes (#1285)

* fix(llc, ui): copy file to tempdir before uploading a file and do not serialize bytes

* fix(llc): do not save failed messages with attachments on web

* fix(llc): replace every with any

* chore(llc): add test

* fix analysis

* fix analysis

* chore(llc,core,ui,persistence): align changelog with v4

* fix(ui): do not linkify system message mentions

* chore(llc,core,ui,persistence,localizations): update changelog and pubspec (#1289)

* chore(ui): update deps

* chore(llc,core,ui,persistence,localizations): update changelog and pubspec

* chore(ui): publish beta.2

* feat(core,ui): add StreamMemberListView, StreamMemberGridView, and StreamMemberListController (#1291)

* feat(core,ui): add StreamMemberListView, StreamMemberGridView, and StreamMemberListController

* chore(core,ui): update changelog

* fix(ui): fix exports

* chore(ui): add tests

* chore(ui): add StreamMemberGridTile typedef

* chore(ui): minor changes

Signed-off-by: xsahil03x <[email protected]>

Co-authored-by: Sahil Kumar <[email protected]>

* fix(ui): do not send empty message

* refactor(ui,localization): removed emoji picker and connected (#1292)

* refactor(ui,localization): removed emoji picker and connected

* chore(ui,localizations): update changelog

* chore(repo): update ui coverage threashold

* doc(ui): add autocomplete_triggers guide

* chore(ui): add tests for isOnlyEmoji

* chore(ui): add tests for isOnlyEmoji

* feat(ui): add `StreamAutocompleteTrigger.triggerOnlyAfterSpace`

* fix(llc): Fix `Channel.removeMessage` not able to remove thread message. (#1294)

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): flutter format

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): fix `stopTyping` unhandled exceptions when network is off or spotty. (#1296)

* feat(llc, ui): Introduce `keyStrokeHandler` to properly handle keyStrokes.

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): update CHANGELOG.md

Signed-off-by: xsahil03x <[email protected]>

* test(llc): add key_stroke_handler_test.dart

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): update CHANGELOG.md

Signed-off-by: xsahil03x <[email protected]>

* fix(core): only handle the channel list event if the state is success.

Signed-off-by: xsahil03x <[email protected]>

* feat(llc): add `hide_history` flag in `channel.addMembers`.

Signed-off-by: xsahil03x <[email protected]>

* refactor(llc): Convert positional argument to optional arguments

Signed-off-by: xsahil03x <[email protected]>

* test(llc): update tests

Signed-off-by: xsahil03x <[email protected]>

* perf: improve image resizing

* fix(core): use copyWith while setting controller items.

Signed-off-by: xsahil03x <[email protected]>

* feat(ui): Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params to `StreamMessageWidget` and `StreamAttachmentPicker` to customize the appearance of image thumbnails.

Signed-off-by: xsahil03x <[email protected]>

* fix analysis

* Release v4.5.0 (#1308)

* chore(llc, ui, core): bump version to v4.5.0

Signed-off-by: xsahil03x <[email protected]>

* chore(ui, core): update CHANGELOG.md

Signed-off-by: xsahil03x <[email protected]>

Signed-off-by: xsahil03x <[email protected]>

* third party video integration

* fix llc

* refactor: clean up and add docs

* chore(llc): update changelog

* chore(llc): add tests

* chore(llc): add more tests

* fix analysis

* chore(llc): add more tests

* chore(llc): add more tests

* fix(llc): only start listening to event once connected.

Signed-off-by: xsahil03x <[email protected]>

* chore(llc): update CHANGELOG.md

Signed-off-by: xsahil03x <[email protected]>

* chore(llc): move files to correct folders.

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): Fix message text hiding because of a flutter bug (#1327)

* fix(ui): remove clip

* chore(ui): update changelog

* cleanup

* fix(ui): hitting "enter" on the android keyboard sends the message instead of going to a new line.

Signed-off-by: xsahil03x <[email protected]>

* feat(ui): add support for additional text field params in message input.

Signed-off-by: xsahil03x <[email protected]>

* fix analysis

* remove print

* chore(llc,core,ui): update pubspec and changelogs (#1333)

* fix(ui): show placeholder for web video thumbs

* fix file download on web

* fix reply and show buttons

* remove tests

* treat svg as files

* fix(ui): change default image resize to clip

* solve some todos

* polish v5 fixing analysis issues

* feat(ui): stream attachment picker bottom sheet (#1336)

* refactor(ui): improve stream_attachment_handler.dart, add stream_attachment_picker_bottom_sheet.dart

Signed-off-by: xsahil03x <[email protected]>

* feat(ui): add copyWith in command_button.dart

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): remove permission_manager.dart

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): some fixes

Signed-off-by: xsahil03x <[email protected]>

* fix(ui): more fixes

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): fix lints, add doc comments

Signed-off-by: xsahil03x <[email protected]>

* chore(ci): update flutter version

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): more fixes

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): more fixes

Signed-off-by: xsahil03x <[email protected]>

* chore(ui): more fixes

Signed-off-by: xsahil03x <[email protected]>

* chore(ci): update melos and flutter version

Signed-off-by: xsahil03x <[email protected]>

Signed-off-by: xsahil03x <[email protected]>
Co-authored-by: Salvatore Giordano <[email protected]>

* remove todo

* update changelog

* update changelogs

* revert

* update coverage threasholds

Signed-off-by: xsahil03x <[email protected]>
Co-authored-by: GroovinChip <[email protected]>
Co-authored-by: Sahil Kumar <[email protected]>
Co-authored-by: Gordon Hayes <[email protected]>
Co-authored-by: Sacha Arbonel <[email protected]>
This commit is contained in:
Salvatore Giordano
2022-09-30 17:55:57 +02:00
committed by GitHub
co-authored by Sahil Kumar GroovinChip Gordon Hayes Sacha Arbonel
parent 3e1d6e91c7
commit 4f2542291c
542 changed files with 28064 additions and 136563 deletions
@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that displays a user.
///
/// This widget is intended to be used as a Tile in
/// [StreamChannelGridView].
///
/// It shows the user's avatar and name.
///
/// See also:
/// * [StreamChannelGridView]
/// * [StreamUserAvatar]
class StreamChannelGridTile extends StatelessWidget {
/// Creates a new instance of [StreamChannelGridTile] widget.
const StreamChannelGridTile({
super.key,
required this.channel,
this.child,
this.footer,
this.onTap,
this.onLongPress,
});
/// The channel to display.
final Channel channel;
/// The widget to display in the body of the tile.
final Widget? child;
/// The widget to display in the footer of the tile.
final Widget? footer;
/// Called when the user taps this grid tile.
final GestureTapCallback? onTap;
/// Called when the user long-presses on this grid tile.
final GestureLongPressCallback? onLongPress;
/// Creates a copy of this tile but with the given fields replaced with
/// the new values.
StreamChannelGridTile copyWith({
Key? key,
Channel? channel,
Widget? child,
Widget? footer,
GestureTapCallback? onTap,
GestureLongPressCallback? onLongPress,
}) =>
StreamChannelGridTile(
key: key ?? this.key,
channel: channel ?? this.channel,
footer: footer ?? this.footer,
onTap: onTap ?? this.onTap,
onLongPress: onLongPress ?? this.onLongPress,
child: child ?? this.child,
);
@override
Widget build(BuildContext context) {
final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
final child = this.child ??
StreamChannelAvatar(
channel: channel,
borderRadius: BorderRadius.circular(32),
constraints: const BoxConstraints.tightFor(
height: 64,
width: 64,
),
);
final footer = this.footer ??
StreamChannelName(
channel: channel,
textStyle: channelPreviewTheme.titleStyle,
);
return InkWell(
onTap: onTap,
onLongPress: onLongPress,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
child,
footer,
],
),
);
}
}
@@ -0,0 +1,396 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Default grid delegate for [StreamChannelGridView].
const defaultChannelGridViewDelegate =
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4);
/// Signature for the item builder that creates the children of the
/// [StreamChannelGridView].
typedef StreamChannelGridViewIndexedWidgetBuilder
= StreamScrollViewIndexedWidgetBuilder<Channel, StreamChannelGridTile>;
/// A [GridView] that shows a grid of [User]s,
/// it uses [StreamChannelGridTile] as a default item.
///
/// Example:
///
/// ```dart
/// StreamChannelGridView(
/// controller: controller,
/// onChannelTap: (channel) {
/// // Handle channel tap event
/// },
/// onChannelLongPress: (channel) {
/// // Handle channel long press event
/// },
/// )
/// ```
///
/// See also:
/// * [StreamChannelGridTile]
/// * [StreamChannelListController]
class StreamChannelGridView extends StatelessWidget {
/// Creates a new instance of [StreamChannelGridView].
const StreamChannelGridView({
super.key,
required this.controller,
this.gridDelegate = defaultChannelGridViewDelegate,
this.itemBuilder,
this.emptyBuilder,
this.loadMoreErrorBuilder,
this.loadMoreIndicatorBuilder,
this.loadingBuilder,
this.errorBuilder,
this.onChannelTap,
this.onChannelLongPress,
this.loadMoreTriggerIndex = 3,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
});
/// The [StreamUserListController] used to control the grid of users.
final StreamChannelListController controller;
/// A delegate that controls the layout of the children within
/// the [PagedValueGridView].
final SliverGridDelegate gridDelegate;
/// A builder that is called to build items in the [PagedValueGridView].
///
/// The `value` parameter is the [Channel] at this position in the grid.
final StreamChannelGridViewIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the empty state of the grid.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the load more error state of the grid.
final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder;
/// A builder that is called to build the load more indicator of the grid.
final WidgetBuilder? loadMoreIndicatorBuilder;
/// A builder that is called to build the loading state of the grid.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the grid.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// Called when the user taps this grid tile.
final void Function(Channel)? onChannelTap;
/// Called when the user long-presses on this grid tile.
final void Function(Channel)? onChannelLongPress;
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
/// {@endtemplate}
final Axis scrollDirection;
/// {@template flutter.widgets.scroll_view.reverse}
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
/// {@endtemplate}
final bool reverse;
/// {@template flutter.widgets.scroll_view.controller}
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.scroll_view.primary}
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When this is true, the scroll view is scrollable even if it does not have
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by
/// an otherwise focused part of the application,
/// the ScrollAction will be evaluated using this scroll view, for example,
/// when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool? primary;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions. Furthermore, if [primary] is
/// false, then the user cannot scroll if there is insufficient content to
/// scroll, while if [primary] is true, they can always attempt to scroll.
///
/// To force the scroll view to always be scrollable even if there is
/// insufficient content, as if [primary] was true but without necessarily
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
/// object, as in:
///
/// ```dart
/// physics: const AlwaysScrollableScrollPhysics(),
/// ```
///
/// To force the scroll view to use the default platform conventions and not
/// be scrollable if there is insufficient content, regardless of the value of
/// [primary], provide an explicit [ScrollPhysics] object, as in:
///
/// ```dart
/// physics: const ScrollPhysics(),
/// ```
///
/// The physics can be changed dynamically (by providing a new object in a
/// subsequent build), but new physics will only take effect if the _class_ of
/// the provided object changes. Merely constructing a new instance with a
/// different configuration is insufficient to cause the physics to be
/// reapplied. (This is because the final object used is generated
/// dynamically, which can be relatively expensive, and it would be
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
/// {@endtemplate}
final bool shrinkWrap;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by an [IndexedSemantics] widget.
///
/// Defaults to true.
///
/// See also:
///
/// * [IndexedSemantics], for an explanation of how to manually
/// provide semantic indexes.
final bool addSemanticIndexes;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// The number of children that will contribute semantic information.
///
/// Some subtypes of [ScrollView] can infer this value automatically. For
/// example [ListView] will use the number of widgets in the child list,
/// while the [ListView.separated] constructor will use half that amount.
///
/// For [CustomScrollView] and other types which do not receive a builder
/// or list of widgets, the child count must be explicitly provided. If the
/// number is unknown or unbounded this should be left unset or set to null.
///
/// See also:
///
/// * [SemanticsConfiguration.scrollChildCount],
/// the corresponding semantics property.
final int? semanticChildCount;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
/// dismiss the keyboard automatically.
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@override
Widget build(BuildContext context) {
return PagedValueGridView<int, Channel>(
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
scrollController: scrollController,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
clipBehavior: clipBehavior,
gridDelegate: gridDelegate,
itemBuilder: (context, channels, index) {
final channel = channels[index];
final onTap = onChannelTap;
final onLongPress = onChannelLongPress;
final streamChannelGridTile = StreamChannelGridTile(
channel: channel,
onTap: onTap == null ? null : () => onTap(channel),
onLongPress: onLongPress == null ? null : () => onLongPress(channel),
);
return itemBuilder?.call(
context,
channels,
index,
streamChannelGridTile,
) ??
streamChannelGridTile;
},
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.message(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.letsStartChattingLabel,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamScrollViewLoadMoreError.grid(
onTap: controller.retry,
error: Text(
context.translations.loadingChannelsError,
textAlign: TextAlign.center,
),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingChannelsError),
onRetryPressed: controller.refresh,
),
),
);
}
}
@@ -0,0 +1,394 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that displays a channel preview.
///
/// This widget is intended to be used as a Tile in [StreamChannelListView]
///
/// It shows the last message of the channel, the last message time, the unread
/// message count, the typing indicator, the sending indicator and the channel
/// avatar.
///
/// See also:
/// * [StreamChannelAvatar]
/// * [StreamChannelName]
class StreamChannelListTile extends StatelessWidget {
/// Creates a new instance of [StreamChannelListTile] widget.
StreamChannelListTile({
super.key,
required this.channel,
this.leading,
this.title,
this.subtitle,
this.trailing,
this.onTap,
this.onLongPress,
this.tileColor,
this.visualDensity = VisualDensity.compact,
this.contentPadding = const EdgeInsets.symmetric(horizontal: 8),
this.unreadIndicatorBuilder,
this.sendingIndicatorBuilder,
this.selected = false,
this.selectedTileColor,
}) : assert(
channel.state != null,
'Channel ${channel.id} is not initialized',
);
/// The channel to display.
final Channel channel;
/// A widget to display before the title.
final Widget? leading;
/// The primary content of the list tile.
final Widget? title;
/// Additional content displayed below the title.
final Widget? subtitle;
/// A widget to display at the end of tile.
final Widget? trailing;
/// Called when the user taps this list tile.
final GestureTapCallback? onTap;
/// Called when the user long-presses on this list tile.
final GestureLongPressCallback? onLongPress;
/// {@template flutter.material.ListTile.tileColor}
/// Defines the background color of `ListTile`.
///
/// When the value is null,
/// the `tileColor` is set to [ListTileTheme.tileColor]
/// if it's not null and to [Colors.transparent] if it's null.
/// {@endtemplate}
final Color? tileColor;
/// Defines how compact the list tile's layout will be.
///
/// {@macro flutter.material.themedata.visualDensity}
///
/// See also:
///
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all
/// widgets within a [Theme].
final VisualDensity visualDensity;
/// The tile's internal padding.
///
/// Insets a [ListTile]'s contents: its [leading], [title], [subtitle],
/// and [trailing] widgets.
///
/// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used.
final EdgeInsetsGeometry contentPadding;
/// The widget builder for the unread indicator.
final WidgetBuilder? unreadIndicatorBuilder;
/// The widget builder for the sending indicator.
///
/// `Message` is the last message in the channel, Use it to determine the
/// status using [Message.status].
final Widget Function(BuildContext, Message)? sendingIndicatorBuilder;
/// True if the tile is in a selected state.
final bool selected;
/// The color of the tile in selected state.
final Color? selectedTileColor;
/// Creates a copy of this tile but with the given fields replaced with
/// the new values.
StreamChannelListTile copyWith({
Key? key,
Channel? channel,
Widget? leading,
Widget? title,
Widget? subtitle,
VoidCallback? onTap,
VoidCallback? onLongPress,
VisualDensity? visualDensity,
EdgeInsetsGeometry? contentPadding,
bool? selected,
Widget Function(BuildContext, Message)? sendingIndicatorBuilder,
Color? tileColor,
Color? selectedTileColor,
WidgetBuilder? unreadIndicatorBuilder,
Widget? trailing,
}) {
return StreamChannelListTile(
key: key ?? this.key,
channel: channel ?? this.channel,
leading: leading ?? this.leading,
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
onTap: onTap ?? this.onTap,
onLongPress: onLongPress ?? this.onLongPress,
visualDensity: visualDensity ?? this.visualDensity,
contentPadding: contentPadding ?? this.contentPadding,
sendingIndicatorBuilder:
sendingIndicatorBuilder ?? this.sendingIndicatorBuilder,
tileColor: tileColor ?? this.tileColor,
trailing: trailing ?? this.trailing,
unreadIndicatorBuilder:
unreadIndicatorBuilder ?? this.unreadIndicatorBuilder,
selected: selected ?? this.selected,
selectedTileColor: selectedTileColor ?? this.selectedTileColor,
);
}
@override
Widget build(BuildContext context) {
final channelState = channel.state!;
final currentUser = channel.client.state.currentUser!;
final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
final leading = this.leading ??
StreamChannelAvatar(
channel: channel,
);
final title = this.title ??
StreamChannelName(
channel: channel,
textStyle: channelPreviewTheme.titleStyle,
);
final subtitle = this.subtitle ??
ChannelListTileSubtitle(
channel: channel,
textStyle: channelPreviewTheme.subtitleStyle,
);
final trailing = this.trailing ??
ChannelLastMessageDate(
channel: channel,
textStyle: channelPreviewTheme.lastMessageAtStyle,
);
return BetterStreamBuilder<bool>(
stream: channel.isMutedStream,
initialData: channel.isMuted,
builder: (context, isMuted) => AnimatedOpacity(
opacity: isMuted ? 0.5 : 1,
duration: const Duration(milliseconds: 300),
child: ListTile(
onTap: onTap,
onLongPress: onLongPress,
visualDensity: visualDensity,
contentPadding: contentPadding,
leading: leading,
tileColor: tileColor,
selected: selected,
selectedTileColor: selectedTileColor ??
StreamChatTheme.of(context).colorTheme.borders,
title: Row(
children: [
Expanded(child: title),
BetterStreamBuilder<List<Member>>(
stream: channelState.membersStream,
initialData: channelState.members,
comparator: const ListEquality().equals,
builder: (context, members) {
if (members.isEmpty ||
!members.any((it) => it.user!.id == currentUser.id)) {
return const Offstage();
}
return unreadIndicatorBuilder?.call(context) ??
StreamUnreadIndicator(cid: channel.cid);
},
),
],
),
subtitle: Row(
children: [
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: subtitle,
),
),
BetterStreamBuilder<List<Message>>(
stream: channelState.messagesStream,
initialData: channelState.messages,
comparator: const ListEquality().equals,
builder: (context, messages) {
final lastMessage = messages.lastWhereOrNull(
(m) => !m.shadowed && !m.isDeleted,
);
if (lastMessage == null ||
(lastMessage.user?.id != currentUser.id)) {
return const Offstage();
}
return Padding(
padding: const EdgeInsets.only(right: 4),
child:
sendingIndicatorBuilder?.call(context, lastMessage) ??
StreamSendingIndicator(
message: lastMessage,
size: channelPreviewTheme.indicatorIconSize,
isMessageRead: channelState
.currentUserRead!.lastRead
.isAfter(lastMessage.createdAt),
),
);
},
),
trailing,
],
),
),
),
);
}
}
/// A widget that displays the channel last message date.
class ChannelLastMessageDate extends StatelessWidget {
/// Creates a new instance of the [ChannelLastMessageDate] widget.
ChannelLastMessageDate({
super.key,
required this.channel,
this.textStyle,
}) : assert(
channel.state != null,
'Channel ${channel.id} is not initialized',
);
/// The channel to display the last message date for.
final Channel channel;
/// The style of the text displayed
final TextStyle? textStyle;
@override
Widget build(BuildContext context) => 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: textStyle,
);
},
);
}
/// A widget that displays the subtitle for [StreamChannelListTile].
class ChannelListTileSubtitle extends StatelessWidget {
/// Creates a new instance of [StreamChannelListTileSubtitle] widget.
ChannelListTileSubtitle({
super.key,
required this.channel,
this.textStyle,
}) : assert(
channel.state != null,
'Channel ${channel.id} is not initialized',
);
/// The channel to create the subtitle from.
final Channel channel;
/// The style of the text displayed
final TextStyle? textStyle;
@override
Widget build(BuildContext context) {
if (channel.isMuted) {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
StreamSvgIcon.mute(size: 16),
Text(
' ${context.translations.channelIsMutedText}',
style: textStyle,
),
],
);
}
return StreamTypingIndicator(
channel: channel,
style: textStyle,
alternativeWidget: ChannelLastMessageText(
channel: channel,
textStyle: textStyle,
),
);
}
}
/// A widget that displays the last message of a channel.
class ChannelLastMessageText extends StatefulWidget {
/// Creates a new instance of [ChannelLastMessageText] widget.
ChannelLastMessageText({
super.key,
required this.channel,
this.textStyle,
}) : assert(
channel.state != null,
'Channel ${channel.id} is not initialized',
);
/// The channel to display the last message of.
final Channel channel;
/// The style of the text displayed
final TextStyle? textStyle;
@override
State<ChannelLastMessageText> createState() => _ChannelLastMessageTextState();
}
class _ChannelLastMessageTextState extends State<ChannelLastMessageText> {
Message? _lastMessage;
@override
Widget build(BuildContext context) => BetterStreamBuilder<List<Message>>(
stream: widget.channel.state!.messagesStream,
initialData: widget.channel.state!.messages,
builder: (context, messages) {
final lastMessage = messages.lastWhereOrNull(
(m) => !m.shadowed && !m.isDeleted,
);
if (widget.channel.state?.isUpToDate == true) {
_lastMessage = lastMessage;
}
if (_lastMessage == null) return const Offstage();
return StreamMessagePreviewText(
message: _lastMessage!,
textStyle: widget.textStyle,
language: widget.channel.client.state.currentUser?.language,
);
},
);
}
@@ -0,0 +1,421 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Default separator builder for [StreamChannelListView].
Widget defaultChannelListViewSeparatorBuilder(
BuildContext context,
List<Channel> items,
int index,
) =>
const StreamChannelListSeparator();
/// Signature for the item builder that creates the children of the
/// [StreamChannelListView].
typedef StreamChannelListViewIndexedWidgetBuilder
= StreamScrollViewIndexedWidgetBuilder<Channel, StreamChannelListTile>;
/// A [ListView] that shows a list of [Channel]s,
/// it uses [StreamChannelListTile] as a default item.
///
/// This is the new version of [StreamChannelListView] that uses
/// [StreamChannelListController].
///
/// Example:
///
/// ```dart
/// StreamChannelListView(
/// controller: controller,
/// onChannelTap: (channel) {
/// // Handle channel tap event
/// },
/// onChannelLongPress: (channel) {
/// // Handle channel long press event
/// },
/// )
/// ```
///
/// See also:
/// * [StreamChannelListTile]
/// * [StreamChannelListController]
class StreamChannelListView extends StatelessWidget {
/// Creates a new instance of [StreamChannelListView].
const StreamChannelListView({
super.key,
required this.controller,
this.itemBuilder,
this.separatorBuilder = defaultChannelListViewSeparatorBuilder,
this.emptyBuilder,
this.loadingBuilder,
this.errorBuilder,
this.onChannelTap,
this.onChannelLongPress,
this.loadMoreTriggerIndex = 3,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
});
/// The [StreamChannelListController] used to control the list of channels.
final StreamChannelListController controller;
/// A builder that is called to build items in the [ListView].
final StreamChannelListViewIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the list separator.
final PagedValueScrollViewIndexedWidgetBuilder<Channel> separatorBuilder;
/// A builder that is called to build the empty state of the list.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the loading state of the list.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the list.
///
/// If not provided, [StreamChannelListErrorWidget] will be used.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// Called when the user taps this list tile.
final void Function(Channel)? onChannelTap;
/// Called when the user long-presses on this list tile.
final void Function(Channel)? onChannelLongPress;
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
/// {@endtemplate}
final Axis scrollDirection;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by an [IndexedSemantics] widget.
///
/// Defaults to true.
///
/// See also:
///
/// * [IndexedSemantics], for an explanation of how to manually
/// provide semantic indexes.
final bool addSemanticIndexes;
/// {@template flutter.widgets.scroll_view.reverse}
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
/// {@endtemplate}
final bool reverse;
/// {@template flutter.widgets.scroll_view.controller}
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.scroll_view.primary}
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When this is true, the scroll view is scrollable even if it does not have
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by an otherwise focused part of the
/// application, the ScrollAction will be evaluated using this scroll view,
/// for example, when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollController] is null.
final bool? primary;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
/// {@endtemplate}
final bool shrinkWrap;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions. Furthermore, if [primary] is
/// false, then the user cannot scroll if there is insufficient content to
/// scroll, while if [primary] is true, they can always attempt to scroll.
///
/// To force the scroll view to always be scrollable even if there is
/// insufficient content, as if [primary] was true but without necessarily
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
/// object, as in:
///
/// ```dart
/// physics: const AlwaysScrollableScrollPhysics(),
/// ```
///
/// To force the scroll view to use the default platform conventions and not
/// be scrollable if there is insufficient content, regardless of the value of
/// [primary], provide an explicit [ScrollPhysics] object, as in:
///
/// ```dart
/// physics: const ScrollPhysics(),
/// ```
///
/// The physics can be changed dynamically (by providing a new object in a
/// subsequent build), but new physics will only take effect if the _class_ of
/// the provided object changes. Merely constructing a new instance with a
/// different configuration is insufficient to cause the physics to be
/// reapplied. (This is because the final object used is generated
/// dynamically, which can be relatively expensive, and it would be
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
/// dismiss the keyboard automatically.
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@override
Widget build(BuildContext context) {
return PagedValueListView<int, Channel>(
scrollDirection: scrollDirection,
padding: padding,
physics: physics,
reverse: reverse,
controller: controller,
scrollController: scrollController,
primary: primary,
shrinkWrap: shrinkWrap,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
dragStartBehavior: dragStartBehavior,
cacheExtent: cacheExtent,
clipBehavior: clipBehavior,
loadMoreTriggerIndex: loadMoreTriggerIndex,
separatorBuilder: separatorBuilder,
itemBuilder: (context, channels, index) {
final channel = channels[index];
final onTap = onChannelTap;
final onLongPress = onChannelLongPress;
final streamChannelListTile = StreamChannelListTile(
channel: channel,
onTap: onTap == null ? null : () => onTap(channel),
onLongPress: onLongPress == null ? null : () => onLongPress(channel),
);
return itemBuilder?.call(
context,
channels,
index,
streamChannelListTile,
) ??
streamChannelListTile;
},
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.message(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.letsStartChattingLabel,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamScrollViewLoadMoreError.list(
onTap: controller.retry,
error: Text(context.translations.loadingChannelsError),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingChannelsError),
onRetryPressed: controller.refresh,
),
),
);
}
}
/// A widget that is used to display a separator between
/// [StreamChannelListTile] items.
class StreamChannelListSeparator extends StatelessWidget {
/// Creates a new instance of [StreamChannelListSeparator].
const StreamChannelListSeparator({super.key});
@override
Widget build(BuildContext context) {
final effect = StreamChatTheme.of(context).colorTheme.borderBottom;
return Container(
height: 1,
color: effect.color!.withOpacity(effect.alpha ?? 1.0),
);
}
}
/// A widget that is used to display an error screen
/// when [StreamChannelListController] fails to load initial channels.
class StreamChannelListErrorWidget extends StatelessWidget {
/// Creates a new instance of [StreamChannelListErrorWidget] widget.
const StreamChannelListErrorWidget({
super.key,
this.onPressed,
});
/// The callback to invoke when the user taps on the retry button.
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text.rich(
TextSpan(
children: [
const WidgetSpan(
child: Padding(
padding: EdgeInsets.only(right: 2),
child: Icon(Icons.error_outline),
),
),
TextSpan(text: context.translations.loadingChannelsError),
],
),
style: Theme.of(context).textTheme.headline6,
),
TextButton(
onPressed: onPressed,
child: Text(context.translations.retryLabel),
),
],
);
}
@@ -0,0 +1,397 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Default grid delegate for [StreamMemberGridView].
const defaultMemberGridViewDelegate =
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4);
/// Signature for the item builder that creates the children of the
/// [StreamMemberGridView].
typedef StreamMemberGridViewIndexedWidgetBuilder
= StreamScrollViewIndexedWidgetBuilder<Member, StreamMemberGridTile>;
/// Signature for the member grid tile, currently equal to [StreamUserGridTile].
typedef StreamMemberGridTile = StreamUserGridTile;
/// A [GridView] that shows a grid of [Member]s,
/// it uses [StreamMemberGridTile] as a default item.
///
/// Example:
///
/// ```dart
/// StreamMemberGridView(
/// controller: controller,
/// onMemberTap: (member) {
/// // Handle member tap event
/// },
/// onMemberLongPress: (member) {
/// // Handle member long press event
/// },
/// )
/// ```
///
/// See also:
/// * [StreamMemberGridTile]
/// * [StreamMemberListController]
class StreamMemberGridView extends StatelessWidget {
/// Creates a new instance of [StreamMemberGridView].
const StreamMemberGridView({
super.key,
required this.controller,
this.gridDelegate = defaultMemberGridViewDelegate,
this.itemBuilder,
this.emptyBuilder,
this.loadMoreErrorBuilder,
this.loadMoreIndicatorBuilder,
this.loadingBuilder,
this.errorBuilder,
this.onMemberTap,
this.onMemberLongPress,
this.loadMoreTriggerIndex = 3,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
});
/// The [StreamMemberListController] used to control the grid of members.
final StreamMemberListController controller;
/// A delegate that controls the layout of the children within
/// the [PagedValueGridView].
final SliverGridDelegate gridDelegate;
/// A builder that is called to build items in the [PagedValueGridView].
final StreamMemberGridViewIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the empty state of the grid.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the load more error state of the grid.
final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder;
/// A builder that is called to build the load more indicator of the grid.
final WidgetBuilder? loadMoreIndicatorBuilder;
/// A builder that is called to build the loading state of the grid.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the grid.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// Called when the member taps this grid tile.
final void Function(Member)? onMemberTap;
/// Called when the member long-presses on this grid tile.
final void Function(Member)? onMemberLongPress;
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
/// {@endtemplate}
final Axis scrollDirection;
/// {@template flutter.widgets.scroll_view.reverse}
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
/// {@endtemplate}
final bool reverse;
/// {@template flutter.widgets.scroll_view.controller}
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.scroll_view.primary}
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When this is true, the scroll view is scrollable even if it does not have
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by
/// an otherwise focused part of the application,
/// the ScrollAction will be evaluated using this scroll view, for example,
/// when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool? primary;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions. Furthermore, if [primary] is
/// false, then the user cannot scroll if there is insufficient content to
/// scroll, while if [primary] is true, they can always attempt to scroll.
///
/// To force the scroll view to always be scrollable even if there is
/// insufficient content, as if [primary] was true but without necessarily
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
/// object, as in:
///
/// ```dart
/// physics: const AlwaysScrollableScrollPhysics(),
/// ```
///
/// To force the scroll view to use the default platform conventions and not
/// be scrollable if there is insufficient content, regardless of the value of
/// [primary], provide an explicit [ScrollPhysics] object, as in:
///
/// ```dart
/// physics: const ScrollPhysics(),
/// ```
///
/// The physics can be changed dynamically (by providing a new object in a
/// subsequent build), but new physics will only take effect if the _class_ of
/// the provided object changes. Merely constructing a new instance with a
/// different configuration is insufficient to cause the physics to be
/// reapplied. (This is because the final object used is generated
/// dynamically, which can be relatively expensive, and it would be
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
/// {@endtemplate}
final bool shrinkWrap;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by an [IndexedSemantics] widget.
///
/// Defaults to true.
///
/// See also:
///
/// * [IndexedSemantics], for an explanation of how to manually
/// provide semantic indexes.
final bool addSemanticIndexes;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// The number of children that will contribute semantic information.
///
/// Some subtypes of [ScrollView] can infer this value automatically. For
/// example [ListView] will use the number of widgets in the child list,
/// while the [ListView.separated] constructor will use half that amount.
///
/// For [CustomScrollView] and other types which do not receive a builder
/// or list of widgets, the child count must be explicitly provided. If the
/// number is unknown or unbounded this should be left unset or set to null.
///
/// See also:
///
/// * [SemanticsConfiguration.scrollChildCount],
/// the corresponding semantics property.
final int? semanticChildCount;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
/// dismiss the keyboard automatically.
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@override
Widget build(BuildContext context) {
return PagedValueGridView<int, Member>(
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
scrollController: scrollController,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
clipBehavior: clipBehavior,
gridDelegate: gridDelegate,
itemBuilder: (context, members, index) {
final member = members[index];
final onTap = onMemberTap;
final onLongPress = onMemberLongPress;
final streamMemberGridTile = StreamMemberGridTile(
user: member.user!,
onTap: onTap == null ? null : () => onTap(member),
onLongPress: onLongPress == null ? null : () => onLongPress(member),
);
return itemBuilder?.call(
context,
members,
index,
streamMemberGridTile,
) ??
streamMemberGridTile;
},
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.user(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.noUsersLabel,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamScrollViewLoadMoreError.grid(
onTap: controller.retry,
error: Text(
context.translations.loadingUsersError,
textAlign: TextAlign.center,
),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingUsersError),
onRetryPressed: controller.refresh,
),
),
);
}
}
@@ -0,0 +1,365 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Default separator builder for [StreamMemberListView].
Widget defaultMemberListViewSeparatorBuilder(
BuildContext context,
List<Member> members,
int index,
) =>
const StreamUserListSeparator();
/// Signature for the item builder that creates the children of the
/// [StreamMemberListView].
typedef StreamMemberListViewIndexedWidgetBuilder
= StreamScrollViewIndexedWidgetBuilder<Member, StreamUserListTile>;
/// Signature for the member grid tile, currently equal to [StreamUserListTile].
typedef StreamMemberListTile = StreamUserListTile;
/// A [ListView] that shows a list of [Member]s,
/// it uses [StreamMemberListTile] as a default item.
///
/// Example:
///
/// ```dart
/// StreamMemberListView(
/// controller: controller,
/// onMemberTap: (member) {
/// // Handle member tap event
/// },
/// onMemberLongPress: (member) {
/// // Handle member long press event
/// },
/// )
/// ```
///
/// See also:
/// * [StreamMemberListTile]
/// * [StreamMemberListController]
class StreamMemberListView extends StatelessWidget {
/// Creates a new instance of [StreamMemberListView].
const StreamMemberListView({
super.key,
required this.controller,
this.itemBuilder,
this.separatorBuilder = defaultMemberListViewSeparatorBuilder,
this.emptyBuilder,
this.loadingBuilder,
this.errorBuilder,
this.onMemberTap,
this.onMemberLongPress,
this.loadMoreTriggerIndex = 3,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
});
/// The [StreamMemberListController] used to control the list of members.
final StreamMemberListController controller;
/// A builder that is called to build items in the [ListView].
final StreamMemberListViewIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the list separator.
final PagedValueScrollViewIndexedWidgetBuilder<Member> separatorBuilder;
/// A builder that is called to build the empty state of the list.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the loading state of the list.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the list.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// Called when the member taps this list tile.
final void Function(Member)? onMemberTap;
/// Called when the member long-presses on this list tile.
final void Function(Member)? onMemberLongPress;
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
/// {@endtemplate}
final Axis scrollDirection;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by an [IndexedSemantics] widget.
///
/// Defaults to true.
///
/// See also:
///
/// * [IndexedSemantics], for an explanation of how to manually
/// provide semantic indexes.
final bool addSemanticIndexes;
/// {@template flutter.widgets.scroll_view.reverse}
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
/// {@endtemplate}
final bool reverse;
/// {@template flutter.widgets.scroll_view.controller}
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.scroll_view.primary}
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When this is true, the scroll view is scrollable even if it does not have
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by an otherwise focused part of the
/// application, the ScrollAction will be evaluated using this scroll view,
/// for example, when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollController] is null.
final bool? primary;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
/// {@endtemplate}
final bool shrinkWrap;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions. Furthermore, if [primary] is
/// false, then the user cannot scroll if there is insufficient content to
/// scroll, while if [primary] is true, they can always attempt to scroll.
///
/// To force the scroll view to always be scrollable even if there is
/// insufficient content, as if [primary] was true but without necessarily
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
/// object, as in:
///
/// ```dart
/// physics: const AlwaysScrollableScrollPhysics(),
/// ```
///
/// To force the scroll view to use the default platform conventions and not
/// be scrollable if there is insufficient content, regardless of the value of
/// [primary], provide an explicit [ScrollPhysics] object, as in:
///
/// ```dart
/// physics: const ScrollPhysics(),
/// ```
///
/// The physics can be changed dynamically (by providing a new object in a
/// subsequent build), but new physics will only take effect if the _class_ of
/// the provided object changes. Merely constructing a new instance with a
/// different configuration is insufficient to cause the physics to be
/// reapplied. (This is because the final object used is generated
/// dynamically, which can be relatively expensive, and it would be
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
/// dismiss the keyboard automatically.
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@override
Widget build(BuildContext context) => PagedValueListView<int, Member>(
scrollDirection: scrollDirection,
padding: padding,
physics: physics,
reverse: reverse,
controller: controller,
scrollController: scrollController,
primary: primary,
shrinkWrap: shrinkWrap,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
dragStartBehavior: dragStartBehavior,
cacheExtent: cacheExtent,
clipBehavior: clipBehavior,
loadMoreTriggerIndex: loadMoreTriggerIndex,
separatorBuilder: separatorBuilder,
itemBuilder: (context, members, index) {
final member = members[index];
final onTap = onMemberTap;
final onLongPress = onMemberLongPress;
final streamUserListTile = StreamMemberListTile(
user: member.user!,
onTap: onTap == null ? null : () => onTap(member),
onLongPress: onLongPress == null ? null : () => onLongPress(member),
);
return itemBuilder?.call(
context,
members,
index,
streamUserListTile,
) ??
streamUserListTile;
},
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.user(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.noUsersLabel,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamScrollViewLoadMoreError.list(
onTap: controller.retry,
error: Text(context.translations.loadingUsersError),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingUsersError),
onRetryPressed: controller.refresh,
),
),
);
}
@@ -0,0 +1,364 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Default grid delegate for [StreamMessageSearchGridView].
const defaultMessageSearchGridViewDelegate =
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4);
/// Signature for the item builder that creates the children of the
/// [StreamMessageSearchGridView].
typedef StreamMessageSearchGridViewIndexedWidgetBuilder
= PagedValueScrollViewIndexedWidgetBuilder<GetMessageResponse>;
/// A [GridView] that shows a grid of [GetMessageResponse]s,
/// it uses [StreamMessageSearchGridTile] as a default item.
///
/// Example:
///
/// ```dart
/// StreamMessageSearchGridView(
/// controller: controller,
/// itemBuilder: (context, messageResponses, index) {
/// return GridTile(message: messageResponses[index]);
/// },
/// )
/// ```
///
/// See also:
/// * [StreamUserListTile]
/// * [StreamUserListController]
class StreamMessageSearchGridView extends StatelessWidget {
/// Creates a new instance of [StreamMessageSearchGridView].
const StreamMessageSearchGridView({
super.key,
required this.controller,
required this.itemBuilder,
this.gridDelegate = defaultMessageSearchGridViewDelegate,
this.emptyBuilder,
this.loadMoreErrorBuilder,
this.loadMoreIndicatorBuilder,
this.loadingBuilder,
this.errorBuilder,
this.loadMoreTriggerIndex = 3,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
});
/// The [StreamUserListController] used to control the grid of users.
final StreamMessageSearchListController controller;
/// A delegate that controls the layout of the children within
/// the [PagedValueGridView].
final SliverGridDelegate gridDelegate;
/// A builder that is called to build items in the [PagedValueGridView].
///
/// The `value` parameter is the [GetMessageBuilder]
/// at this position in the grid.
final StreamMessageSearchGridViewIndexedWidgetBuilder itemBuilder;
/// A builder that is called to build the empty state of the grid.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the load more error state of the grid.
final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder;
/// A builder that is called to build the load more indicator of the grid.
final WidgetBuilder? loadMoreIndicatorBuilder;
/// A builder that is called to build the loading state of the grid.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the grid.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
/// {@endtemplate}
final Axis scrollDirection;
/// {@template flutter.widgets.scroll_view.reverse}
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
/// {@endtemplate}
final bool reverse;
/// {@template flutter.widgets.scroll_view.controller}
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.scroll_view.primary}
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When this is true, the scroll view is scrollable even if it does not have
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by
/// an otherwise focused part of the application,
/// the ScrollAction will be evaluated using this scroll view, for example,
/// when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool? primary;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions. Furthermore, if [primary] is
/// false, then the user cannot scroll if there is insufficient content to
/// scroll, while if [primary] is true, they can always attempt to scroll.
///
/// To force the scroll view to always be scrollable even if there is
/// insufficient content, as if [primary] was true but without necessarily
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
/// object, as in:
///
/// ```dart
/// physics: const AlwaysScrollableScrollPhysics(),
/// ```
///
/// To force the scroll view to use the default platform conventions and not
/// be scrollable if there is insufficient content, regardless of the value of
/// [primary], provide an explicit [ScrollPhysics] object, as in:
///
/// ```dart
/// physics: const ScrollPhysics(),
/// ```
///
/// The physics can be changed dynamically (by providing a new object in a
/// subsequent build), but new physics will only take effect if the _class_ of
/// the provided object changes. Merely constructing a new instance with a
/// different configuration is insufficient to cause the physics to be
/// reapplied. (This is because the final object used is generated
/// dynamically, which can be relatively expensive, and it would be
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
/// {@endtemplate}
final bool shrinkWrap;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by an [IndexedSemantics] widget.
///
/// Defaults to true.
///
/// See also:
///
/// * [IndexedSemantics], for an explanation of how to manually
/// provide semantic indexes.
final bool addSemanticIndexes;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// The number of children that will contribute semantic information.
///
/// Some subtypes of [ScrollView] can infer this value automatically. For
/// example [ListView] will use the number of widgets in the child list,
/// while the [ListView.separated] constructor will use half that amount.
///
/// For [CustomScrollView] and other types which do not receive a builder
/// or list of widgets, the child count must be explicitly provided. If the
/// number is unknown or unbounded this should be left unset or set to null.
///
/// See also:
///
/// * [SemanticsConfiguration.scrollChildCount],
/// the corresponding semantics property.
final int? semanticChildCount;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
/// dismiss the keyboard automatically.
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@override
Widget build(BuildContext context) {
return PagedValueGridView<String, GetMessageResponse>(
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
scrollController: scrollController,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
clipBehavior: clipBehavior,
gridDelegate: gridDelegate,
itemBuilder: itemBuilder,
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.message(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.emptyMessagesText,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamScrollViewLoadMoreError.grid(
onTap: controller.retry,
error: Text(context.translations.loadingMessagesError),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
onRetryPressed: controller.refresh,
),
),
);
}
}
@@ -0,0 +1,239 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that displays a message search item.
///
/// This widget is intended to be used as a
/// Tile in [StreamMessageSearchListView].
///
/// It displays the message's text, channel, sender, and timestamp.
///
/// See also:
/// * [StreamMessageSearchListView]
/// * [StreamUserAvatar]
class StreamMessageSearchListTile extends StatelessWidget {
/// Creates a new instance of [StreamMessageSearchListTile].
const StreamMessageSearchListTile({
super.key,
required this.messageResponse,
this.leading,
this.title,
this.subtitle,
this.trailing,
this.onTap,
this.onLongPress,
this.tileColor,
this.visualDensity = VisualDensity.compact,
this.contentPadding = const EdgeInsets.symmetric(horizontal: 8),
});
/// The message response to display.
final GetMessageResponse messageResponse;
/// A widget to display before the title.
final Widget? leading;
/// The primary content of the list tile.
final Widget? title;
/// Additional content displayed below the title.
final Widget? subtitle;
/// A widget to display at the end of tile.
final Widget? trailing;
/// Called when the user taps this list tile.
final GestureTapCallback? onTap;
/// Called when the user long-presses on this list tile.
final GestureLongPressCallback? onLongPress;
/// {@template flutter.material.ListTile.tileColor}
/// Defines the background color of `ListTile`.
///
/// When the value is null,
/// the `tileColor` is set to [ListTileTheme.tileColor]
/// if it's not null and to [Colors.transparent] if it's null.
/// {@endtemplate}
final Color? tileColor;
/// Defines how compact the list tile's layout will be.
///
/// {@macro flutter.material.themedata.visualDensity}
///
/// See also:
///
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all
/// widgets within a [Theme].
final VisualDensity visualDensity;
/// The tile's internal padding.
///
/// Insets a [ListTile]'s contents: its [leading], [title], [subtitle],
/// and [trailing] widgets.
///
/// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used.
final EdgeInsetsGeometry contentPadding;
/// Creates a copy of this tile but with the given fields replaced with
/// the new values.
StreamMessageSearchListTile copyWith({
Key? key,
GetMessageResponse? messageResponse,
Widget? leading,
Widget? title,
Widget? subtitle,
Widget? trailing,
GestureTapCallback? onTap,
GestureLongPressCallback? onLongPress,
Color? tileColor,
VisualDensity? visualDensity,
EdgeInsetsGeometry? contentPadding,
}) =>
StreamMessageSearchListTile(
key: key ?? this.key,
messageResponse: messageResponse ?? this.messageResponse,
leading: leading ?? this.leading,
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
trailing: trailing ?? this.trailing,
onTap: onTap ?? this.onTap,
onLongPress: onLongPress ?? this.onLongPress,
tileColor: tileColor ?? this.tileColor,
visualDensity: visualDensity ?? this.visualDensity,
contentPadding: contentPadding ?? this.contentPadding,
);
@override
Widget build(BuildContext context) {
final message = messageResponse.message;
final user = message.user!;
final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
final leading = this.leading ??
StreamUserAvatar(
user: user,
constraints: const BoxConstraints.tightFor(
height: 40,
width: 40,
),
);
final title = this.title ??
MessageSearchListTileTitle(
messageResponse: messageResponse,
textStyle: channelPreviewTheme.titleStyle,
);
final subtitle = this.subtitle ??
Row(
children: [
Expanded(
child: StreamMessagePreviewText(
message: message,
textStyle: channelPreviewTheme.subtitleStyle,
),
),
const SizedBox(width: 16),
MessageSearchTileMessageDate(
message: message,
textStyle: channelPreviewTheme.lastMessageAtStyle,
),
],
);
return ListTile(
onTap: onTap,
onLongPress: onLongPress,
visualDensity: visualDensity,
contentPadding: contentPadding,
tileColor: tileColor,
leading: leading,
trailing: trailing,
title: title,
subtitle: subtitle,
);
}
}
/// A widget that displays the title of a [StreamMessageSearchListTile].
class MessageSearchListTileTitle extends StatelessWidget {
/// Creates a new [MessageSearchListTileTitle] instance.
const MessageSearchListTileTitle({
super.key,
required this.messageResponse,
this.textStyle,
});
/// The message response for the tile.
final GetMessageResponse messageResponse;
/// The style to use for the title.
final TextStyle? textStyle;
@override
Widget build(BuildContext context) {
final user = messageResponse.message.user!;
final channel = messageResponse.channel;
final channelName = channel?.extraData['name'];
return Row(
children: [
Text(
user.id == StreamChat.of(context).currentUser?.id
? context.translations.youText
: user.name,
style: textStyle,
),
if (channelName != null) ...[
Text(
' ${context.translations.inText} ',
style: textStyle?.copyWith(
fontWeight: FontWeight.normal,
),
),
Text(
channelName as String,
style: textStyle,
),
],
],
);
}
}
/// A widget which shows formatted created date of the passed [message].
class MessageSearchTileMessageDate extends StatelessWidget {
/// Creates a new instance of [MessageSearchTileMessageDate].
const MessageSearchTileMessageDate({
super.key,
required this.message,
this.textStyle,
});
/// The searched message response.
final Message message;
/// The text style to use for the date.
final TextStyle? textStyle;
@override
Widget build(BuildContext context) {
final createdAt = message.createdAt;
String stringDate;
final now = DateTime.now();
if (now.year != createdAt.year ||
now.month != createdAt.month ||
now.day != createdAt.day) {
stringDate = Jiffy(createdAt.toLocal()).yMd;
} else {
stringDate = Jiffy(createdAt.toLocal()).jm;
}
return Text(
stringDate,
style: textStyle,
);
}
}
@@ -0,0 +1,387 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Default separator builder for [StreamMessageSearchListView].
Widget defaultMessageSearchListViewSeparatorBuilder(
BuildContext context,
List<GetMessageResponse> responses,
int index,
) =>
const StreamMessageSearchListSeparator();
/// Signature for the item builder that creates the children of the
/// [StreamMessageSearchListView].
typedef StreamMessageSearchListViewIndexedWidgetBuilder
= StreamScrollViewIndexedWidgetBuilder<GetMessageResponse,
StreamMessageSearchListTile>;
/// A [ListView] that shows a list of [GetMessageResponse]s,
/// it uses [StreamMessageSearchListTile] as a default item.
///
/// This is the new version of [MessageSearchListView] that uses
/// [StreamMessageSearchListController].
///
/// Example:
///
/// ```dart
/// StreamMessageSearchListView(
/// controller: controller,
/// onMessageTap: (user) {
/// // Handle user tap event
/// },
/// onMessageLongPress: (user) {
/// // Handle user long press event
/// },
/// )
/// ```
///
/// See also:
/// * [StreamMessageSearchListTile]
/// * [StreamMessageSearchListController]
class StreamMessageSearchListView extends StatelessWidget {
/// Creates a new instance of [StreamMessageSearchListView].
const StreamMessageSearchListView({
super.key,
required this.controller,
this.itemBuilder,
this.separatorBuilder = defaultMessageSearchListViewSeparatorBuilder,
this.emptyBuilder,
this.loadingBuilder,
this.errorBuilder,
this.onMessageTap,
this.onMessageLongPress,
this.loadMoreTriggerIndex = 3,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
});
/// The [StreamUserListController] used to control the list of
/// searched messages.
final StreamMessageSearchListController controller;
/// A builder that is called to build items in the [ListView].
final StreamMessageSearchListViewIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the list separator.
final PagedValueScrollViewIndexedWidgetBuilder<GetMessageResponse>
separatorBuilder;
/// A builder that is called to build the empty state of the list.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the loading state of the list.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the list.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// Called when the user taps this list tile.
final void Function(GetMessageResponse)? onMessageTap;
/// Called when the user long-presses on this list tile.
final void Function(GetMessageResponse)? onMessageLongPress;
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
/// {@endtemplate}
final Axis scrollDirection;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by an [IndexedSemantics] widget.
///
/// Defaults to true.
///
/// See also:
///
/// * [IndexedSemantics], for an explanation of how to manually
/// provide semantic indexes.
final bool addSemanticIndexes;
/// {@template flutter.widgets.scroll_view.reverse}
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
/// {@endtemplate}
final bool reverse;
/// {@template flutter.widgets.scroll_view.controller}
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.scroll_view.primary}
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When this is true, the scroll view is scrollable even if it does not have
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by an otherwise focused part of the
/// application, the ScrollAction will be evaluated using this scroll view,
/// for example, when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollController] is null.
final bool? primary;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
/// {@endtemplate}
final bool shrinkWrap;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions. Furthermore, if [primary] is
/// false, then the user cannot scroll if there is insufficient content to
/// scroll, while if [primary] is true, they can always attempt to scroll.
///
/// To force the scroll view to always be scrollable even if there is
/// insufficient content, as if [primary] was true but without necessarily
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
/// object, as in:
///
/// ```dart
/// physics: const AlwaysScrollableScrollPhysics(),
/// ```
///
/// To force the scroll view to use the default platform conventions and not
/// be scrollable if there is insufficient content, regardless of the value of
/// [primary], provide an explicit [ScrollPhysics] object, as in:
///
/// ```dart
/// physics: const ScrollPhysics(),
/// ```
///
/// The physics can be changed dynamically (by providing a new object in a
/// subsequent build), but new physics will only take effect if the _class_ of
/// the provided object changes. Merely constructing a new instance with a
/// different configuration is insufficient to cause the physics to be
/// reapplied. (This is because the final object used is generated
/// dynamically, which can be relatively expensive, and it would be
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
/// dismiss the keyboard automatically.
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@override
Widget build(BuildContext context) {
return PagedValueListView<String, GetMessageResponse>(
scrollDirection: scrollDirection,
padding: padding,
physics: physics,
reverse: reverse,
controller: controller,
scrollController: scrollController,
primary: primary,
shrinkWrap: shrinkWrap,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
dragStartBehavior: dragStartBehavior,
cacheExtent: cacheExtent,
clipBehavior: clipBehavior,
loadMoreTriggerIndex: loadMoreTriggerIndex,
separatorBuilder: separatorBuilder,
itemBuilder: (context, messageResponses, index) {
final messageResponse = messageResponses[index];
final onTap = onMessageTap;
final onLongPress = onMessageLongPress;
final streamMessageSearchListTile = StreamMessageSearchListTile(
messageResponse: messageResponse,
onTap: onTap == null ? null : () => onTap(messageResponse),
onLongPress:
onLongPress == null ? null : () => onLongPress(messageResponse),
);
return itemBuilder?.call(
context,
messageResponses,
index,
streamMessageSearchListTile,
) ??
streamMessageSearchListTile;
},
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.message(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.emptyMessagesText,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamScrollViewLoadMoreError.list(
onTap: controller.retry,
error: Text(context.translations.loadingMessagesError),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingMessagesError),
onRetryPressed: controller.refresh,
),
),
);
}
}
/// A widget that is used to display a separator between
/// [StreamMessageSearchListTile] items.
class StreamMessageSearchListSeparator extends StatelessWidget {
/// Creates a new instance of [StreamMessageSearchListSeparator].
const StreamMessageSearchListSeparator({super.key});
@override
Widget build(BuildContext context) {
final effect = StreamChatTheme.of(context).colorTheme.borderBottom;
return Container(
height: 1,
color: effect.color!.withOpacity(effect.alpha ?? 1.0),
);
}
}
@@ -0,0 +1,417 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart'
show AssetEntity, ThumbnailFormat, ThumbnailSize;
import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/scroll_view/photo_gallery/stream_photo_gallery_controller.dart';
import 'package:stream_chat_flutter/src/scroll_view/photo_gallery/stream_photo_gallery_tile.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_empty_widget.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_indexed_widget_builder.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.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';
/// Default grid delegate for [StreamPhotoGallery].
const defaultStreamPhotoGalleryDelegate =
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
);
/// Signature for the item builder that creates the children of the
/// [StreamPhotoGallery].
typedef StreamPhotoGalleryIndexedWidgetBuilder
= StreamScrollViewIndexedWidgetBuilder<AssetEntity, StreamPhotoGalleryTile>;
/// Widget used to display a gallery of photos in the form of grid.
class StreamPhotoGallery extends StatelessWidget {
/// Creates a [StreamPhotoGallery] widget.
const StreamPhotoGallery({
super.key,
required this.controller,
this.gridDelegate = defaultStreamPhotoGalleryDelegate,
this.itemBuilder,
this.emptyBuilder,
this.loadMoreErrorBuilder,
this.loadMoreIndicatorBuilder,
this.loadingBuilder,
this.errorBuilder,
this.onMediaTap,
this.onMediaLongPress,
this.loadMoreTriggerIndex = 3,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.thumbnailSize = const ThumbnailSize(400, 400),
this.thumbnailFormat = ThumbnailFormat.jpeg,
this.thumbnailQuality = 100,
this.thumbnailScale = 1,
});
/// The [StreamPhotoGalleryController] used to control the grid of users.
final StreamPhotoGalleryController controller;
/// A delegate that controls the layout of the children within
/// the [PagedValueGridView].
final SliverGridDelegate gridDelegate;
/// A builder that is called to build items in the [PagedValueGridView].
final StreamPhotoGalleryIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the empty state of the grid.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the load more error state of the grid.
final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder;
/// A builder that is called to build the load more indicator of the grid.
final WidgetBuilder? loadMoreIndicatorBuilder;
/// A builder that is called to build the loading state of the grid.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the grid.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// Called when the user taps this grid tile.
final void Function(AssetEntity)? onMediaTap;
/// Called when the user long-presses on this grid tile.
final void Function(AssetEntity)? onMediaLongPress;
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
/// {@endtemplate}
final Axis scrollDirection;
/// {@template flutter.widgets.scroll_view.reverse}
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
/// {@endtemplate}
final bool reverse;
/// {@template flutter.widgets.scroll_view.controller}
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.scroll_view.primary}
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When this is true, the scroll view is scrollable even if it does not have
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by
/// an otherwise focused part of the application,
/// the ScrollAction will be evaluated using this scroll view, for example,
/// when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool? primary;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions. Furthermore, if [primary] is
/// false, then the user cannot scroll if there is insufficient content to
/// scroll, while if [primary] is true, they can always attempt to scroll.
///
/// To force the scroll view to always be scrollable even if there is
/// insufficient content, as if [primary] was true but without necessarily
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
/// object, as in:
///
/// ```dart
/// physics: const AlwaysScrollableScrollPhysics(),
/// ```
///
/// To force the scroll view to use the default platform conventions and not
/// be scrollable if there is insufficient content, regardless of the value of
/// [primary], provide an explicit [ScrollPhysics] object, as in:
///
/// ```dart
/// physics: const ScrollPhysics(),
/// ```
///
/// The physics can be changed dynamically (by providing a new object in a
/// subsequent build), but new physics will only take effect if the _class_ of
/// the provided object changes. Merely constructing a new instance with a
/// different configuration is insufficient to cause the physics to be
/// reapplied. (This is because the final object used is generated
/// dynamically, which can be relatively expensive, and it would be
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
/// {@endtemplate}
final bool shrinkWrap;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by an [IndexedSemantics] widget.
///
/// Defaults to true.
///
/// See also:
///
/// * [IndexedSemantics], for an explanation of how to manually
/// provide semantic indexes.
final bool addSemanticIndexes;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// The number of children that will contribute semantic information.
///
/// Some subtypes of [ScrollView] can infer this value automatically. For
/// example [ListView] will use the number of widgets in the child list,
/// while the [ListView.separated] constructor will use half that amount.
///
/// For [CustomScrollView] and other types which do not receive a builder
/// or list of widgets, the child count must be explicitly provided. If the
/// number is unknown or unbounded this should be left unset or set to null.
///
/// See also:
///
/// * [SemanticsConfiguration.scrollChildCount],
/// the corresponding semantics property.
final int? semanticChildCount;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
/// dismiss the keyboard automatically.
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
/// The thumbnail size.
final ThumbnailSize thumbnailSize;
/// {@macro photo_manager.ThumbnailFormat}
final ThumbnailFormat thumbnailFormat;
/// The quality value for the thumbnail.
///
/// Valid from 1 to 100.
/// Defaults to 100.
final int thumbnailQuality;
/// Scale of the image.
final double thumbnailScale;
@override
Widget build(BuildContext context) {
return PagedValueGridView<int, AssetEntity>(
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
scrollController: scrollController,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
clipBehavior: clipBehavior,
loadMoreTriggerIndex: loadMoreTriggerIndex,
gridDelegate: gridDelegate,
itemBuilder: (context, mediaList, index) {
final media = mediaList[index];
final onTap = onMediaTap;
final onLongPress = onMediaLongPress;
final streamPhotoGalleryTile = StreamPhotoGalleryTile(
media: media,
onTap: onTap == null ? null : () => onTap(media),
onLongPress: onLongPress == null ? null : () => onLongPress(media),
thumbnailSize: thumbnailSize,
thumbnailFormat: thumbnailFormat,
thumbnailQuality: thumbnailQuality,
thumbnailScale: thumbnailScale,
);
return itemBuilder?.call(
context,
mediaList,
index,
streamPhotoGalleryTile,
) ??
streamPhotoGalleryTile;
},
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.pictures(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.noUsersLabel,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) {
return StreamScrollViewLoadMoreError.grid(
onTap: controller.retry,
error: Text(
context.translations.loadingUsersError,
textAlign: TextAlign.center,
),
);
},
loadMoreIndicatorBuilder: (context) {
return const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamScrollViewLoadMoreIndicator(),
),
);
},
loadingBuilder: (context) {
return loadingBuilder?.call(context) ??
const Center(
child: StreamScrollViewLoadingWidget(),
);
},
errorBuilder: (context, error) {
return errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingUsersError),
onRetryPressed: controller.refresh,
),
);
},
);
}
}
@@ -0,0 +1,87 @@
import 'package:collection/collection.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
///
class StreamPhotoGalleryController
extends PagedValueNotifier<int, AssetEntity> {
///
StreamPhotoGalleryController({
this.limit = 50,
}) : super(const PagedValue.loading());
/// The maximum number of items to load at once.
final int limit;
Future<AssetPathEntity?> _getRecentAssetPathList({
RequestType type = RequestType.common,
FilterOptionGroup? filterOption,
}) {
return PhotoManager.getAssetPathList(
type: type,
onlyAll: true,
filterOption: filterOption,
).then((it) => it.firstOrNull);
}
@override
Future<void> doInitialLoad() async {
try {
final assets = await _getRecentAssetPathList();
if (assets == null) {
value = const PagedValue(items: []);
return;
}
final mediaList = await assets.getAssetListPaged(
page: 0,
size: limit,
);
final nextKey = mediaList.length < limit ? null : 1;
value = PagedValue(
items: mediaList,
nextPageKey: nextKey,
);
} on StreamChatError catch (error) {
value = PagedValue.error(error);
} catch (error) {
final chatError = StreamChatError(error.toString());
value = PagedValue.error(chatError);
}
}
@override
Future<void> loadMore(int page) async {
final previousValue = value.asSuccess;
try {
final assets = await _getRecentAssetPathList();
if (assets == null) {
const chatError = StreamChatError('No media found');
value = previousValue.copyWith(error: chatError);
return;
}
final mediaList = await assets.getAssetListPaged(
page: page,
size: limit,
);
final previousItems = previousValue.items;
final newItems = previousItems + mediaList;
final nextKey = mediaList.length < limit ? null : page + 1;
value = PagedValue(
items: newItems,
nextPageKey: nextKey,
);
} on StreamChatError catch (error) {
value = previousValue.copyWith(error: error);
} catch (error) {
final chatError = StreamChatError(error.toString());
value = previousValue.copyWith(error: chatError);
}
}
}
@@ -0,0 +1,259 @@
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter/src/misc/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/theme/stream_chat_theme.dart';
/// Widget that displays a photo or video item from the gallery.
class StreamPhotoGalleryTile extends StatelessWidget {
/// Creates a new instance of [StreamPhotoGalleryTile].
const StreamPhotoGalleryTile({
super.key,
required this.media,
this.selected = false,
this.onTap,
this.onLongPress,
this.thumbnailSize = const ThumbnailSize(400, 400),
this.thumbnailFormat = ThumbnailFormat.jpeg,
this.thumbnailQuality = 100,
this.thumbnailScale = 1,
});
/// The media item to display.
final AssetEntity media;
/// Whether the media item is selected.
final bool selected;
/// Called when the user taps this grid tile.
final GestureTapCallback? onTap;
/// Called when the user long-presses on this grid tile.
final GestureLongPressCallback? onLongPress;
/// The thumbnail size.
final ThumbnailSize thumbnailSize;
/// {@macro photo_manager.ThumbnailFormat}
final ThumbnailFormat thumbnailFormat;
/// The quality value for the thumbnail.
///
/// Valid from 1 to 100.
/// Defaults to 100.
final int thumbnailQuality;
/// Scale of the image.
final double thumbnailScale;
/// Creates a copy of this tile but with the given fields replaced with
/// the new values.
StreamPhotoGalleryTile copyWith({
Key? key,
AssetEntity? media,
bool? selected,
GestureTapCallback? onTap,
GestureLongPressCallback? onLongPress,
ThumbnailSize? thumbnailSize,
ThumbnailFormat? thumbnailFormat,
int? thumbnailQuality,
double? thumbnailScale,
}) =>
StreamPhotoGalleryTile(
key: key ?? this.key,
media: media ?? this.media,
selected: selected ?? this.selected,
onTap: onTap ?? this.onTap,
onLongPress: onLongPress ?? this.onLongPress,
thumbnailSize: thumbnailSize ?? this.thumbnailSize,
thumbnailFormat: thumbnailFormat ?? this.thumbnailFormat,
thumbnailQuality: thumbnailQuality ?? this.thumbnailQuality,
thumbnailScale: thumbnailScale ?? this.thumbnailScale,
);
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
return Stack(
children: [
AspectRatio(
aspectRatio: 1,
child: FadeInImage(
fadeInDuration: const Duration(milliseconds: 300),
placeholder: const AssetImage(
'images/placeholder.png',
package: 'stream_chat_flutter',
),
fit: BoxFit.cover,
image: MediaThumbnailProvider(
media: media,
size: thumbnailSize,
format: thumbnailFormat,
quality: thumbnailQuality,
scale: thumbnailScale,
),
),
),
Positioned.fill(
child: IgnorePointer(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: selected ? 1.0 : 0.0,
child: Container(
color:
chatThemeData.colorTheme.textHighEmphasis.withOpacity(0.5),
alignment: Alignment.topRight,
padding: const EdgeInsets.only(
top: 8,
right: 8,
),
child: CircleAvatar(
radius: 12,
backgroundColor: chatThemeData.colorTheme.barsBg,
child: StreamSvgIcon.check(
size: 24,
color: chatThemeData.colorTheme.textHighEmphasis,
),
),
),
),
),
),
if (media.type == AssetType.video) ...[
Positioned(
left: 8,
bottom: 10,
child: StreamSvgIcon.videoCall(),
),
Positioned(
right: 4,
bottom: 10,
child: Text(
media.videoDuration.format(),
style: TextStyle(
color: chatThemeData.colorTheme.barsBg,
),
),
),
],
// https://stackoverflow.com/a/59317162/10036882
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
onLongPress: onLongPress,
),
),
),
],
);
}
}
extension on Duration {
String format() {
final s = '$this'.split('.')[0].padLeft(8, '0');
if (s.startsWith('00:')) {
return s.replaceFirst('00:', '');
}
return s;
}
}
/// {@template mediaThumbnailProvider}
/// Builds a thumbnail using [ImageProvider].
/// {@endtemplate}
class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
/// {@macro mediaThumbnailProvider}
const MediaThumbnailProvider({
required this.media,
// TODO: Are these sizes optimal? Consider web/desktop
this.size = const ThumbnailSize(400, 400),
this.format = ThumbnailFormat.jpeg,
this.quality = 100,
this.scale = 2,
});
/// Media to load
final AssetEntity media;
/// The thumbnail size.
final ThumbnailSize size;
/// {@macro photo_manager.ThumbnailFormat}
final ThumbnailFormat format;
/// The quality value for the thumbnail.
///
/// Valid from 1 to 100.
/// Defaults to 100.
final int quality;
/// Scale of the image.
final double scale;
@override
Future<MediaThumbnailProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<MediaThumbnailProvider>(this);
}
@override
ImageStreamCompleter loadBuffer(
MediaThumbnailProvider key,
DecoderBufferCallback decode,
) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: key.scale,
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>(
'Thumbnail provider: $this \n Thumbnail key: $key',
this,
style: DiagnosticsTreeStyle.errorProperty,
);
},
);
}
Future<ui.Codec> _loadAsync(
MediaThumbnailProvider key,
DecoderBufferCallback decode,
) async {
assert(key == this, '$key is not $this');
final bytes = await media.thumbnailDataWithSize(
size,
format: format,
quality: quality,
);
final buffer = await ui.ImmutableBuffer.fromUint8List(bytes!);
return decode(buffer);
}
@override
bool operator ==(dynamic other) {
if (other is MediaThumbnailProvider) {
return media == other.media &&
size == other.size &&
format == other.format &&
quality == other.quality &&
scale == other.scale;
}
return false;
}
@override
int get hashCode => Object.hash(media, size, format, quality, scale);
@override
String toString() => '$runtimeType('
'media: $media, '
'size: $size, '
'format: $format, '
'quality: $quality, '
'scale: $scale'
')';
}
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that shows an empty view when the [StreamScrollView] loads
/// empty data.
class StreamScrollViewEmptyWidget extends StatelessWidget {
/// Creates a new instance of the [StreamScrollViewEmptyWidget].
const StreamScrollViewEmptyWidget({
super.key,
required this.emptyIcon,
required this.emptyTitle,
this.emptyTitleStyle,
this.mainAxisSize = MainAxisSize.max,
this.mainAxisAlignment = MainAxisAlignment.center,
this.crossAxisAlignment = CrossAxisAlignment.center,
});
/// The title of the empty view.
final Widget emptyTitle;
/// The style of the title.
final TextStyle? emptyTitleStyle;
/// The icon of the empty view.
final Widget emptyIcon;
/// The main axis size of the empty view.
final MainAxisSize mainAxisSize;
/// The main axis alignment of the empty view.
final MainAxisAlignment mainAxisAlignment;
/// The cross axis alignment of the empty view.
final CrossAxisAlignment crossAxisAlignment;
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
final emptyIcon = AnimatedSwitcher(
duration: kThemeChangeDuration,
child: this.emptyIcon,
);
final emptyTitleText = AnimatedDefaultTextStyle(
style: emptyTitleStyle ?? chatThemeData.textTheme.headline,
duration: kThemeChangeDuration,
child: emptyTitle,
);
return Column(
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: [
emptyIcon,
emptyTitleText,
],
);
}
}
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that is displayed when a [StreamScrollView] encounters an error
/// while loading the initial items.
class StreamScrollViewErrorWidget extends StatelessWidget {
/// Creates a new instance of the [StreamScrollViewErrorWidget].
const StreamScrollViewErrorWidget({
super.key,
this.errorTitle,
this.errorTitleStyle,
this.errorIcon,
this.retryButtonText,
this.retryButtonTextStyle,
required this.onRetryPressed,
this.mainAxisSize = MainAxisSize.max,
this.mainAxisAlignment = MainAxisAlignment.center,
this.crossAxisAlignment = CrossAxisAlignment.center,
});
/// The title of the error.
final Widget? errorTitle;
/// The style of the title.
final TextStyle? errorTitleStyle;
/// The icon to display when the list shows error.
final Widget? errorIcon;
/// The text to display in the retry button.
final Widget? retryButtonText;
/// The style of the retryButtonText.
final TextStyle? retryButtonTextStyle;
/// The callback to invoke when the user taps on the retry button.
final VoidCallback onRetryPressed;
/// The main axis size of the error view.
final MainAxisSize mainAxisSize;
/// The main axis alignment of the error view.
final MainAxisAlignment mainAxisAlignment;
/// The cross axis alignment of the error view.
final CrossAxisAlignment crossAxisAlignment;
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
final errorIcon = AnimatedSwitcher(
duration: kThemeChangeDuration,
child: this.errorIcon ??
Icon(
Icons.error_outline_rounded,
size: 148,
color: chatThemeData.colorTheme.disabled,
),
);
final titleText = AnimatedDefaultTextStyle(
style: errorTitleStyle ?? chatThemeData.textTheme.headline,
duration: kThemeChangeDuration,
child: errorTitle ?? const SizedBox(),
);
final retryButtonText = AnimatedDefaultTextStyle(
style: errorTitleStyle ??
chatThemeData.textTheme.headline.copyWith(
color: Colors.white,
),
duration: kThemeChangeDuration,
child: this.retryButtonText ?? Text(context.translations.retryLabel),
);
return Column(
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: [
errorIcon,
titleText,
ElevatedButton(
onPressed: onRetryPressed,
child: retryButtonText,
),
],
);
}
}
@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
/// Signature for a function that creates a widget for a given index, e.g., in a
/// list, grid.
///
/// Used by [StreamChannelListView], [StreamMessageSearchListView]
/// and [StreamUserListView].
typedef StreamScrollViewIndexedWidgetBuilder<ItemType,
WidgetType extends Widget>
= Widget Function(
BuildContext context,
List<ItemType> items,
int index,
WidgetType defaultWidget,
);
@@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A tile that is used to display the error indicator when
/// loading more items fails.
class StreamScrollViewLoadMoreError extends StatelessWidget {
/// Creates a new instance of [StreamScrollViewLoadMoreError.list].
const StreamScrollViewLoadMoreError.list({
super.key,
this.error,
this.errorStyle,
this.errorIcon,
this.backgroundColor,
required this.onTap,
this.padding = const EdgeInsets.all(16),
this.mainAxisSize = MainAxisSize.max,
this.mainAxisAlignment = MainAxisAlignment.spaceBetween,
this.crossAxisAlignment = CrossAxisAlignment.center,
}) : _isList = true;
/// Creates a new instance of [StreamScrollViewLoadMoreError.grid].
const StreamScrollViewLoadMoreError.grid({
super.key,
this.error,
this.errorStyle,
this.errorIcon,
this.backgroundColor,
required this.onTap,
this.padding = const EdgeInsets.all(16),
this.mainAxisSize = MainAxisSize.max,
this.mainAxisAlignment = MainAxisAlignment.spaceEvenly,
this.crossAxisAlignment = CrossAxisAlignment.center,
}) : _isList = false;
/// The error message to display.
final Widget? error;
/// The style of the error message.
final TextStyle? errorStyle;
/// The icon to display next to the message.
final Widget? errorIcon;
/// The background color of the error message.
final Color? backgroundColor;
/// The callback to invoke when the user taps on the error indicator.
final GestureTapCallback onTap;
/// The amount of space by which to inset the child.
final EdgeInsetsGeometry padding;
/// The main axis size of the error view.
final MainAxisSize mainAxisSize;
/// The main axis alignment of the error view.
final MainAxisAlignment mainAxisAlignment;
/// The cross axis alignment of the error view.
final CrossAxisAlignment crossAxisAlignment;
final bool _isList;
@override
Widget build(BuildContext context) {
final theme = StreamChatTheme.of(context);
final errorText = AnimatedDefaultTextStyle(
style: errorStyle ?? theme.textTheme.body.copyWith(color: Colors.white),
duration: kThemeChangeDuration,
child: error ?? const SizedBox(),
);
final errorIcon = AnimatedSwitcher(
duration: kThemeChangeDuration,
child: this.errorIcon ?? StreamSvgIcon.retry(color: Colors.white),
);
final backgroundColor = this.backgroundColor ??
theme.colorTheme.textLowEmphasis.withOpacity(0.9);
final children = [errorText, errorIcon];
return InkWell(
onTap: onTap,
child: ColoredBox(
color: backgroundColor,
child: Padding(
padding: padding,
child: _isList
? Row(
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: children,
)
: Column(
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
children: children,
),
),
),
);
}
}
@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
/// A widget that shows a loading indicator when the user is near the bottom of
/// the list.
class StreamScrollViewLoadMoreIndicator extends StatelessWidget {
/// Creates a new instance of [StreamScrollViewLoadMoreIndicator].
const StreamScrollViewLoadMoreIndicator({
super.key,
this.height = 16,
this.width = 16,
});
/// The height of the indicator.
final double height;
/// The width of the indicator.
final double width;
@override
Widget build(BuildContext context) => SizedBox(
height: height,
width: width,
child: const CircularProgressIndicator.adaptive(),
);
}
@@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
/// A widget that is displayed while the [StreamScrollView] is loading.
class StreamScrollViewLoadingWidget extends StatelessWidget {
/// Creates a new instance of [StreamScrollViewLoadingWidget].
const StreamScrollViewLoadingWidget({
super.key,
this.height = 42,
this.width = 42,
});
/// The height of the indicator.
final double height;
/// The width of the indicator.
final double width;
@override
Widget build(BuildContext context) => SizedBox(
height: height,
width: width,
child: const CircularProgressIndicator.adaptive(),
);
}
@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that displays a user.
///
/// This widget is intended to be used as a Tile in [StreamUserGridView]
///
/// It shows the user's avatar and name.
///
/// See also:
/// * [StreamUserGridView]
/// * [StreamUserAvatar]
class StreamUserGridTile extends StatelessWidget {
/// Creates a new instance of [StreamUserGridTile] widget.
const StreamUserGridTile({
super.key,
required this.user,
this.child,
this.footer,
this.onTap,
this.onLongPress,
});
/// The user to display.
final User user;
/// The widget to display in the body of the tile.
final Widget? child;
/// The widget to display in the footer of the tile.
final Widget? footer;
/// Called when the user taps this grid tile.
final GestureTapCallback? onTap;
/// Called when the user long-presses on this grid tile.
final GestureLongPressCallback? onLongPress;
/// Creates a copy of this tile but with the given fields replaced with
/// the new values.
StreamUserGridTile copyWith({
Key? key,
User? user,
Widget? child,
Widget? footer,
GestureTapCallback? onTap,
GestureLongPressCallback? onLongPress,
}) =>
StreamUserGridTile(
key: key ?? this.key,
user: user ?? this.user,
footer: footer ?? this.footer,
onTap: onTap ?? this.onTap,
onLongPress: onLongPress ?? this.onLongPress,
child: child ?? this.child,
);
@override
Widget build(BuildContext context) {
final child = this.child ??
StreamUserAvatar(
user: user,
borderRadius: BorderRadius.circular(32),
constraints: const BoxConstraints.tightFor(
height: 64,
width: 64,
),
onlineIndicatorConstraints: const BoxConstraints.tightFor(
height: 12,
width: 12,
),
);
final footer = this.footer ??
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
user.name,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
);
return InkWell(
onTap: onTap,
onLongPress: onLongPress,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
child,
const SizedBox(height: 4),
footer,
],
),
);
}
}
@@ -0,0 +1,394 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Default grid delegate for [StreamUserGridView].
const defaultUserGridViewDelegate =
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4);
/// Signature for the item builder that creates the children of the
/// [StreamUserGridView].
typedef StreamUserGridViewIndexedWidgetBuilder
= StreamScrollViewIndexedWidgetBuilder<User, StreamUserGridTile>;
/// A [GridView] that shows a grid of [User]s,
/// it uses [StreamUserGridTile] as a default item.
///
/// Example:
///
/// ```dart
/// StreamUserGridView(
/// controller: controller,
/// onUserTap: (user) {
/// // Handle user tap event
/// },
/// onUserLongPress: (user) {
/// // Handle user long press event
/// },
/// )
/// ```
///
/// See also:
/// * [StreamUserListTile]
/// * [StreamUserListController]
class StreamUserGridView extends StatelessWidget {
/// Creates a new instance of [StreamUserGridView].
const StreamUserGridView({
super.key,
required this.controller,
this.gridDelegate = defaultUserGridViewDelegate,
this.itemBuilder,
this.emptyBuilder,
this.loadMoreErrorBuilder,
this.loadMoreIndicatorBuilder,
this.loadingBuilder,
this.errorBuilder,
this.onUserTap,
this.onUserLongPress,
this.loadMoreTriggerIndex = 3,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
});
/// The [StreamUserListController] used to control the grid of users.
final StreamUserListController controller;
/// A delegate that controls the layout of the children within
/// the [PagedValueGridView].
final SliverGridDelegate gridDelegate;
/// A builder that is called to build items in the [PagedValueGridView].
final StreamUserGridViewIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the empty state of the grid.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the load more error state of the grid.
final PagedValueScrollViewLoadMoreErrorBuilder? loadMoreErrorBuilder;
/// A builder that is called to build the load more indicator of the grid.
final WidgetBuilder? loadMoreIndicatorBuilder;
/// A builder that is called to build the loading state of the grid.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the grid.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// Called when the user taps this grid tile.
final void Function(User)? onUserTap;
/// Called when the user long-presses on this grid tile.
final void Function(User)? onUserLongPress;
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
/// {@endtemplate}
final Axis scrollDirection;
/// {@template flutter.widgets.scroll_view.reverse}
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
/// left to right when [reverse] is false and from right to left when
/// [reverse] is true.
///
/// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
/// {@endtemplate}
final bool reverse;
/// {@template flutter.widgets.scroll_view.controller}
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.scroll_view.primary}
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When this is true, the scroll view is scrollable even if it does not have
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by
/// an otherwise focused part of the application,
/// the ScrollAction will be evaluated using this scroll view, for example,
/// when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool? primary;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions. Furthermore, if [primary] is
/// false, then the user cannot scroll if there is insufficient content to
/// scroll, while if [primary] is true, they can always attempt to scroll.
///
/// To force the scroll view to always be scrollable even if there is
/// insufficient content, as if [primary] was true but without necessarily
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
/// object, as in:
///
/// ```dart
/// physics: const AlwaysScrollableScrollPhysics(),
/// ```
///
/// To force the scroll view to use the default platform conventions and not
/// be scrollable if there is insufficient content, regardless of the value of
/// [primary], provide an explicit [ScrollPhysics] object, as in:
///
/// ```dart
/// physics: const ScrollPhysics(),
/// ```
///
/// The physics can be changed dynamically (by providing a new object in a
/// subsequent build), but new physics will only take effect if the _class_ of
/// the provided object changes. Merely constructing a new instance with a
/// different configuration is insufficient to cause the physics to be
/// reapplied. (This is because the final object used is generated
/// dynamically, which can be relatively expensive, and it would be
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
/// {@endtemplate}
final bool shrinkWrap;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by an [IndexedSemantics] widget.
///
/// Defaults to true.
///
/// See also:
///
/// * [IndexedSemantics], for an explanation of how to manually
/// provide semantic indexes.
final bool addSemanticIndexes;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// The number of children that will contribute semantic information.
///
/// Some subtypes of [ScrollView] can infer this value automatically. For
/// example [ListView] will use the number of widgets in the child list,
/// while the [ListView.separated] constructor will use half that amount.
///
/// For [CustomScrollView] and other types which do not receive a builder
/// or list of widgets, the child count must be explicitly provided. If the
/// number is unknown or unbounded this should be left unset or set to null.
///
/// See also:
///
/// * [SemanticsConfiguration.scrollChildCount],
/// the corresponding semantics property.
final int? semanticChildCount;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
/// dismiss the keyboard automatically.
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@override
Widget build(BuildContext context) {
return PagedValueGridView<int, User>(
scrollDirection: scrollDirection,
reverse: reverse,
controller: controller,
primary: primary,
physics: physics,
shrinkWrap: shrinkWrap,
padding: padding,
scrollController: scrollController,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
cacheExtent: cacheExtent,
semanticChildCount: semanticChildCount,
dragStartBehavior: dragStartBehavior,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
clipBehavior: clipBehavior,
gridDelegate: gridDelegate,
itemBuilder: (context, users, index) {
final user = users[index];
final onTap = onUserTap;
final onLongPress = onUserLongPress;
final streamUserGridTile = StreamUserGridTile(
user: user,
onTap: onTap == null ? null : () => onTap(user),
onLongPress: onLongPress == null ? null : () => onLongPress(user),
);
return itemBuilder?.call(
context,
users,
index,
streamUserGridTile,
) ??
streamUserGridTile;
},
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.user(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.noUsersLabel,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamScrollViewLoadMoreError.grid(
onTap: controller.retry,
error: Text(
context.translations.loadingUsersError,
textAlign: TextAlign.center,
),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingUsersError),
onRetryPressed: controller.refresh,
),
),
);
}
}
@@ -0,0 +1,189 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils/extensions.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that displays a user.
///
/// This widget is intended to be used as a Tile in [StreamUserListView]
///
/// It shows the user's avatar, name and last message.
///
/// See also:
/// * [StreamUserListView]
/// * [StreamUserAvatar]
class StreamUserListTile extends StatelessWidget {
/// Creates a new instance of [StreamUserListTile].
const StreamUserListTile({
super.key,
required this.user,
this.leading,
this.title,
this.subtitle,
this.selected = false,
this.selectedWidget,
this.onTap,
this.onLongPress,
this.tileColor,
this.visualDensity = VisualDensity.compact,
this.contentPadding = const EdgeInsets.symmetric(horizontal: 8),
});
/// The user to display.
final User user;
/// A widget to display before the title.
final Widget? leading;
/// The primary content of the list tile.
final Widget? title;
/// Additional content displayed below the title.
final Widget? subtitle;
/// A widget to display at the end of tile.
final Widget? selectedWidget;
/// If this tile is also [enabled] then icons and text are rendered with the
/// same color.
///
/// By default the selected color is the theme's primary color. The selected
/// color can be overridden with a [ListTileTheme].
///
/// {@tool dartpad}
/// Here is an example of using a [StatefulWidget] to keep track of the
/// selected index, and using that to set the `selected` property on the
/// corresponding [ListTile].
///
/// ** See code in examples/api/lib/material/list_tile/list_tile.selected.0.dart **
/// {@end-tool}
final bool selected;
/// Called when the user taps this list tile.
final GestureTapCallback? onTap;
/// Called when the user long-presses on this list tile.
final GestureLongPressCallback? onLongPress;
/// {@template flutter.material.ListTile.tileColor}
/// Defines the background color of `ListTile`.
///
/// When the value is null,
/// the `tileColor` is set to [ListTileTheme.tileColor]
/// if it's not null and to [Colors.transparent] if it's null.
/// {@endtemplate}
final Color? tileColor;
/// Defines how compact the list tile's layout will be.
///
/// {@macro flutter.material.themedata.visualDensity}
///
/// See also:
///
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all
/// widgets within a [Theme].
final VisualDensity visualDensity;
/// The tile's internal padding.
///
/// Insets a [ListTile]'s contents: its [leading], [title], [subtitle],
/// and [trailing] widgets.
///
/// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used.
final EdgeInsetsGeometry contentPadding;
/// Creates a copy of this tile but with the given fields replaced with
/// the new values.
StreamUserListTile copyWith({
Key? key,
User? user,
Widget? leading,
Widget? title,
Widget? subtitle,
Widget? selectedWidget,
bool? selected,
GestureTapCallback? onTap,
GestureLongPressCallback? onLongPress,
Color? tileColor,
VisualDensity? visualDensity,
EdgeInsetsGeometry? contentPadding,
}) =>
StreamUserListTile(
key: key ?? this.key,
user: user ?? this.user,
leading: leading ?? this.leading,
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
selectedWidget: selectedWidget ?? this.selectedWidget,
selected: selected ?? this.selected,
onTap: onTap ?? this.onTap,
onLongPress: onLongPress ?? this.onLongPress,
tileColor: tileColor ?? this.tileColor,
visualDensity: visualDensity ?? this.visualDensity,
contentPadding: contentPadding ?? this.contentPadding,
);
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
final leading = this.leading ??
StreamUserAvatar(
user: user,
constraints: const BoxConstraints.tightFor(
height: 40,
width: 40,
),
);
final title = this.title ??
Text(
user.name,
style: chatThemeData.textTheme.bodyBold,
);
final subtitle = this.subtitle ??
UserLastActive(
user: user,
);
final selectedWidget = this.selectedWidget ??
StreamSvgIcon.checkSend(
color: chatThemeData.colorTheme.accentPrimary,
);
return ListTile(
onTap: onTap,
onLongPress: onLongPress,
leading: leading,
trailing: selected ? selectedWidget : null,
title: title,
subtitle: subtitle,
);
}
}
/// A widget that displays a user's last active time.
class UserLastActive extends StatelessWidget {
/// Creates a new instance of the [UserLastActive] widget.
const UserLastActive({
super.key,
required this.user,
});
/// The user whose last active time is displayed.
final User user;
@override
Widget build(BuildContext context) {
final chatTheme = StreamChatTheme.of(context);
return Text(
user.online
? context.translations.userOnlineText
: '${context.translations.userLastOnlineText} '
'${Jiffy(user.lastActive).fromNow()}',
style: chatTheme.textTheme.footnote.copyWith(
color: chatTheme.colorTheme.textHighEmphasis.withOpacity(0.5),
),
);
}
}
@@ -0,0 +1,381 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_error_widget.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_error.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_load_more_indicator.dart';
import 'package:stream_chat_flutter/src/scroll_view/stream_scroll_view_loading_widget.dart';
import 'package:stream_chat_flutter/src/utils/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Default separator builder for [StreamUserListView].
Widget defaultUserListViewSeparatorBuilder(
BuildContext context,
List<User> users,
int index,
) =>
const StreamUserListSeparator();
/// Signature for the item builder that creates the children of the
/// [StreamUserListView].
typedef StreamUserListViewIndexedWidgetBuilder
= StreamScrollViewIndexedWidgetBuilder<User, StreamUserListTile>;
/// A [ListView] that shows a list of [User]s,
/// it uses [StreamUserListTile] as a default item.
///
/// This is the new version of [UserListView] that uses
/// [StreamUserListController].
///
/// Example:
///
/// ```dart
/// StreamUserListView(
/// controller: controller,
/// onUserTap: (user) {
/// // Handle user tap event
/// },
/// onUserLongPress: (user) {
/// // Handle user long press event
/// },
/// )
/// ```
///
/// See also:
/// * [StreamUserListTile]
/// * [StreamUserListController]
class StreamUserListView extends StatelessWidget {
/// Creates a new instance of [StreamUserListView].
const StreamUserListView({
super.key,
required this.controller,
this.itemBuilder,
this.separatorBuilder = defaultUserListViewSeparatorBuilder,
this.emptyBuilder,
this.loadingBuilder,
this.errorBuilder,
this.onUserTap,
this.onUserLongPress,
this.loadMoreTriggerIndex = 3,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.scrollController,
this.primary,
this.physics,
this.shrinkWrap = false,
this.padding,
this.addAutomaticKeepAlives = true,
this.addRepaintBoundaries = true,
this.addSemanticIndexes = true,
this.cacheExtent,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
});
/// The [StreamUserListController] used to control the list of users.
final StreamUserListController controller;
/// A builder that is called to build items in the [ListView].
final StreamUserListViewIndexedWidgetBuilder? itemBuilder;
/// A builder that is called to build the list separator.
final PagedValueScrollViewIndexedWidgetBuilder<User> separatorBuilder;
/// A builder that is called to build the empty state of the list.
final WidgetBuilder? emptyBuilder;
/// A builder that is called to build the loading state of the list.
final WidgetBuilder? loadingBuilder;
/// A builder that is called to build the error state of the list.
final Widget Function(BuildContext, StreamChatError)? errorBuilder;
/// Called when the user taps this list tile.
final void Function(User)? onUserTap;
/// Called when the user long-presses on this list tile.
final void Function(User)? onUserLongPress;
/// The index to take into account when triggering [controller.loadMore].
final int loadMoreTriggerIndex;
/// {@template flutter.widgets.scroll_view.scrollDirection}
/// The axis along which the scroll view scrolls.
///
/// Defaults to [Axis.vertical].
/// {@endtemplate}
final Axis scrollDirection;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry? padding;
/// Whether to wrap each child in an [AutomaticKeepAlive].
///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen.
///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children
/// are going to manually maintain their [KeepAlive] state. It may also be
/// more efficient to disable this feature if it is known ahead of time that
/// none of the children will ever try to keep themselves alive.
///
/// Defaults to true.
final bool addAutomaticKeepAlives;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
/// Whether to wrap each child in an [IndexedSemantics].
///
/// Typically, children in a scrolling container must be annotated with a
/// semantic index in order to generate the correct accessibility
/// announcements. This should only be set to false if the indexes have
/// already been provided by an [IndexedSemantics] widget.
///
/// Defaults to true.
///
/// See also:
///
/// * [IndexedSemantics], for an explanation of how to manually
/// provide semantic indexes.
final bool addSemanticIndexes;
/// {@template flutter.widgets.scroll_view.reverse}
/// Whether the scroll view scrolls in the reading direction.
///
/// For example, if [scrollDirection] is [Axis.vertical], then the scroll view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
/// {@endtemplate}
final bool reverse;
/// {@template flutter.widgets.scroll_view.controller}
/// An object that can be used to control the position to which this scroll
/// view is scrolled.
///
/// Must be null if [primary] is true.
///
/// A [ScrollController] serves several purposes. It can be used to control
/// the initial scroll position (see [ScrollController.initialScrollOffset]).
/// It can be used to control whether the scroll view should automatically
/// save and restore its scroll position in the [PageStorage] (see
/// [ScrollController.keepScrollOffset]). It can be used to read the current
/// scroll position (see [ScrollController.offset]), or change it (see
/// [ScrollController.animateTo]).
/// {@endtemplate}
final ScrollController? scrollController;
/// {@template flutter.widgets.scroll_view.primary}
/// Whether this is the primary scroll view associated with the parent
/// [PrimaryScrollController].
///
/// When this is true, the scroll view is scrollable even if it does not have
/// sufficient content to actually scroll. Otherwise, by default the user can
/// only scroll the view if it has sufficient content. See [physics].
///
/// Also when true, the scroll view is used for default [ScrollAction]s. If a
/// ScrollAction is not handled by an otherwise focused part of the
/// application, the ScrollAction will be evaluated using this scroll view,
/// for example, when executing [Shortcuts] key events like page up and down.
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollController] is null.
final bool? primary;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// If the scroll view does not shrink wrap, then the scroll view will expand
/// to the maximum allowed size in the [scrollDirection]. If the scroll view
/// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must
/// be true.
///
/// Shrink wrapping the content of the scroll view is significantly more
/// expensive than expanding to the maximum allowed size because the content
/// can expand and contract during scrolling, which means the size of the
/// scroll view needs to be recomputed whenever the scroll position changes.
///
/// Defaults to false.
/// {@endtemplate}
final bool shrinkWrap;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
/// user stops dragging the scroll view.
///
/// Defaults to matching platform conventions. Furthermore, if [primary] is
/// false, then the user cannot scroll if there is insufficient content to
/// scroll, while if [primary] is true, they can always attempt to scroll.
///
/// To force the scroll view to always be scrollable even if there is
/// insufficient content, as if [primary] was true but without necessarily
/// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics
/// object, as in:
///
/// ```dart
/// physics: const AlwaysScrollableScrollPhysics(),
/// ```
///
/// To force the scroll view to use the default platform conventions and not
/// be scrollable if there is insufficient content, regardless of the value of
/// [primary], provide an explicit [ScrollPhysics] object, as in:
///
/// ```dart
/// physics: const ScrollPhysics(),
/// ```
///
/// The physics can be changed dynamically (by providing a new object in a
/// subsequent build), but new physics will only take effect if the _class_ of
/// the provided object changes. Merely constructing a new instance with a
/// different configuration is insufficient to cause the physics to be
/// reapplied. (This is because the final object used is generated
/// dynamically, which can be relatively expensive, and it would be
/// inefficient to speculatively create this object each frame to see if the
/// physics should be updated.)
/// {@endtemplate}
///
/// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
/// [ScrollPhysics] provided by that behavior will take precedence after
/// [physics].
final ScrollPhysics? physics;
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
final double? cacheExtent;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.scroll_view.keyboardDismissBehavior}
/// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
/// dismiss the keyboard automatically.
/// {@endtemplate}
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
/// {@macro flutter.widgets.scrollable.restorationId}
final String? restorationId;
/// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@override
Widget build(BuildContext context) => PagedValueListView<int, User>(
scrollDirection: scrollDirection,
padding: padding,
physics: physics,
reverse: reverse,
controller: controller,
scrollController: scrollController,
primary: primary,
shrinkWrap: shrinkWrap,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
keyboardDismissBehavior: keyboardDismissBehavior,
restorationId: restorationId,
dragStartBehavior: dragStartBehavior,
cacheExtent: cacheExtent,
clipBehavior: clipBehavior,
loadMoreTriggerIndex: loadMoreTriggerIndex,
separatorBuilder: separatorBuilder,
itemBuilder: (context, users, index) {
final user = users[index];
final onTap = onUserTap;
final onLongPress = onUserLongPress;
final streamUserListTile = StreamUserListTile(
user: user,
onTap: onTap == null ? null : () => onTap(user),
onLongPress: onLongPress == null ? null : () => onLongPress(user),
);
return itemBuilder?.call(
context,
users,
index,
streamUserListTile,
) ??
streamUserListTile;
},
emptyBuilder: (context) {
final chatThemeData = StreamChatTheme.of(context);
return emptyBuilder?.call(context) ??
Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.user(
size: 148,
color: chatThemeData.colorTheme.disabled,
),
emptyTitle: Text(
context.translations.noUsersLabel,
style: chatThemeData.textTheme.headline,
),
),
),
);
},
loadMoreErrorBuilder: (context, error) =>
StreamScrollViewLoadMoreError.list(
onTap: controller.retry,
error: Text(context.translations.loadingUsersError),
),
loadMoreIndicatorBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.all(16),
child: StreamScrollViewLoadMoreIndicator(),
),
),
loadingBuilder: (context) =>
loadingBuilder?.call(context) ??
const Center(
child: StreamScrollViewLoadingWidget(),
),
errorBuilder: (context, error) =>
errorBuilder?.call(context, error) ??
Center(
child: StreamScrollViewErrorWidget(
errorTitle: Text(context.translations.loadingUsersError),
onRetryPressed: controller.refresh,
),
),
);
}
/// A widget that is used to display a separator between
/// [StreamUserListTile] items.
class StreamUserListSeparator extends StatelessWidget {
/// Creates a new instance of [StreamUserListSeparator].
const StreamUserListSeparator({super.key});
@override
Widget build(BuildContext context) {
final effect = StreamChatTheme.of(context).colorTheme.borderBottom;
return Container(
height: 1,
color: effect.color!.withOpacity(effect.alpha ?? 1.0),
);
}
}