V5 (#1222)
* chore(flutter): change BoxFit of VLC player * chore(flutter): remove unused imports * chore(flutter sample): tweak breakpoints, ignore some lints * chore(flutter): minor code readability improvements * chore(flutter): extract out platform_widget_builder to its own "package" * fix(flutter): fix a theme bug with PlatformDialog * chore(persistence): test plugin on macOS * Generated the macos runner for the example * Re-generated the android runner because it was using the v1 embedding * chore(persistence): test plugin on web * chore(persistence): add Windows runner to example * chore(persistence): add linux runner to example * feat(flutter): branded context menus * chore(flutter): fix bug where lines would be shown in between menu items * chore(flutter): import/export fixes * chore(flutter): move file * chore(flutter): /src/channel Also refactors several /channel widgets to use class widgets instead of methods. Some methods, however, have been left as those cases do actually produce cleaner code. chore(flutter): fix bad directory placement chore(flutter): fix bad directory placement * chore(flutter): more directory organization * chore(flutter): navigator improvements * chore(flutter): void async improvements * chore(flutter): fix imports/exports * docs(flutter): documentation updates for /attachment * docs(flutter): doc updates For: * /avatars * /bottom_sheets * /channel * feat(flutter): selectable message text on web & desktop * Squashed commit of the following: commit 50b249edb6b2ee2d744e4e00e97f81979026aab4 Author: GroovinChip <[email protected]> Date: Wed Mar 16 14:55:05 2022 -0400 chore(flutter): wrapping up docs commit 7d289ae45e20c0109bcdb4496f7b693e2adc4ef9 Author: GroovinChip <[email protected]> Date: Wed Mar 16 14:42:09 2022 -0400 chore(flutter): more docs & typedefs commit 6b36f6f3864f15b52e441d58ee7e62577ff8f843 Author: GroovinChip <[email protected]> Date: Wed Mar 16 14:22:34 2022 -0400 chore(flutter): more doc fixes commit 1a9a6de3c836e9ee5d6373033f1d9a7da830bddc Merge: 59188f5d 88abf99f Author: GroovinChip <[email protected]> Date: Wed Mar 16 13:17:52 2022 -0400 Merge branch 'desk_web_dev' into utils commit 59188f5d9c1dd68edd531086cdfd9d42faf93fc8 Author: GroovinChip <[email protected]> Date: Wed Mar 16 12:48:16 2022 -0400 chore(flutter): add todo commit 9b4a986ca5757f284677eb5335637d11ccb1d642 Author: GroovinChip <[email protected]> Date: Wed Mar 16 12:43:12 2022 -0400 chore(flutter): more docs and typedefs updates commit 8786d2dd82b94f673b36371fb2b33a1ebcc9840a Author: GroovinChip <[email protected]> Date: Wed Mar 16 12:14:14 2022 -0400 chore(flutter): more typedef and doc template updates commit 9e906beb0e7a054c13a32c8c8c5b0e2a9e27bb21 Author: GroovinChip <[email protected]> Date: Wed Mar 16 10:46:04 2022 -0400 chore(flutter): create /uitils and move files Fixes subsequent imports/exports * chore(flutter): update file_selector plugin * chore(flutter): include utils/typedefs.dart in library export * Squashed commit of the following: commit 501228afa614b0d740858dec1ebedd3f671e2c96 Author: GroovinChip <[email protected]> Date: Thu Mar 17 16:18:12 2022 -0400 feat(flutter): ensure fullscreen media feature works on ALL platforms commit cd0aab92bc7d90af03258829a5aae66125578af5 Author: GroovinChip <[email protected]> Date: Thu Mar 17 14:26:48 2022 -0400 feat(flutter): working conditional import for initializing VLC * fix(flutter): remove stray import causing web to break * chore(flutter): ensure context menus can show on web in sample * feat(flutter) FullScreenMedia context menus for Web * feat(flutter): branded context menus * chore(flutter): no success dialog on file download (web) * chore(flutter): remove platform UI packages * chore(flutter): properly show/hide delete message menu item Also deletes deprecated PlatformDialog * feat(flutter): 'view info' channel preview context menu * feat(flutter): more menu actions for ChannelPreview Also new ConfirmationDialog and add missing docs * feat(flutter/localizations): translations related to channel mutes * chore(flutter): remove prints and fix super.didChangeDependencies order * chore(flutter): rename some things in ChannelBottomSheet * chore(flutter): more sensible name in TypingIndicator * chore(flutter): remove unused code in MessageWidget * feat(flutter): ChannelInfoDialog * chore(flutter): organize files more Also refactors out some internal MessageListView widgets * fix(flutter): fix render overflow issue * chore(flutter): move MediaListView * chore(flutter): remove unused imports * fix(flutter): fix weird share menu placement on macos * chore(flutter): clean up fullscreenmedia conditional imports * chore(flutter): adjust package exports * chore(flutter): update dart_vlc * chore(flutter): clean up platform checks * chore(flutter): run flutter format on lib * chore(flutter): minor sample cleanup * chore(flutter): ensure vlc doesn't get initialized in non-desktop testing environments * test(flutter): reformat existing tests * fix(flutter): pass onThreadReplyTap down to ThreadReplyButton * First round of new tests commit a90bed427521123fdaa4c5e276744a224fac2b68 Author: GroovinChip <[email protected]> Date: Thu Apr 7 12:34:19 2022 -0400 test(flutter): Username test commit d7bc179c20eae2713c9248191518bf1d955104a9 Author: GroovinChip <[email protected]> Date: Thu Apr 7 12:30:47 2022 -0400 chore(flutter): remove unused imports commit 09c9517de2917dc5ccd7de019b50a7875386991c Author: GroovinChip <[email protected]> Date: Thu Apr 7 12:29:58 2022 -0400 test(flutter): ThreadSeparator test commit 9422d1129211aaaac9a51ca4d29fe94bde06593c Author: GroovinChip <[email protected]> Date: Thu Apr 7 12:19:43 2022 -0400 chore(flutter): rename LoadingIndicator parameter commit 63af943a1a16e78a1e0377d76176420bd7e1fbc7 Author: GroovinChip <[email protected]> Date: Thu Apr 7 11:47:46 2022 -0400 test(flutter): FloatingDateDivider test commit d8e67cf6fca2b76ac091a9047104c0910d5fdfef Author: GroovinChip <[email protected]> Date: Tue Mar 29 15:19:39 2022 -0400 test(flutter): InputAttachment tests commit d2b7d5dd3304c8be6f74bf5b0281d7a95dd027ec Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:41:40 2022 -0400 test(flutter): DmCheckbox tests commit 01567f235a2f00880bb530b232489e3790159b25 Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:16:08 2022 -0400 chore(flutter): remove unused imports commit c9625858e840df6082506ae9374f1d2710053cbd Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:15:44 2022 -0400 test(flutter): CountdownButton tests commit d770474bb11ee8b36a3b0e18db31695fd812a6b3 Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:11:37 2022 -0400 test(flutter): CommandButton tests commit 95e63706d1dd971373d39d471894a38bb2a634f2 Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:07:43 2022 -0400 test(flutter): ClearInputItem tests commit 4156586945c91408cb7943b6c57c2ae560a2857c Author: GroovinChip <[email protected]> Date: Tue Mar 29 14:03:59 2022 -0400 test(flutter): AttachmentButton tests commit bba042806326999aabf338636c6e33900de86160 Author: GroovinChip <[email protected]> Date: Tue Mar 29 13:00:42 2022 -0400 test(flutter): golden test for SendButton commit e2cd0f00b9eeb8d81399f6316edb298234de12dc Author: GroovinChip <[email protected]> Date: Tue Mar 29 12:56:52 2022 -0400 chore(flutter): deal with lints commit b4e045f9cbd54c4d42c3f30acfe72d5f8ccf9d82 Author: GroovinChip <[email protected]> Date: Tue Mar 29 12:56:00 2022 -0400 test(flutter): SendButton test commit 4e3d5db0bddb97e508fcdda2e7272fe31e70808f Author: GroovinChip <[email protected]> Date: Tue Mar 29 12:38:21 2022 -0400 test(flutter): KeyboardShortcutRunner tests commit 0dfd7a0bd6fd6bc76902e87f62ac29da0b01bac6 Author: GroovinChip <[email protected]> Date: Tue Mar 29 12:17:30 2022 -0400 test(flutter): UploadProgressIndicator commit ec50c8aea716bdc19e4308e367c8331211c61ea3 Author: GroovinChip <[email protected]> Date: Tue Mar 29 11:47:53 2022 -0400 test(flutter): SendingIndicator tests commit e87bd1450fb5bfb713a867c7e831968f76428848 Author: GroovinChip <[email protected]> Date: Tue Mar 29 11:32:35 2022 -0400 test(flutter): more gallery tests commit 93d46ca649277fe6d289a70f0bdd23584fd0ab94 Author: GroovinChip <[email protected]> Date: Tue Mar 29 11:17:29 2022 -0400 test(flutter): remove bad mock commit c7b03a39f0f3ca7a334dfb908f3473b5e4deab38 Author: GroovinChip <[email protected]> Date: Tue Mar 29 10:45:45 2022 -0400 chore(flutter): minor refactor commit 33aeb7b5ed398287cb930f3b4702f47a82fce2c6 Author: GroovinChip <[email protected]> Date: Tue Mar 29 09:49:07 2022 -0400 test(flutter): MessageDialog tests commit 220b3ae591e135d9606254666e04d4771945d0ae Author: GroovinChip <[email protected]> Date: Tue Mar 29 09:42:51 2022 -0400 test(flutter): DeleteMessageDialog tests commit db508478bb96749971f563de87b6c2a49e1e4e57 Author: GroovinChip <[email protected]> Date: Mon Mar 28 14:46:41 2022 -0400 test(flutter): ConfirmationDialog tests commit e8172dad6bfe18f40b65554ef2c6c16d7437cb56 Author: GroovinChip <[email protected]> Date: Mon Mar 28 14:35:50 2022 -0400 test(flutter): ChannelInfoDialog test commit 04c21458a601af3c3f4a5fc5ce9e8b61493c208c Author: GroovinChip <[email protected]> Date: Mon Mar 28 13:57:02 2022 -0400 test(flutter): DownloadMenuItem tests commit afee1dace543e2fef8543f9b035f684b2ee00f3b Author: GroovinChip <[email protected]> Date: Mon Mar 28 13:53:21 2022 -0400 test(flutter): StreamChatContextMenuItem tests commit 4616b1a973d0cf5224b5a71914c9908339c8462a Author: GroovinChip <[email protected]> Date: Mon Mar 28 13:02:07 2022 -0400 test(flutter): golden test for ErrorAlertSheet commit 22fb8241ae333f57ade8e8550f87695929836b2b Author: GroovinChip <[email protected]> Date: Mon Mar 28 12:59:01 2022 -0400 test(flutter): golden test for EditMessageSheet commit 0be79964827f344ba66e99cb9e5bd3d627aed01d Author: GroovinChip <[email protected]> Date: Mon Mar 28 12:56:08 2022 -0400 test(flutter): golden test for AttachmentModalSheet commit 0ae842a98143478885bc211c788353c8bca57b40 Author: GroovinChip <[email protected]> Date: Mon Mar 28 12:53:26 2022 -0400 test(flutter): GroupAvatar tests commit 697d294c6c7e1bba1d9089a53d5c60d8609ea64e Author: GroovinChip <[email protected]> Date: Mon Mar 28 12:30:25 2022 -0400 test(flutter): UserAvatar tests commit c03ba4124068c83d3a49c4db00ca18a659801c23 Author: GroovinChip <[email protected]> Date: Mon Mar 28 11:04:55 2022 -0400 test(flutter): ErrorAlertSheet test commit 3414f28530b0e22614b83af20361f21dd4967fae Author: GroovinChip <[email protected]> Date: Mon Mar 28 10:45:37 2022 -0400 test(flutter): EditMessageSheet test commit b2962b641dfbf14c11d3da7f80a4be27f9980d90 Author: GroovinChip <[email protected]> Date: Mon Mar 28 10:37:18 2022 -0400 test(flutter): AttachmentModalSheet test test(flutter): AttachmentModalSheet test chore(flutter): update exports commit 7d7e278692c65dea84e7ef3b2d3c1311ebb28408 Author: GroovinChip <[email protected]> Date: Mon Mar 28 09:52:29 2022 -0400 test(flutter): more organization commit 4479b649835acc62d8a0553f9505a0af8077abfc Author: GroovinChip <[email protected]> Date: Mon Mar 28 09:33:06 2022 -0400 test(flutter): tweak AttachmentHandler tests commit b4a41106567e3c08105ce8dc272b1ebf40b3b0bb Author: GroovinChip <[email protected]> Date: Mon Mar 28 09:24:00 2022 -0400 chore(flutter): streamline attachment handling * MobileAttachmentHandler for streamlining related code for mobile * Break out DesktopAttachmentHandler into its own file * Update minSdk version in Android runner commit bfb06d31b79cd941d8b611c7e89e2d7728a394cb Merge: a44b3b39 15c83d03 Author: GroovinChip <[email protected]> Date: Fri Mar 25 13:00:27 2022 -0400 Merge branch 'desk_web_dev' into tests commit a44b3b390adc25a00d1597bb1460973e8555fa9e Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:59:14 2022 -0400 chore(flutter): start cleaning up test dir commit de104a3491d942d12f72dd66152b62c6e8b10a5b Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:56:48 2022 -0400 test(flutter): VideoAttachment test commit 4748a1c081a3272f7fdbdeb69c501344e221e01e Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:52:30 2022 -0400 test(flutter): UrlAttachment test commit 54408ef70f07c60adf8350553fdf3173e5edd7c7 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:48:24 2022 -0400 test(flutter): ImageGroup test commit 811cff069e490c6bd03be0c0ac2f5ec86fb990e9 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:46:04 2022 -0400 test(flutter): ImageAttachment test commit a72677976c6524e76793432b0520e99cac4e7a62 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:34:06 2022 -0400 test(flutter): GiphyAttachment test commit 32ddac022e2c149726b4b6607f41850d556055b8 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:28:01 2022 -0400 test(flutter): FileAttachment test commit 6534958cab775be1fb9bda56e67caa4746aa1502 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:14:25 2022 -0400 test(flutter): AttachmentError test commit 718a83c10c4e883ce01c9c002ced24de671d30b9 Author: GroovinChip <[email protected]> Date: Fri Mar 25 12:06:43 2022 -0400 test(flutter): AttachmentUploadStateBuilder test commit a05108c460e36b2e425e4ec06f8bf408b0c20e97 Author: GroovinChip <[email protected]> Date: Fri Mar 25 11:44:34 2022 -0400 chore(flutter): minor cleanup commit 6e73038ac3714a2bd96f8c587af7f743bf2b2023 Author: GroovinChip <[email protected]> Date: Fri Mar 25 11:43:23 2022 -0400 test(flutter): AttachmentTitle test commit c6ee52ec951877feef31dc1427eec3bfc2bd8fcb Author: GroovinChip <[email protected]> Date: Fri Mar 25 11:43:11 2022 -0400 test(flutter): minor cleanup commit 09845cd845aa8c546043f4ae04805a8e6f53f61c Author: GroovinChip <[email protected]> Date: Fri Mar 25 11:42:52 2022 -0400 chore(flutter): improve device segmentation & vlc init check commit 172e6b25a064b5147b7641f29ebff3c920156f70 Author: GroovinChip <[email protected]> Date: Fri Mar 25 10:37:43 2022 -0400 test(flutter): attachment_handler tests commit 5f5a91c70cd7c069f39518f1f50bb74ab484c7e9 Author: GroovinChip <[email protected]> Date: Wed Mar 23 13:55:27 2022 -0400 test(flutter): test PlatformWidgetBuilder & DesktopWidgetBuilder commit 5a457b7557cb4127b74ee022389a24e5e9150601 Author: GroovinChip <[email protected]> Date: Wed Mar 23 13:55:05 2022 -0400 chore(flutter): make base platform widgets testable Also fix environment check in stream_chat.dart commit 88fb16018103c581236aeebd978e705b223b2243 Author: GroovinChip <[email protected]> Date: Wed Mar 23 11:59:19 2022 -0400 chore(flutter): add readme to conditional_parent_widget library commit e9307f8a6a5639d59358c245f033f9d9b209fe20 Author: GroovinChip <[email protected]> Date: Wed Mar 23 11:54:54 2022 -0400 test(flutter): ConditionalParentBuilder tests * chore(flutter): add thumblr dependency * chore(flutter): fix typos * chore(flutter): properly order super calls * feat(flutter) video thumbnails on desktop * chore(flutter): fix incorrect file name * chore(flutter): fix docs for generateVideoThumbnail * chore(flutter): remove incorrect comment * chore(flutter): run flutter format * chore(flutter): ignore lint * chore(flutter): flutter format * chore(flutter): fix tests * fix(flutter): fix MessageListView bug caused by rebase * chore(flutter): migrate desktop stuff to v4/stream_message_input.dart * chore(flutter): migrate over desktop changes for message replies * chore(flutter): fix message replies on desktop * chore(flutter): address lints * chore(flutter): update dart_vlc * chore(flutter): melos clean & update photo_view * fix(ui): port #1118 * fix(ui): use attachmentButtonBuilder * fix(ui): fix file attachment * format * fix(ui): fix message bottom row * chore(flutter sample): use modern window look * chore(flutter): pass userAvatarBuilder through to MessageWidgetContent Also pass onUserAvatarTap & userAvatarBuilder through to UserAvatarTransform * chore: fix MessageWidget menu actions * fix(flutter): fix "enter" sending empty messages in StreamMessageInput * fix(flutter): fix Giphys not loading in quoted messages * chore(flutter): update thumblr * feat(flutter): keyboard navigation for gallery items * fix(ui): fix double date divider * fix(ui): add scroll to bottom back * fix(ui): use mouse region * fix(ui): remove backbutton in desktop example * fix(ui): rename full screen media widget * feat(flutter): elevation prop for gallery header * chore(flutter): update dart_vlc * fix(flutter): fix "show in chat" showing when it shouldn't * fix(flutter): fix "show in chat" for mobile * docs(flutter): minor doc update * chore(flutter): add onMessageSwiped in example app * chore(flutter): pass usernameBuilder through to BottomRow * fix(flutter): hacky fix for chonky link previews * fix(flutter): fix chonky text messages * feat(flutter): press "esc" to close gallery Also refactors KeyboardShortcutRunner API * chore(flutter): fix innaccurate name * fix(ui): fix attachment + long text sizing * add key to channelpage * fix(ui): better example * fix format * fix example * chore(flutter): revert modern window look * improvement(flutter): improve `GalleryNavigationItem` * Make buttons more visible * Spacing from the sides of the screen * fix video thumbnail * fix split view * fix(ui): fix giphy in thread * feat(flutter): desktop message reactions commit 4b880ecb6d4dcbf315a7d41d863174817a9c530a Author: Reuben Turner <[email protected]> Date: Thu Jun 9 12:25:21 2022 -0400 fix(flutter): fixes for desktop reactions * Fix reactions for reverse messages * Ensure reactions near the edges of MessageListView show properly commit f511513f8dbb272397e81d08bad33ee60108d738 Author: Salvatore Giordano <[email protected]> Date: Thu Jun 9 16:43:03 2022 +0200 fix(ui): fix DesktopReactionBuilder and Portal commit c4885648330552f5212c5ba6b771c0f07023f6ae Merge: 2b710a2b 72b34c21 Author: Reuben Turner <[email protected]> Date: Wed Jun 8 14:12:44 2022 -0400 Merge branch 'desk_web_dev' into desktop_reactions commit 2b710a2bb610326123d5b7f75c26caa705114c9b Author: Reuben Turner <[email protected]> Date: Mon Jun 6 12:19:12 2022 -0400 chore(flutter): add debug props commit e24dabc1de12da8dce697b6d4e1a3617e9835549 Author: Reuben Turner <[email protected]> Date: Mon Jun 6 12:00:03 2022 -0400 feat(flutter): first pass at desktop reactions popup on mouse hover * fix(flutter): potential fix for #1199 * fix(flutter): fix non-reversed reactions placement * refactor(flutter): extract bottom reaction, stacked reaction with debug props * fix(flutter): fixes for desktop reactions * Fix bottom row being rendered above reaction buttons * Fix padding issues related to avatars * chore(flutter): add back lost changes from 2b1fb6a Also removed commented code * chore(flutter): stacked reactions update * Show "You" if userId matches the reaction username (per design) * Ensure long names don't throw off alignment * chore(flutter): format * fix(ui): fix mime type generation * chore(flutter): tweak desktop video * chore(flutter): add some missing things from develop * merge develop * fix merge * fix merge * format * fix tests * increate test action timeout * remove test * refactor(ui,core): remove deprecated code and cleanup * run builder * update coverage threasholds * refactor(ui): remove arrow function from countdown button * Update packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart * Update packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart * update comment * fix(ui): fix attachment + link solution * fix(ui): fix attachment + link solution * chore(flutter) update changelog and version * chore(localizations): update changelog and versions * chore(llc,core,ui,persistence,localizations): update changelog and pubspec * chore(llc,core,ui,persistence,localizations): update changelog and pubspec * fix example * fix deps * fix(ui): fix enter sending the message while in slowmode * fix(ui): fix reaction list * fix web * fix(ui): fix image download when using assetUrl * fix(ui): fix typo in changelog * fix(ui): fix gallery navigation * fix(ui): fix giphy sending size * fix(ui): add keyboard shorcuts to gallery view * fix(ui): fix gallery footer * fix(core): allow scroll notification bubbling in lazy load scroll view * fix(ui): fix lazy load scroll view * fix(ui): fix loading to unread position * fix(ui): fix last message preview * chore(ui): update changelog * fix tests * fix(llc): fix connecting while connecting and disconneting (#1237) * fix(llc): fix connecting while connecting and disconneting * chore(llc): update changelog * chore(llc): add test * chore(llc): improve test * fix(llc): _resetRequestFlags regardless of the connection status * feat(llc): add support for AttachmentFileUploaderProvider. (#1246) * feat(llc): add support for `AttachmentFileUploaderProvider`. Signed-off-by: xsahil03x <[email protected]> * chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> * refactor(llc): Remove deprecated attachmentFileUploader field. Signed-off-by: xsahil03x <[email protected]> * port no localization * fix(ui): fix jiffy locale initialization (#1255) * fix(ui): fix jiffy locale initialization * chore(ui): update changelog * fix(ui): cleanup * chore(llc,core,ui,persistence,localizations): align changelog * chore(llc,core,ui,persistence,localizations): add links to 4.x changelogs * fix(ui): fix date divider not showing (#1259) * fix(ui): fix date divider not showing * chore(ui): update changelog * chore(ui): minor changes Signed-off-by: xsahil03x <[email protected]> * fix(ui): fix separator Signed-off-by: xsahil03x <[email protected]> Co-authored-by: Sahil Kumar <[email protected]> * chore(ui): add contextmenu requirements for web (#1261) * fix unread separator * feat(ui): `StreamChatConfiguration` class (#1128) * feat: StreamChatConfiguration class See #1125 for more details * chore(flutter): update changelog * fix analysis * cleanup * feat(ui): add enforceUniqueReactions to `StreamChatConfiguration` widget * add tests * refactor(ui): rename StreamChatConfigurationProvider and StreamChatConfiguration * refactor(ui): rename StreamChat.config to streamChatConfigData * refactor(ui): make enforceUniqueReactions non nullable * chore(ui): fix analysis errors * refactor(ui): make config fields non nullable * chore(ui): format Co-authored-by: Salvatore Giordano <[email protected]> * feat(ui, core): add StreamAutocomplete (#1263) * feat(ui, core): add stream_autocomplete.dart Signed-off-by: xsahil03x <[email protected]> * refactor(core, ui): refactor message input controller Signed-off-by: xsahil03x <[email protected]> * fix(ui): minor fixes Signed-off-by: xsahil03x <[email protected]> * feat(ui): add StreamAutocompleteOptions widget Signed-off-by: xsahil03x <[email protected]> * Update packages/stream_chat_flutter/lib/src/autocomplete/stream_autocomplete.dart * fix(ui): cleanup * chore(core, ui): cleanup Signed-off-by: xsahil03x <[email protected]> * chore(ui): ignore no empty block * chore(ui): more cleanup Signed-off-by: xsahil03x <[email protected]> * fix(ui): fix command button for bigger screens, other fixes. Signed-off-by: xsahil03x <[email protected]> Co-authored-by: Salvatore Giordano <[email protected]> * fix(ui): increase default file limit to 100MB (#1279) * fix(ui): increase default file limit to 100MB * Update packages/stream_chat_flutter/lib/src/message_input/stream_message_input.dart * fix(ui): export kDefaultMaxAttachmentSize and use it in `StreamAttachmentPicker` default * feat(ui): show dialog after clicking on the camera button and permission is denied (#1262) * feat(ui): show dialog after clicking on the camera button and permission is denied * chore(ui): update changelog * Remove permission_handler plugin and refactor * cleanup * cleanup * add tests * fix(ui): reset the permission state on resume * cleanup * fix concurrent calls * fix(ui): cleanup * fix(ui): add try catch * fix(llc, ui): copy file to tempdir before uploading a file and do not serialize bytes (#1285) * fix(llc, ui): copy file to tempdir before uploading a file and do not serialize bytes * fix(llc): do not save failed messages with attachments on web * fix(llc): replace every with any * chore(llc): add test * fix analysis * fix analysis * chore(llc,core,ui,persistence): align changelog with v4 * fix(ui): do not linkify system message mentions * chore(llc,core,ui,persistence,localizations): update changelog and pubspec (#1289) * chore(ui): update deps * chore(llc,core,ui,persistence,localizations): update changelog and pubspec * chore(ui): publish beta.2 * feat(core,ui): add StreamMemberListView, StreamMemberGridView, and StreamMemberListController (#1291) * feat(core,ui): add StreamMemberListView, StreamMemberGridView, and StreamMemberListController * chore(core,ui): update changelog * fix(ui): fix exports * chore(ui): add tests * chore(ui): add StreamMemberGridTile typedef * chore(ui): minor changes Signed-off-by: xsahil03x <[email protected]> Co-authored-by: Sahil Kumar <[email protected]> * fix(ui): do not send empty message * refactor(ui,localization): removed emoji picker and connected (#1292) * refactor(ui,localization): removed emoji picker and connected * chore(ui,localizations): update changelog * chore(repo): update ui coverage threashold * doc(ui): add autocomplete_triggers guide * chore(ui): add tests for isOnlyEmoji * chore(ui): add tests for isOnlyEmoji * feat(ui): add `StreamAutocompleteTrigger.triggerOnlyAfterSpace` * fix(llc): Fix `Channel.removeMessage` not able to remove thread message. (#1294) Signed-off-by: xsahil03x <[email protected]> * chore(ui): flutter format Signed-off-by: xsahil03x <[email protected]> * fix(ui): fix `stopTyping` unhandled exceptions when network is off or spotty. (#1296) * feat(llc, ui): Introduce `keyStrokeHandler` to properly handle keyStrokes. Signed-off-by: xsahil03x <[email protected]> * chore(ui): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> * test(llc): add key_stroke_handler_test.dart Signed-off-by: xsahil03x <[email protected]> * chore(ui): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> * fix(core): only handle the channel list event if the state is success. Signed-off-by: xsahil03x <[email protected]> * feat(llc): add `hide_history` flag in `channel.addMembers`. Signed-off-by: xsahil03x <[email protected]> * refactor(llc): Convert positional argument to optional arguments Signed-off-by: xsahil03x <[email protected]> * test(llc): update tests Signed-off-by: xsahil03x <[email protected]> * perf: improve image resizing * fix(core): use copyWith while setting controller items. Signed-off-by: xsahil03x <[email protected]> * feat(ui): Added `thumbnailSize`, `thumbnailResizeType`, and `thumbnailCropType` params to `StreamMessageWidget` and `StreamAttachmentPicker` to customize the appearance of image thumbnails. Signed-off-by: xsahil03x <[email protected]> * fix analysis * Release v4.5.0 (#1308) * chore(llc, ui, core): bump version to v4.5.0 Signed-off-by: xsahil03x <[email protected]> * chore(ui, core): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> Signed-off-by: xsahil03x <[email protected]> * third party video integration * fix llc * refactor: clean up and add docs * chore(llc): update changelog * chore(llc): add tests * chore(llc): add more tests * fix analysis * chore(llc): add more tests * chore(llc): add more tests * fix(llc): only start listening to event once connected. Signed-off-by: xsahil03x <[email protected]> * chore(llc): update CHANGELOG.md Signed-off-by: xsahil03x <[email protected]> * chore(llc): move files to correct folders. Signed-off-by: xsahil03x <[email protected]> * fix(ui): Fix message text hiding because of a flutter bug (#1327) * fix(ui): remove clip * chore(ui): update changelog * cleanup * fix(ui): hitting "enter" on the android keyboard sends the message instead of going to a new line. Signed-off-by: xsahil03x <[email protected]> * feat(ui): add support for additional text field params in message input. Signed-off-by: xsahil03x <[email protected]> * fix analysis * remove print * chore(llc,core,ui): update pubspec and changelogs (#1333) * fix(ui): show placeholder for web video thumbs * fix file download on web * fix reply and show buttons * remove tests * treat svg as files * fix(ui): change default image resize to clip * solve some todos * polish v5 fixing analysis issues * feat(ui): stream attachment picker bottom sheet (#1336) * refactor(ui): improve stream_attachment_handler.dart, add stream_attachment_picker_bottom_sheet.dart Signed-off-by: xsahil03x <[email protected]> * feat(ui): add copyWith in command_button.dart Signed-off-by: xsahil03x <[email protected]> * chore(ui): remove permission_manager.dart Signed-off-by: xsahil03x <[email protected]> * fix(ui): some fixes Signed-off-by: xsahil03x <[email protected]> * fix(ui): more fixes Signed-off-by: xsahil03x <[email protected]> * chore(ui): fix lints, add doc comments Signed-off-by: xsahil03x <[email protected]> * chore(ci): update flutter version Signed-off-by: xsahil03x <[email protected]> * chore(ui): more fixes Signed-off-by: xsahil03x <[email protected]> * chore(ui): more fixes Signed-off-by: xsahil03x <[email protected]> * chore(ui): more fixes Signed-off-by: xsahil03x <[email protected]> * chore(ci): update melos and flutter version Signed-off-by: xsahil03x <[email protected]> Signed-off-by: xsahil03x <[email protected]> Co-authored-by: Salvatore Giordano <[email protected]> * remove todo * update changelog * update changelogs * revert * update coverage threasholds Signed-off-by: xsahil03x <[email protected]> Co-authored-by: GroovinChip <[email protected]> Co-authored-by: Sahil Kumar <[email protected]> Co-authored-by: Gordon Hayes <[email protected]> Co-authored-by: Sacha Arbonel <[email protected]>
This commit is contained in:
co-authored by
Sahil Kumar
GroovinChip
Gordon Hayes
Sacha Arbonel
parent
3e1d6e91c7
commit
4f2542291c
+91
@@ -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,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+396
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+394
@@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
+421
@@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
+397
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+365
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
+364
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+239
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+387
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
+417
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+87
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+259
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+15
@@ -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,
|
||||
);
|
||||
+107
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+25
@@ -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(),
|
||||
);
|
||||
}
|
||||
+24
@@ -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(),
|
||||
);
|
||||
}
|
||||
+102
@@ -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,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+394
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+189
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+381
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user