Merge branch 'develop' into feat/positioned-list-experiment
This commit is contained in:
@@ -1,3 +1,98 @@
|
||||
## Upcoming
|
||||
|
||||
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`
|
||||
|
||||
## 3.1.1
|
||||
|
||||
- Updated `stream_chat_flutter_core` dependency to [`3.1.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||
- Updated `file_picker`, `image_gallery_saver`, and `video_thumbnail` to the latest versions.
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [[#687]](https://github.com/GetStream/stream-chat-flutter/issues/687): Fix Users losing their place in the conversation after replying in threads.
|
||||
- Fixed floating date stream subscription causing "Bad state: stream has already been listened.” error.
|
||||
- Fixed `String` capitalize extension not working on empty strings.
|
||||
|
||||
✅ Added
|
||||
|
||||
- Added `MessageInput.customOverlays` property to add custom overlays to the message input.
|
||||
- Added `MessageInput.mentionAllAppUsers` property to mention all app users in the message input.
|
||||
- The `MessageInput` now supports local search for channels with less than 100 members.
|
||||
- Added `MessageListView.paginationLoadingIndicatorBuilder` to override the default loading indicator shown while paginating the message list.
|
||||
- Added new `linkBackgroundColor` in `MessageTheme` for setting background colors of link attachments.
|
||||
|
||||
⚠️ Deprecated
|
||||
|
||||
- `MessageInput.mentionsTileBuilder` is now deprecated in favor of `MessageInput.userMentionsTileBuilder`.
|
||||
- `MentionTile` is now deprecated in favor of `UserMentionsTile`.
|
||||
|
||||
## 3.0.0
|
||||
|
||||
- Updated `stream_chat_flutter_core` dependency to [`3.0.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||
|
||||
🛑️ Breaking Changes from `2.2.1`
|
||||
|
||||
- `UserListView` `filter` property now is non-nullable.
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [[#668]](https://github.com/GetStream/stream-chat-flutter/issues/668): Fix `MessageInput` rendering errors in case
|
||||
there are no actions available to show.
|
||||
- [[#349]](https://github.com/GetStream/stream-chat-flutter/issues/349): Fix `MessageInput` attachment render overflow error.
|
||||
- `MessageInput` overlays now follow the `MessageInput` focus.
|
||||
- [[#674]](https://github.com/GetStream/stream-chat-flutter/issues/674): Check scrollController is attached before calling jump in MessageListView.
|
||||
- Fixed `MessageListView` header and footer when `reverse: false`.
|
||||
|
||||
🔄 Changed
|
||||
|
||||
- Animation curves changed from default `Curves.linear` to `Curves.easeOut` and `Curves.easeIn` for attachment controls.
|
||||
- Removed default padding in `DateDivider` in `MessageListView`
|
||||
|
||||
✅ Added
|
||||
|
||||
- Added `MessageInput.customPortalOptions` property to add custom overlays to the `MessageInput`.
|
||||
|
||||
## 2.2.1
|
||||
|
||||
⚠️ Deprecated
|
||||
|
||||
- `MessageSearchListView` `paginationParams` property is now deprecated in favor of `limit`.
|
||||
```dart
|
||||
// previous
|
||||
paginationParams = const PaginationParams(limit: 30)
|
||||
|
||||
// new
|
||||
limit = 30
|
||||
```
|
||||
- `UserListView` `pagination` property is now deprecated in favor of `limit`.
|
||||
```dart
|
||||
// previous
|
||||
pagination = const PaginationParams(limit: 30)
|
||||
|
||||
// new
|
||||
limit = 30
|
||||
```
|
||||
- `ChannelListView` `pagination` property is now deprecated in favor of `limit`.
|
||||
```dart
|
||||
// previous
|
||||
pagination = const PaginationParams(limit: 30)
|
||||
|
||||
// new
|
||||
limit = 30
|
||||
```
|
||||
|
||||
🔄 Changed
|
||||
|
||||
- `UserListViewCore` filter property now has a default value.
|
||||
```dart
|
||||
filter = const Filter.empty()
|
||||
```
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Fixed `MessageSearchListView` pagination.
|
||||
- Fixed `MessageWidget` attachment tap callbacks.
|
||||
|
||||
## 2.2.1
|
||||
|
||||
- Updated `stream_chat_flutter_core` dependency to 2.2.1
|
||||
@@ -7,14 +102,13 @@
|
||||
✅ Added
|
||||
|
||||
- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516):
|
||||
Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image
|
||||
is loading
|
||||
Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image is loading
|
||||
- Added a `backgroundColor` property to the following widgets:
|
||||
- `ChannelHeader`
|
||||
- `ChannelListHeader`
|
||||
- `GalleryHeader`
|
||||
- `GalleryFooter`
|
||||
- `ThreadHeader`
|
||||
- `ChannelHeader`
|
||||
- `ChannelListHeader`
|
||||
- `GalleryHeader`
|
||||
- `GalleryFooter`
|
||||
- `ThreadHeader`
|
||||
- Added `MessageInput.attachmentLimit` in order to limit the no. of attachments that can be sent with a single message.
|
||||
- Added `MessageInput.onAttachmentLimitExceed` callback which will be called when the `attachmentLimit` is exceeded.
|
||||
This will override the default error alert behaviour.
|
||||
@@ -34,9 +128,8 @@ You can call `.copyWith` to customize just a subset of properties.
|
||||
|
||||
🔄 Changed
|
||||
|
||||
Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with
|
||||
them, and have been upgraded with some goodies like `lerp` functions. Here's the full naming
|
||||
breakdown:
|
||||
Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with them, and have been
|
||||
upgraded with some goodies like `lerp` functions. Here's the full naming breakdown:
|
||||
|
||||
* `AvatarTheme` is now `AvatarThemeData`
|
||||
* `ChannelHeaderTheme` is now `ChannelHeaderThemeData`
|
||||
@@ -53,18 +146,18 @@ breakdown:
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the
|
||||
camera is null.
|
||||
- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null.
|
||||
- Fixed date dividers position/alignment in non reversed `MessageListView`.
|
||||
- Fixed `MessageListView` not opening to the right initialMessage if `StreamChannel.initialMessageId` is set.
|
||||
- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when sending a message with no text.
|
||||
- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when
|
||||
sending a message with no text.
|
||||
|
||||
## 2.1.2
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no
|
||||
members when sending message
|
||||
- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no members when sending
|
||||
message
|
||||
|
||||
## 2.1.1
|
||||
|
||||
@@ -82,8 +175,7 @@ breakdown:
|
||||
🔄 Changed
|
||||
|
||||
- `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`.
|
||||
- `StreamChat.of(context).userStream` is now deprecated in favor
|
||||
of `StreamChat.of(context).currentUserStream`.
|
||||
- `StreamChat.of(context).userStream` is now deprecated in favor of `StreamChat.of(context).currentUserStream`.
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
@@ -136,8 +228,7 @@ You can call `.copyWith` to customize just a subset of properties
|
||||
|
||||
- Added video compress options (frame and quality) to `MessageInput`
|
||||
- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads
|
||||
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView
|
||||
header/footer
|
||||
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer
|
||||
- `MessageWidget` accepts a `userAvatarBuilder`
|
||||
- Added pinMessage ui support
|
||||
- Added `MessageListView.threadSeparatorBuilder` property
|
||||
@@ -146,12 +237,10 @@ You can call `.copyWith` to customize just a subset of properties
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text
|
||||
box when editing message
|
||||
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator
|
||||
use case
|
||||
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without
|
||||
a reload
|
||||
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing
|
||||
message
|
||||
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case
|
||||
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload
|
||||
- `MessageListView` not rendering if the user is not a member of the channel
|
||||
- Fix `MessageInput` overflow when there are no actions
|
||||
- Minor fixes and improvements
|
||||
@@ -204,18 +293,15 @@ You can call `.copyWith` to customize just a subset of properties.
|
||||
✅ Added
|
||||
|
||||
- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads
|
||||
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView
|
||||
header/footer
|
||||
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer
|
||||
- `MessageWidget` accepts a `userAvatarBuilder`
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text
|
||||
box when editing message
|
||||
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator
|
||||
use case
|
||||
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without
|
||||
a reload
|
||||
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing
|
||||
message
|
||||
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case
|
||||
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload
|
||||
- `MessageListView` not rendering if the user is not a member of the channel
|
||||
|
||||
## 2.0.0-nullsafety.7
|
||||
@@ -285,8 +371,7 @@ You can call `.copyWith` to customize just a subset of properties.
|
||||
- Show error messages as system and keep them in the message input
|
||||
- Remove notification badge logic
|
||||
- Use shimmer while loading images
|
||||
- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated
|
||||
to `MessageInput`
|
||||
- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated to `MessageInput`
|
||||
- Add possibility to specify custom message actions using `MessageWidget.customActions`
|
||||
- Added `MessageListView.onAttachmentTap` callback
|
||||
- Fixed message newline issue
|
||||
@@ -343,8 +428,7 @@ You can call `.copyWith` to customize just a subset of properties.
|
||||
- Improved api documentation
|
||||
- Updated `stream_chat` dependency to `^1.0.0-beta`
|
||||
- Extracted sample app into dedicated [repo](https://github.com/GetStream/flutter-samples)
|
||||
- Reimplemented existing widgets
|
||||
using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core)
|
||||
- Reimplemented existing widgets using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core)
|
||||
|
||||
## 0.2.21
|
||||
|
||||
@@ -361,8 +445,8 @@ You can call `.copyWith` to customize just a subset of properties.
|
||||
|
||||
## 0.2.20+2
|
||||
|
||||
- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the
|
||||
list when a new message arrives
|
||||
- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message
|
||||
arrives
|
||||
|
||||
## 0.2.20+1
|
||||
|
||||
@@ -396,8 +480,7 @@ You can call `.copyWith` to customize just a subset of properties.
|
||||
|
||||
## 0.2.16
|
||||
|
||||
- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress
|
||||
implementation
|
||||
- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress implementation
|
||||
- Make public autofocus field of the TextField of message_input
|
||||
|
||||
## 0.2.15
|
||||
@@ -582,11 +665,10 @@ You can call `.copyWith` to customize just a subset of properties.
|
||||
|
||||
## 0.2.1-alpha+1
|
||||
|
||||
- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have
|
||||
the `StreamChat` widget as ancestor in every route. Now the recommended way to add `StreamChat` to
|
||||
your app is using the `builder` property of your `MaterialApp` widget. Otherwise you can use it in
|
||||
the usual way, but you need to add a `StreamChat` widget to every route of your app.
|
||||
Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more
|
||||
- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have the `StreamChat` widget
|
||||
as ancestor in every route. Now the recommended way to add `StreamChat` to your app is using the `builder` property of
|
||||
your `MaterialApp` widget. Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to
|
||||
every route of your app. Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more
|
||||
information.
|
||||
|
||||
```dart
|
||||
@@ -688,8 +770,8 @@ Widget build(BuildContext context) {
|
||||
|
||||
- Add gesture (vertical drag down) to close the keyboard
|
||||
|
||||
- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will
|
||||
even close the keyboard)
|
||||
- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will even close the
|
||||
keyboard)
|
||||
|
||||
The property showVideoFullScreen was added mainly because of this issue brianegan/chewie#261
|
||||
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>8.0</string>
|
||||
<string>9.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -106,9 +106,7 @@ class ChannelListPage extends StatelessWidget {
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
pagination: const PaginationParams(
|
||||
limit: 20,
|
||||
),
|
||||
limit: 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -83,9 +83,7 @@ class ChannelListPage extends StatelessWidget {
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
pagination: const PaginationParams(
|
||||
limit: 20,
|
||||
),
|
||||
limit: 20,
|
||||
channelWidget: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -85,9 +85,7 @@ class ChannelListPage extends StatelessWidget {
|
||||
),
|
||||
channelPreviewBuilder: _channelPreviewBuilder,
|
||||
// sort: [SortOption('last_message_at')],
|
||||
pagination: const PaginationParams(
|
||||
limit: 20,
|
||||
),
|
||||
limit: 20,
|
||||
channelWidget: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -69,9 +69,7 @@ class ChannelListPage extends StatelessWidget {
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
pagination: const PaginationParams(
|
||||
limit: 20,
|
||||
),
|
||||
limit: 20,
|
||||
channelWidget: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -75,9 +75,7 @@ class ChannelListPage extends StatelessWidget {
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
pagination: const PaginationParams(
|
||||
limit: 20,
|
||||
),
|
||||
limit: 20,
|
||||
channelWidget: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -34,7 +34,7 @@ Future<void> main() async {
|
||||
|
||||
await client.connectUser(
|
||||
User(id: 'super-band-9'),
|
||||
'''eyJ0eXAiO«iJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
|
||||
'''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
|
||||
);
|
||||
|
||||
runApp(
|
||||
@@ -102,9 +102,7 @@ class ChannelListPage extends StatelessWidget {
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
pagination: const PaginationParams(
|
||||
limit: 20,
|
||||
),
|
||||
limit: 20,
|
||||
channelWidget: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -19,42 +19,36 @@ class AttachmentTitle extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
onTap: () {
|
||||
if (attachment.titleLink != null) {
|
||||
launchURL(context, attachment.titleLink);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
if (attachment.title != null)
|
||||
Text(
|
||||
attachment.title!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: messageTheme.messageTextStyle?.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
Widget build(BuildContext context) {
|
||||
final normalizedTitleLink = attachment.titleLink?.replaceFirst(
|
||||
RegExp(r'https?://(www\.)?'),
|
||||
'',
|
||||
);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final titleLink = attachment.titleLink;
|
||||
if (titleLink != null) launchURL(context, titleLink);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
if (attachment.title != null)
|
||||
Text(
|
||||
attachment.title!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: messageTheme.messageTextStyle?.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
if (attachment.titleLink != null ||
|
||||
attachment.ogScrapeUrl != null)
|
||||
Text(
|
||||
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl!)
|
||||
.authority
|
||||
.split('.')
|
||||
.reversed
|
||||
.take(2)
|
||||
.toList()
|
||||
.reversed
|
||||
.join('.'),
|
||||
style: messageTheme.messageTextStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (normalizedTitleLink != null)
|
||||
Text(normalizedTitleLink, style: messageTheme.messageTextStyle),
|
||||
],
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Widget to build in progress
|
||||
typedef InProgressBuilder = Widget Function(BuildContext, int, int);
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||
|
||||
/// Widget for displaying file attachments
|
||||
class FileAttachment extends AttachmentWidget {
|
||||
@@ -258,7 +257,8 @@ class FileAttachment extends AttachmentWidget {
|
||||
visualDensity: VisualDensity.compact,
|
||||
splashRadius: 16,
|
||||
onPressed: () {
|
||||
launchURL(context, attachment.assetUrl);
|
||||
final assetUrl = attachment.assetUrl;
|
||||
if (assetUrl != null) launchURL(context, assetUrl);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/visible_footnote.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Widget for showing a GIF attachment
|
||||
class GiphyAttachment extends AttachmentWidget {
|
||||
@@ -98,7 +98,13 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: GestureDetector(
|
||||
onTap: () => onAttachmentTap ?? _onImageTap(context),
|
||||
onTap: () {
|
||||
if (onAttachmentTap != null) {
|
||||
onAttachmentTap?.call();
|
||||
} else {
|
||||
_onImageTap(context);
|
||||
}
|
||||
},
|
||||
child: CachedNetworkImage(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
@@ -253,21 +259,12 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
Widget _buildSentAttachment(BuildContext context, String imageUrl) =>
|
||||
SizedBox(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final res =
|
||||
await Navigator.push(context, MaterialPageRoute(builder: (_) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user?.name,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
}));
|
||||
if (res != null) onReturnAction!(res);
|
||||
onTap: () {
|
||||
if (onAttachmentTap != null) {
|
||||
onAttachmentTap?.call();
|
||||
} else {
|
||||
_onImageTap(context);
|
||||
}
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
|
||||
+7
-5
@@ -10,6 +10,7 @@ class UrlAttachment extends StatelessWidget {
|
||||
Key? key,
|
||||
required this.urlAttachment,
|
||||
required this.hostDisplayName,
|
||||
required this.messageTheme,
|
||||
this.textPadding = const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
@@ -25,15 +26,16 @@ class UrlAttachment extends StatelessWidget {
|
||||
/// Padding for text
|
||||
final EdgeInsets textPadding;
|
||||
|
||||
/// [MessageThemeData] for showing image title
|
||||
final MessageThemeData messageTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
launchURL(
|
||||
context,
|
||||
urlAttachment.ogScrapeUrl,
|
||||
);
|
||||
final titleLink = urlAttachment.titleLink;
|
||||
if (titleLink != null) launchURL(context, titleLink);
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -60,7 +62,7 @@ class UrlAttachment extends StatelessWidget {
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
color: chatThemeData.colorTheme.linkBg,
|
||||
color: messageTheme.linkBackgroundColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_info.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Bottom Sheet with options
|
||||
class ChannelBottomSheet extends StatefulWidget {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:stream_chat_flutter/src/back_button.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_info.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_name.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/info_tile.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
@@ -137,12 +138,17 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
break;
|
||||
}
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return InfoTile(
|
||||
showMessage: showConnectionStateTile && showStatus,
|
||||
message: statusString,
|
||||
child: AppBar(
|
||||
textTheme: Theme.of(context).textTheme,
|
||||
brightness: Theme.of(context).brightness,
|
||||
toolbarTextStyle: theme.textTheme.bodyText2,
|
||||
titleTextStyle: theme.textTheme.headline6,
|
||||
systemOverlayStyle: theme.brightness == Brightness.dark
|
||||
? SystemUiOverlayStyle.light
|
||||
: SystemUiOverlayStyle.dark,
|
||||
elevation: 1,
|
||||
leading: leadingWidget,
|
||||
backgroundColor: backgroundColor ?? channelHeaderTheme.color,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Widget which shows channel info
|
||||
class ChannelInfo extends StatelessWidget {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart';
|
||||
@@ -121,12 +122,16 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
final channelListHeaderThemeData = ChannelListHeaderTheme.of(context);
|
||||
final theme = Theme.of(context);
|
||||
return InfoTile(
|
||||
showMessage: showConnectionStateTile && showStatus,
|
||||
message: statusString,
|
||||
child: AppBar(
|
||||
textTheme: Theme.of(context).textTheme,
|
||||
brightness: Theme.of(context).brightness,
|
||||
toolbarTextStyle: theme.textTheme.bodyText2,
|
||||
titleTextStyle: theme.textTheme.headline6,
|
||||
systemOverlayStyle: theme.brightness == Brightness.dark
|
||||
? SystemUiOverlayStyle.light
|
||||
: SystemUiOverlayStyle.dark,
|
||||
elevation: 1,
|
||||
backgroundColor:
|
||||
backgroundColor ?? channelListHeaderThemeData.color,
|
||||
|
||||
@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Callback called when tapping on a channel
|
||||
typedef ChannelTapCallback = void Function(Channel, Widget?);
|
||||
@@ -59,7 +59,7 @@ typedef ViewInfoCallback = void Function(Channel);
|
||||
/// Modify it to change the widget appearance.
|
||||
class ChannelListView extends StatefulWidget {
|
||||
/// Instantiate a new ChannelListView
|
||||
const ChannelListView({
|
||||
ChannelListView({
|
||||
Key? key,
|
||||
this.filter,
|
||||
this.sort,
|
||||
@@ -68,9 +68,12 @@ class ChannelListView extends StatefulWidget {
|
||||
this.presence = false,
|
||||
this.memberLimit,
|
||||
this.messageLimit,
|
||||
this.pagination = const PaginationParams(
|
||||
limit: 25,
|
||||
),
|
||||
@Deprecated(
|
||||
"'pagination' is deprecated and shouldn't be used. "
|
||||
"This property is no longer used, Please use 'limit' instead",
|
||||
)
|
||||
this.pagination,
|
||||
int? limit,
|
||||
this.onChannelTap,
|
||||
this.onChannelLongPress,
|
||||
this.channelWidget,
|
||||
@@ -92,7 +95,8 @@ class ChannelListView extends StatefulWidget {
|
||||
this.onDeletePressed,
|
||||
this.swipeActions,
|
||||
this.channelListController,
|
||||
}) : super(key: key);
|
||||
}) : limit = limit ?? pagination?.limit ?? 25,
|
||||
super(key: key);
|
||||
|
||||
/// If true a default swipe to action behaviour will be added to this widget
|
||||
final bool swipeToAction;
|
||||
@@ -129,7 +133,14 @@ class ChannelListView extends StatefulWidget {
|
||||
/// limit: the number of channels to return (max is 30)
|
||||
/// offset: the offset (max is 1000)
|
||||
/// message_limit: how many messages should be included to each channel
|
||||
final PaginationParams pagination;
|
||||
@Deprecated(
|
||||
"'pagination' is deprecated and shouldn't be used. "
|
||||
"This property is no longer used, Please use 'limit' instead",
|
||||
)
|
||||
final PaginationParams? pagination;
|
||||
|
||||
/// The amount of channels requested per API call.
|
||||
final int limit;
|
||||
|
||||
/// Function called when tapping on a channel
|
||||
/// By default it calls [Navigator.push] building a [MaterialPageRoute]
|
||||
@@ -218,7 +229,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
presence: widget.presence,
|
||||
memberLimit: widget.memberLimit,
|
||||
messageLimit: widget.messageLimit,
|
||||
pagination: widget.pagination,
|
||||
limit: widget.limit,
|
||||
channelListController: _channelListController,
|
||||
listBuilder: widget.listBuilder ?? _buildListView,
|
||||
emptyBuilder: widget.emptyBuilder ?? _buildEmptyWidget,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// It shows the current [Channel] name using a [Text] widget.
|
||||
///
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Overlay for displaying commands that can be used
|
||||
class CommandsOverlay extends StatelessWidget {
|
||||
/// Constructor for creating a [CommandsOverlay]
|
||||
const CommandsOverlay({
|
||||
required this.text,
|
||||
required this.onCommandResult,
|
||||
required this.size,
|
||||
required this.channel,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The size of the overlay
|
||||
final Size size;
|
||||
|
||||
/// Query for searching commands
|
||||
final String text;
|
||||
|
||||
/// The channel to search for users
|
||||
final Channel channel;
|
||||
|
||||
/// Callback called when a command is selected
|
||||
final ValueChanged<Command> onCommandResult;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _streamChatTheme = StreamChatTheme.of(context);
|
||||
final commands = channel.config?.commands
|
||||
.where((c) => c.name.contains(text.replaceFirst('/', '')))
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
if (commands.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Container(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(0),
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
if (commands.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
child: StreamSvgIcon.lightning(
|
||||
color: _streamChatTheme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
context.translations.instantCommandsLabel,
|
||||
style: TextStyle(
|
||||
color: _streamChatTheme.colorTheme.textHighEmphasis
|
||||
.withOpacity(.5),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
...commands
|
||||
.map(
|
||||
(c) => InkWell(
|
||||
onTap: () {
|
||||
onCommandResult(c);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
_buildCommandIcon(_streamChatTheme, c.name),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: c.name.capitalize(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: ' /${c.name} ${c.args}',
|
||||
style: _streamChatTheme.textTheme.body
|
||||
.copyWith(
|
||||
// ignore: lines_longer_than_80_chars
|
||||
color: _streamChatTheme
|
||||
// ignore: lines_longer_than_80_chars
|
||||
.colorTheme
|
||||
.textLowEmphasis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCommandIcon(
|
||||
StreamChatThemeData _streamChatTheme, String iconType) {
|
||||
switch (iconType) {
|
||||
case 'giphy':
|
||||
return CircleAvatar(
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.giphyIcon(
|
||||
size: 24,
|
||||
),
|
||||
);
|
||||
case 'ban':
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.iconUserDelete(
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
case 'flag':
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.flag(
|
||||
size: 14,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
case 'imgur':
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: ClipOval(
|
||||
child: StreamSvgIcon.imgur(
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
);
|
||||
case 'mute':
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.mute(
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
case 'unban':
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.userAdd(
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
case 'unmute':
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.volumeUp(
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
default:
|
||||
return CircleAvatar(
|
||||
backgroundColor: _streamChatTheme.colorTheme.accentPrimary,
|
||||
radius: 12,
|
||||
child: StreamSvgIcon.lightning(
|
||||
size: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
/// It shows a date divider depending on the date difference
|
||||
class DateDivider extends StatelessWidget {
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:collection/collection.dart'
|
||||
show IterableExtension, ListEquality;
|
||||
|
||||
/// All Groups
|
||||
enum EmojiGroup {
|
||||
@@ -114240,7 +114241,6 @@ final emojiRegex = RegExp(
|
||||
class Emoji {
|
||||
static const variationSelector16 = 65039;
|
||||
static const ZWJ = 8205;
|
||||
|
||||
final String? name;
|
||||
final String? char;
|
||||
final String? shortName;
|
||||
@@ -114252,14 +114252,39 @@ class Emoji {
|
||||
|
||||
/// Emoji class.
|
||||
/// [name] of emoji. [char] and character of emoji. [shortName] and a digest name of emoji, [emojiGroup] is emoji's group and [emojiSubgroup] is emoji's subgroup. [keywords] list of keywords for emoji. [modifiable] `true` if emoji has skin.
|
||||
Emoji(
|
||||
{this.name,
|
||||
this.char,
|
||||
this.shortName,
|
||||
this.emojiGroup,
|
||||
this.emojiSubgroup,
|
||||
this.keywords = const [],
|
||||
this.modifiable = false});
|
||||
Emoji({
|
||||
this.name,
|
||||
this.char,
|
||||
this.shortName,
|
||||
this.emojiGroup,
|
||||
this.emojiSubgroup,
|
||||
this.keywords = const [],
|
||||
this.modifiable = false,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Emoji &&
|
||||
runtimeType == other.runtimeType &&
|
||||
name == other.name &&
|
||||
char == other.char &&
|
||||
shortName == other.shortName &&
|
||||
emojiGroup == other.emojiGroup &&
|
||||
emojiSubgroup == other.emojiSubgroup &&
|
||||
const ListEquality().equals(keywords, other.keywords) &&
|
||||
modifiable == other.modifiable;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
name.hashCode,
|
||||
char.hashCode,
|
||||
shortName.hashCode,
|
||||
emojiGroup.hashCode,
|
||||
emojiSubgroup.hashCode,
|
||||
keywords.hashCode,
|
||||
modifiable.hashCode,
|
||||
);
|
||||
|
||||
/// Runes of Emoji Character
|
||||
List<int> get charRunes {
|
||||
@@ -114339,9 +114364,11 @@ class Emoji {
|
||||
return _emojis.firstWhereOrNull((Emoji emoji) => emoji.name == name);
|
||||
}
|
||||
|
||||
/// Returns Emoji by [name] as short name.
|
||||
static Emoji? byShortName(String name) {
|
||||
return _emojis.firstWhereOrNull((Emoji emoji) => emoji.char == name);
|
||||
/// Returns Emoji by [shortName] as short name.
|
||||
static Emoji? byShortName(String shortName) {
|
||||
return _emojis.firstWhereOrNull(
|
||||
(Emoji emoji) => emoji.shortName == shortName,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns list of Emojis in a same [group]
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:substring_highlight/substring_highlight.dart';
|
||||
|
||||
/// Overlay for displaying emoji that can be used
|
||||
class EmojiOverlay extends StatelessWidget {
|
||||
/// Constructor for creating a [EmojiOverlay]
|
||||
const EmojiOverlay({
|
||||
required this.query,
|
||||
required this.onEmojiResult,
|
||||
required this.size,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The size of the overlay
|
||||
final Size size;
|
||||
|
||||
/// Query for searching emoji
|
||||
final String query;
|
||||
|
||||
/// Callback called when an emoji is selected
|
||||
final ValueChanged<Emoji> onEmojiResult;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _streamChatTheme = StreamChatTheme.of(context);
|
||||
final _emojiNames =
|
||||
Emoji.all().where((it) => it.name != null).map((e) => e.name!);
|
||||
|
||||
final emojis = _emojiNames
|
||||
.where((e) => e.contains(query))
|
||||
.map(Emoji.byName)
|
||||
.where((e) => e != null);
|
||||
|
||||
if (emojis.isEmpty) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
elevation: 2,
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Container(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
spreadRadius: -8,
|
||||
blurRadius: 5,
|
||||
offset: Offset(0, -4),
|
||||
),
|
||||
],
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(0),
|
||||
shrinkWrap: true,
|
||||
itemCount: emojis.length + 1,
|
||||
itemBuilder: (context, i) {
|
||||
if (i == 0) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8, top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: StreamSvgIcon.smile(
|
||||
color: _streamChatTheme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
context.translations.emojiMatchingQueryText(
|
||||
query,
|
||||
),
|
||||
style: TextStyle(
|
||||
color: _streamChatTheme.colorTheme.textHighEmphasis
|
||||
.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final emoji = emojis.elementAt(i - 1)!;
|
||||
final themeData = Theme.of(context);
|
||||
return ListTile(
|
||||
title: SubstringHighlight(
|
||||
text:
|
||||
// ignore: lines_longer_than_80_chars
|
||||
"${emoji.char} ${emoji.name!.replaceAll('_', ' ')}",
|
||||
term: query,
|
||||
textStyleHighlight: themeData.textTheme.headline6!.copyWith(
|
||||
fontSize: 14.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textStyle: themeData.textTheme.headline6!.copyWith(
|
||||
fontSize: 14.5,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
onEmojiResult(emoji);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:characters/characters.dart';
|
||||
import 'package:diacritic/diacritic.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||
@@ -11,7 +12,7 @@ final _emojiChars = Emoji.chars();
|
||||
extension StringExtension on String {
|
||||
/// Returns the capitalized string
|
||||
String capitalize() =>
|
||||
'${this[0].toUpperCase()}${substring(1).toLowerCase()}';
|
||||
isNotEmpty ? '${this[0].toUpperCase()}${substring(1).toLowerCase()}' : '';
|
||||
|
||||
/// Returns whether the string contains only emoji's or not.
|
||||
///
|
||||
@@ -24,6 +25,12 @@ extension StringExtension on String {
|
||||
final characters = trim().characters;
|
||||
return characters.every(_emojiChars.contains);
|
||||
}
|
||||
|
||||
/// Removes accents and diacritics from the given String.
|
||||
String get diacriticsInsensitive => removeDiacritics(this);
|
||||
|
||||
/// Levenshtein distance between this and [t].
|
||||
int levenshteinDistance(String t) => levenshtein(this, t);
|
||||
}
|
||||
|
||||
/// List extension
|
||||
@@ -170,3 +177,49 @@ extension IconButtonX on IconButton {
|
||||
icon: icon ?? this.icon,
|
||||
);
|
||||
}
|
||||
|
||||
/// Extensions on List<User>
|
||||
extension UserListX on List<User> {
|
||||
/// It does an search on a list of [User] and returns users with
|
||||
/// `id` or `name` containing the [query].
|
||||
///
|
||||
/// Results are returned sorted by their edit distance from the
|
||||
/// searched string, distance is calculated using the [levenshtein] algorithm.
|
||||
List<User> search(String query) {
|
||||
String normalize(String input) => input.toLowerCase().diacriticsInsensitive;
|
||||
|
||||
final normalizedQuery = normalize(query);
|
||||
|
||||
final matchingUsers = <User, int>{}; // User:lDistance
|
||||
|
||||
for (final user in this) {
|
||||
final normalizedId = normalize(user.id);
|
||||
final normalizedUserName = normalize(user.name);
|
||||
final lDistance = normalizedUserName.levenshteinDistance(normalizedQuery);
|
||||
final containsId = normalizedId.contains(normalizedQuery);
|
||||
final containsName = normalizedUserName.contains(normalizedQuery);
|
||||
if (lDistance < 3 || containsId || containsName) {
|
||||
matchingUsers[user] = lDistance;
|
||||
}
|
||||
}
|
||||
|
||||
final entries = matchingUsers.entries.toList(growable: false)
|
||||
..sort((prev, curr) {
|
||||
bool containsQuery(User user) =>
|
||||
normalize(user.id).contains(normalizedQuery) ||
|
||||
normalize(user.name).contains(normalizedQuery);
|
||||
|
||||
final containsInPrev = containsQuery(prev.key);
|
||||
final containsInCurr = containsQuery(curr.key);
|
||||
|
||||
if (containsInPrev && !containsInCurr) {
|
||||
return -1;
|
||||
} else if (!containsInPrev && containsInCurr) {
|
||||
return 1;
|
||||
}
|
||||
return prev.value.compareTo(curr.value);
|
||||
});
|
||||
|
||||
return entries.map((e) => e.key).toList(growable: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/gallery_footer.dart';
|
||||
import 'package:stream_chat_flutter/src/gallery_header.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Return action for coming back from pages
|
||||
enum ReturnActionType {
|
||||
@@ -112,6 +112,8 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl;
|
||||
return PhotoView(
|
||||
loadingBuilder: (context, image) =>
|
||||
const Offstage(),
|
||||
imageProvider: (imageUrl == null &&
|
||||
attachment.localUri != null &&
|
||||
attachment.file?.bytes != null)
|
||||
|
||||
@@ -7,12 +7,12 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Footer widget for media display
|
||||
class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment_actions_modal.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
@@ -57,9 +58,13 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
|
||||
final theme = Theme.of(context);
|
||||
return AppBar(
|
||||
textTheme: Theme.of(context).textTheme,
|
||||
brightness: Theme.of(context).brightness,
|
||||
toolbarTextStyle: theme.textTheme.bodyText2,
|
||||
titleTextStyle: theme.textTheme.headline6,
|
||||
systemOverlayStyle: theme.brightness == Brightness.dark
|
||||
? SystemUiOverlayStyle.light
|
||||
: SystemUiOverlayStyle.dark,
|
||||
elevation: 1,
|
||||
leading: showBackButton
|
||||
? IconButton(
|
||||
|
||||
@@ -15,6 +15,7 @@ class ImageGroup extends StatelessWidget {
|
||||
required this.size,
|
||||
this.onReturnAction,
|
||||
this.onShowMessage,
|
||||
this.onAttachmentTap,
|
||||
}) : super(key: key);
|
||||
|
||||
/// List of attachments to show
|
||||
@@ -23,6 +24,9 @@ class ImageGroup extends StatelessWidget {
|
||||
/// Callback when attachment is returned to from other screens
|
||||
final ValueChanged<ReturnActionType>? onReturnAction;
|
||||
|
||||
/// Callback when attachment is tapped
|
||||
final void Function(Message message, Attachment attachment)? onAttachmentTap;
|
||||
|
||||
/// Message which images are attached to
|
||||
final Message message;
|
||||
|
||||
@@ -117,6 +121,10 @@ class ImageGroup extends StatelessWidget {
|
||||
BuildContext context,
|
||||
int index,
|
||||
) async {
|
||||
if (onAttachmentTap != null) {
|
||||
return onAttachmentTap!(message, images[index]);
|
||||
}
|
||||
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
final res = await Navigator.push(
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// This widget is used for showing user tiles for mentions
|
||||
/// Use [title], [subtitle], [leading], [trailing] for
|
||||
/// substituting widgets in respective positions
|
||||
@Deprecated('Use `UserMentionTile` instead. Will be removed in future release')
|
||||
class MentionTile extends StatelessWidget {
|
||||
/// Constructor for creating a [MentionTile] widget
|
||||
const MentionTile(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -169,6 +169,7 @@ class MessageListView extends StatefulWidget {
|
||||
this.messageListController,
|
||||
this.reverse = true,
|
||||
this.paginationLimit = 20,
|
||||
this.paginationLoadingIndicatorBuilder,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Function used to build a custom message widget
|
||||
@@ -284,6 +285,9 @@ class MessageListView extends StatefulWidget {
|
||||
/// Use [ChannelListController.paginateData] pagination.
|
||||
final MessageListController? messageListController;
|
||||
|
||||
/// Builder used to build the loading indicator shown while paginating.
|
||||
final WidgetBuilder? paginationLoadingIndicatorBuilder;
|
||||
|
||||
@override
|
||||
_MessageListViewState createState() => _MessageListViewState();
|
||||
}
|
||||
@@ -293,7 +297,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
void Function(Message)? _onThreadTap;
|
||||
bool _showScrollToBottom = false;
|
||||
late final ItemPositionsListener _itemPositionListener;
|
||||
late final Stream<Iterable<ItemPosition>> _itemPositionStream;
|
||||
int? _messageListLength;
|
||||
StreamChannelState? streamChannel;
|
||||
late StreamChatThemeData _streamTheme;
|
||||
@@ -499,14 +502,18 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
return _buildThreadSeparator();
|
||||
}
|
||||
if (i == itemCount - 3) {
|
||||
if (widget.headerBuilder == null) {
|
||||
if (widget.reverse
|
||||
? widget.headerBuilder == null
|
||||
: widget.footerBuilder == null) {
|
||||
if (_isThreadConversation) return const Offstage();
|
||||
return const SizedBox(height: 52);
|
||||
}
|
||||
return const SizedBox(height: 8);
|
||||
}
|
||||
if (i == 0) {
|
||||
if (widget.footerBuilder == null) {
|
||||
if (widget.reverse
|
||||
? widget.footerBuilder == null
|
||||
: widget.headerBuilder == null) {
|
||||
return const SizedBox(height: 30);
|
||||
}
|
||||
return const SizedBox(height: 8);
|
||||
@@ -530,13 +537,13 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
? widget.dateDividerBuilder!(
|
||||
nextMessage.createdAt.toLocal(),
|
||||
)
|
||||
: DateDivider(
|
||||
dateTime: nextMessage.createdAt.toLocal(),
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: DateDivider(
|
||||
dateTime: nextMessage.createdAt.toLocal(),
|
||||
),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: divider,
|
||||
);
|
||||
return divider;
|
||||
}
|
||||
final timeDiff =
|
||||
Jiffy(nextMessage.createdAt.toLocal()).diff(
|
||||
@@ -565,27 +572,42 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
}
|
||||
|
||||
if (i == itemCount - 2) {
|
||||
return widget.headerBuilder?.call(context) ??
|
||||
const Offstage();
|
||||
if (widget.reverse) {
|
||||
return widget.headerBuilder?.call(context) ??
|
||||
const Offstage();
|
||||
} else {
|
||||
return widget.footerBuilder?.call(context) ??
|
||||
const Offstage();
|
||||
}
|
||||
}
|
||||
|
||||
final indicatorBuilder =
|
||||
widget.paginationLoadingIndicatorBuilder;
|
||||
|
||||
if (i == itemCount - 3) {
|
||||
return _buildLoadingIndicator(
|
||||
return _loadingIndicator(
|
||||
streamChannel!,
|
||||
QueryDirection.top,
|
||||
indicatorBuilder: indicatorBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
if (i == 1) {
|
||||
return _buildLoadingIndicator(
|
||||
return _loadingIndicator(
|
||||
streamChannel!,
|
||||
QueryDirection.bottom,
|
||||
indicatorBuilder: indicatorBuilder,
|
||||
);
|
||||
}
|
||||
|
||||
if (i == 0) {
|
||||
return widget.footerBuilder?.call(context) ??
|
||||
const Offstage();
|
||||
if (widget.reverse) {
|
||||
return widget.footerBuilder?.call(context) ??
|
||||
const Offstage();
|
||||
} else {
|
||||
return widget.headerBuilder?.call(context) ??
|
||||
const Offstage();
|
||||
}
|
||||
}
|
||||
|
||||
const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer
|
||||
@@ -657,7 +679,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
right: 0,
|
||||
child: BetterStreamBuilder<Iterable<ItemPosition>>(
|
||||
initialData: _itemPositionListener.itemPositions.value,
|
||||
stream: _itemPositionStream,
|
||||
stream: _valueListenableToStreamAdapter(
|
||||
_itemPositionListener.itemPositions),
|
||||
comparator: (a, b) {
|
||||
if (a == null || b == null) {
|
||||
return false;
|
||||
@@ -808,15 +831,17 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildLoadingIndicator(
|
||||
Widget _loadingIndicator(
|
||||
StreamChannelState streamChannel,
|
||||
QueryDirection direction,
|
||||
) =>
|
||||
QueryDirection direction, {
|
||||
WidgetBuilder? indicatorBuilder,
|
||||
}) =>
|
||||
_LoadingIndicator(
|
||||
direction: direction,
|
||||
streamTheme: _streamTheme,
|
||||
streamChannel: streamChannel,
|
||||
isThreadConversation: _isThreadConversation,
|
||||
indicatorBuilder: indicatorBuilder,
|
||||
);
|
||||
|
||||
Widget _buildBottomMessage(
|
||||
@@ -996,7 +1021,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
|
||||
|
||||
final hasUrlAttachment =
|
||||
message.attachments.any((it) => it.ogScrapeUrl != null) == true;
|
||||
message.attachments.any((it) => it.titleLink != null) == true;
|
||||
|
||||
final borderSide =
|
||||
isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment)
|
||||
@@ -1185,8 +1210,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
_scrollController = widget.scrollController ?? ItemScrollController();
|
||||
_itemPositionListener =
|
||||
widget.itemPositionListener ?? ItemPositionsListener.create();
|
||||
_itemPositionStream =
|
||||
_valueListenableToStreamAdapter(_itemPositionListener.itemPositions);
|
||||
|
||||
_getOnThreadTap();
|
||||
super.initState();
|
||||
@@ -1204,10 +1227,12 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
initialAlignment = _initialAlignment;
|
||||
|
||||
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
|
||||
_scrollController?.jumpTo(
|
||||
index: initialIndex,
|
||||
alignment: initialAlignment,
|
||||
);
|
||||
if (_scrollController?.isAttached == true) {
|
||||
_scrollController?.jumpTo(
|
||||
index: initialIndex,
|
||||
alignment: initialAlignment,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
_messageNewListener =
|
||||
@@ -1216,8 +1241,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
_bottomPaginationActive = false;
|
||||
_topPaginationActive = false;
|
||||
}
|
||||
if (event.message!.user!.id ==
|
||||
streamChannel!.channel.client.state.currentUser!.id) {
|
||||
if (event.message?.parentId == widget.parentMessage?.id &&
|
||||
event.message!.user!.id ==
|
||||
streamChannel!.channel.client.state.currentUser!.id) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) {
|
||||
_scrollController?.jumpTo(
|
||||
index: 0,
|
||||
@@ -1280,12 +1306,14 @@ class _LoadingIndicator extends StatelessWidget {
|
||||
required this.isThreadConversation,
|
||||
required this.direction,
|
||||
required this.streamChannel,
|
||||
this.indicatorBuilder,
|
||||
}) : super(key: key);
|
||||
|
||||
final StreamChatThemeData streamTheme;
|
||||
final bool isThreadConversation;
|
||||
final QueryDirection direction;
|
||||
final StreamChannelState streamChannel;
|
||||
final WidgetBuilder? indicatorBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -1304,12 +1332,13 @@ class _LoadingIndicator extends StatelessWidget {
|
||||
),
|
||||
builder: (context, data) {
|
||||
if (!data) return const Offstage();
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
return indicatorBuilder?.call(context) ??
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_picker.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat.dart';
|
||||
@@ -8,7 +9,6 @@ import 'package:stream_chat_flutter/src/theme/themes.dart';
|
||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Modal widget for displaying message reactions
|
||||
class MessageReactionsModal extends StatelessWidget {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// It shows the current [Message] preview.
|
||||
///
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/info_tile.dart';
|
||||
import 'package:stream_chat_flutter/src/message_search_item.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Callback called when tapping on a user
|
||||
typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
|
||||
@@ -30,13 +30,13 @@ typedef EmptyMessageSearchBuilder = Widget Function(
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Scaffold(
|
||||
/// body: MessageSearchListView(
|
||||
/// messageQuery: _channelQuery,
|
||||
/// filters: {
|
||||
/// 'members': {
|
||||
/// r'$in': [user.id]
|
||||
/// }
|
||||
/// },
|
||||
/// paginationParams: PaginationParams(limit: 20),
|
||||
/// messageQuery: _channelQuery,
|
||||
/// filters: {
|
||||
/// 'members': {
|
||||
/// r'$in': [user.id]
|
||||
/// }
|
||||
/// },
|
||||
/// limit: 20,
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
@@ -58,7 +58,7 @@ class MessageSearchListView extends StatefulWidget {
|
||||
required this.filters,
|
||||
this.messageQuery,
|
||||
this.sortOptions,
|
||||
this.paginationParams,
|
||||
this.limit = 30,
|
||||
this.messageFilters,
|
||||
this.separatorBuilder,
|
||||
this.itemBuilder,
|
||||
@@ -89,11 +89,8 @@ class MessageSearchListView extends StatefulWidget {
|
||||
/// Direction can be ascending or descending.
|
||||
final List<SortOption>? sortOptions;
|
||||
|
||||
/// Pagination parameters
|
||||
/// limit: the number of users to return (max is 30)
|
||||
/// offset: the offset (max is 1000)
|
||||
/// message_limit: how many messages should be included to each channel
|
||||
final PaginationParams? paginationParams;
|
||||
/// The amount of messages requested per API call.
|
||||
final int limit;
|
||||
|
||||
/// The message query filters to use.
|
||||
/// You can query on any of the custom fields you've defined on the [Channel].
|
||||
@@ -152,7 +149,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
filters: widget.filters,
|
||||
sortOptions: widget.sortOptions,
|
||||
messageQuery: widget.messageQuery,
|
||||
paginationParams: widget.paginationParams,
|
||||
limit: widget.limit,
|
||||
messageFilters: widget.messageFilters,
|
||||
messageSearchListController: _messageSearchListController,
|
||||
emptyBuilder: widget.emptyBuilder ??
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/url_attachment.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/image_group.dart';
|
||||
import 'package:stream_chat_flutter/src/message_action.dart';
|
||||
@@ -15,7 +16,6 @@ import 'package:stream_chat_flutter/src/message_reactions_modal.dart';
|
||||
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
||||
import 'package:stream_chat_flutter/src/url_attachment.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Widget builder for building attachments
|
||||
@@ -96,7 +96,7 @@ class MessageWidget extends StatefulWidget {
|
||||
this.bottomRowBuilder,
|
||||
this.deletedBottomRowBuilder,
|
||||
this.onReturnAction,
|
||||
Map<String, AttachmentBuilder>? customAttachmentBuilders,
|
||||
this.customAttachmentBuilders,
|
||||
this.readList,
|
||||
this.padding,
|
||||
this.textPadding = const EdgeInsets.symmetric(
|
||||
@@ -133,6 +133,7 @@ class MessageWidget extends StatefulWidget {
|
||||
messageTheme: messageTheme,
|
||||
onShowMessage: onShowMessage,
|
||||
onReturnAction: onReturnAction,
|
||||
onAttachmentTap: onAttachmentTap,
|
||||
),
|
||||
),
|
||||
border,
|
||||
@@ -214,6 +215,11 @@ class MessageWidget extends StatefulWidget {
|
||||
),
|
||||
onShowMessage: onShowMessage,
|
||||
onReturnAction: onReturnAction,
|
||||
onAttachmentTap: onAttachmentTap != null
|
||||
? () {
|
||||
onAttachmentTap(message, attachment);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
@@ -243,6 +249,11 @@ class MessageWidget extends StatefulWidget {
|
||||
mediaQueryData.size.width * 0.8,
|
||||
mediaQueryData.size.height * 0.3,
|
||||
),
|
||||
onAttachmentTap: onAttachmentTap != null
|
||||
? () {
|
||||
onAttachmentTap(message, attachment);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
border,
|
||||
reverse,
|
||||
@@ -395,6 +406,9 @@ class MessageWidget extends StatefulWidget {
|
||||
/// Builder for respective attachment types
|
||||
final Map<String, AttachmentBuilder> attachmentBuilders;
|
||||
|
||||
/// Builder for respective attachment types (user facing builder)
|
||||
final Map<String, AttachmentBuilder>? customAttachmentBuilders;
|
||||
|
||||
/// Center user avatar with bottom of the message
|
||||
final bool translateUserAvatar;
|
||||
|
||||
@@ -519,7 +533,7 @@ class MessageWidget extends StatefulWidget {
|
||||
showPinButton: showPinButton ?? this.showPinButton,
|
||||
showPinHighlight: showPinHighlight ?? this.showPinHighlight,
|
||||
customAttachmentBuilders:
|
||||
customAttachmentBuilders ?? attachmentBuilders,
|
||||
customAttachmentBuilders ?? this.customAttachmentBuilders,
|
||||
translateUserAvatar: translateUserAvatar ?? this.translateUserAvatar,
|
||||
onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap,
|
||||
onMessageTap: onMessageTap ?? this.onMessageTap,
|
||||
@@ -567,11 +581,11 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true;
|
||||
|
||||
bool get hasNonUrlAttachments => widget.message.attachments
|
||||
.where((it) => it.ogScrapeUrl == null)
|
||||
.where((it) => it.titleLink == null || it.type == 'giphy')
|
||||
.isNotEmpty;
|
||||
|
||||
bool get hasUrlAttachments =>
|
||||
widget.message.attachments.any((it) => it.ogScrapeUrl != null) == true;
|
||||
bool get hasUrlAttachments => widget.message.attachments
|
||||
.any((it) => it.titleLink != null && it.type != 'giphy');
|
||||
|
||||
bool get showBottomRow =>
|
||||
showThreadReplyIndicator ||
|
||||
@@ -984,9 +998,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
|
||||
Widget _buildUrlAttachment() {
|
||||
final urlAttachment = widget.message.attachments
|
||||
.firstWhere((element) => element.ogScrapeUrl != null);
|
||||
.firstWhere((element) => element.titleLink != null);
|
||||
|
||||
final host = Uri.parse(urlAttachment.ogScrapeUrl!).host;
|
||||
final host = Uri.parse(urlAttachment.titleLink!).host;
|
||||
final splitList = host.split('.');
|
||||
final hostName = splitList.length == 3 ? splitList[1] : splitList[0];
|
||||
final hostDisplayName = urlAttachment.authorName?.capitalize() ??
|
||||
@@ -997,6 +1011,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
urlAttachment: urlAttachment,
|
||||
hostDisplayName: hostDisplayName,
|
||||
textPadding: widget.textPadding,
|
||||
messageTheme: widget.messageTheme,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1155,7 +1170,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
final attachmentGroups = <String, List<Attachment>>{};
|
||||
|
||||
widget.message.attachments
|
||||
.where((element) => element.ogScrapeUrl == null && element.type != null)
|
||||
.where((element) =>
|
||||
(element.titleLink == null && element.type != null) ||
|
||||
element.type == 'giphy')
|
||||
.forEach((e) {
|
||||
if (attachmentGroups[e.type] == null) {
|
||||
attachmentGroups[e.type!] = [];
|
||||
@@ -1335,7 +1352,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
}
|
||||
|
||||
if (hasUrlAttachments) {
|
||||
return _streamChatTheme.colorTheme.linkBg;
|
||||
return widget.messageTheme.linkBackgroundColor;
|
||||
}
|
||||
|
||||
if (isOnlyEmoji) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
|
||||
/// Class that contains the parameters for building an overlay entry
|
||||
class OverlayOptions {
|
||||
/// Constructs a new overlay options object
|
||||
/// [visible] - the visibility of the overlay
|
||||
/// [widget] - the widget to be displayed
|
||||
OverlayOptions({
|
||||
required this.visible,
|
||||
required this.widget,
|
||||
});
|
||||
|
||||
/// the visibility of the overlay
|
||||
final bool visible;
|
||||
|
||||
/// the widget to be displayed
|
||||
final Widget widget;
|
||||
}
|
||||
|
||||
/// Widget that renders a single overlay widget from a list of [overlayOptions]
|
||||
/// It shows the first one that is visible
|
||||
class MultiOverlay extends StatelessWidget {
|
||||
/// Constructs a new MultiOverlay widget
|
||||
/// [overlayOptions] - the list of overlay options
|
||||
/// [overlayAnchor] - the anchor relative to the overlay
|
||||
/// [childAnchor] - the anchor relative to the child
|
||||
/// [child] - the child widget
|
||||
const MultiOverlay({
|
||||
Key? key,
|
||||
required this.overlayOptions,
|
||||
required this.child,
|
||||
required this.overlayAnchor,
|
||||
required this.childAnchor,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The list of overlay options
|
||||
final List<OverlayOptions> overlayOptions;
|
||||
|
||||
/// The child widget
|
||||
final Widget child;
|
||||
|
||||
/// The anchor relative to the overlay
|
||||
final Alignment? overlayAnchor;
|
||||
|
||||
/// The anchor relative to the child
|
||||
final Alignment? childAnchor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final visibleOverlay =
|
||||
overlayOptions.firstWhereOrNull((element) => element.visible);
|
||||
|
||||
return PortalEntry(
|
||||
childAnchor: childAnchor,
|
||||
portalAnchor: overlayAnchor,
|
||||
visible: visibleOverlay != null,
|
||||
portal: visibleOverlay?.widget,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -97,8 +97,8 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
|
||||
bool get _hasAttachments => message.attachments.isNotEmpty == true;
|
||||
|
||||
bool get _containsScrapeUrl =>
|
||||
message.attachments.any((element) => element.ogScrapeUrl != null) == true;
|
||||
bool get _containsLinkAttachment =>
|
||||
message.attachments.any((element) => element.titleLink != null) == true;
|
||||
|
||||
bool get _containsText => message.text?.isNotEmpty == true;
|
||||
|
||||
@@ -198,9 +198,9 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
Widget _parseAttachments(BuildContext context) {
|
||||
Widget child;
|
||||
Attachment attachment;
|
||||
if (_containsScrapeUrl) {
|
||||
if (_containsLinkAttachment) {
|
||||
attachment = message.attachments.firstWhere(
|
||||
(element) => element.ogScrapeUrl != null,
|
||||
(element) => element.titleLink != null,
|
||||
);
|
||||
child = _buildUrlAttachment(attachment);
|
||||
} else {
|
||||
@@ -280,8 +280,8 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
};
|
||||
|
||||
Color? _getBackgroundColor(BuildContext context) {
|
||||
if (_containsScrapeUrl) {
|
||||
return StreamChatTheme.of(context).colorTheme.linkBg;
|
||||
if (_containsLinkAttachment) {
|
||||
return messageTheme.linkBackgroundColor;
|
||||
}
|
||||
return messageTheme.messageBackgroundColor;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,9 @@ class StreamChatState extends State<StreamChat> {
|
||||
return Theme(
|
||||
data: materialTheme.copyWith(
|
||||
primaryIconTheme: streamTheme.primaryIconTheme,
|
||||
accentColor: streamTheme.colorTheme.accentPrimary,
|
||||
colorScheme: materialTheme.colorScheme.copyWith(
|
||||
secondary: streamTheme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
child: StreamChatCore(
|
||||
client: client,
|
||||
|
||||
@@ -131,7 +131,7 @@ class StreamChatThemeData {
|
||||
final defaultTheme = StreamChatThemeData(brightness: theme.brightness);
|
||||
final customizedTheme = StreamChatThemeData.fromColorAndTextTheme(
|
||||
defaultTheme.colorTheme.copyWith(
|
||||
accentPrimary: theme.accentColor,
|
||||
accentPrimary: theme.colorScheme.secondary,
|
||||
),
|
||||
defaultTheme.textTheme,
|
||||
);
|
||||
@@ -223,6 +223,7 @@ class StreamChatThemeData {
|
||||
messageLinksStyle: TextStyle(
|
||||
color: accentColor,
|
||||
),
|
||||
linkBackgroundColor: colorTheme.linkBg,
|
||||
),
|
||||
otherMessageTheme: MessageThemeData(
|
||||
reactionsBackgroundColor: colorTheme.disabled,
|
||||
@@ -246,6 +247,7 @@ class StreamChatThemeData {
|
||||
width: 32,
|
||||
),
|
||||
),
|
||||
linkBackgroundColor: colorTheme.linkBg,
|
||||
),
|
||||
messageInputTheme: MessageInputThemeData(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
|
||||
@@ -17,6 +17,7 @@ class MessageThemeData with Diagnosticable {
|
||||
this.reactionsMaskColor,
|
||||
this.avatarTheme,
|
||||
this.createdAtStyle,
|
||||
this.linkBackgroundColor,
|
||||
});
|
||||
|
||||
/// Text style for message text
|
||||
@@ -52,6 +53,9 @@ class MessageThemeData with Diagnosticable {
|
||||
/// Theme of the avatar
|
||||
final AvatarThemeData? avatarTheme;
|
||||
|
||||
/// Background color for messages with url attachments.
|
||||
final Color? linkBackgroundColor;
|
||||
|
||||
/// Copy with a theme
|
||||
MessageThemeData copyWith({
|
||||
TextStyle? messageTextStyle,
|
||||
@@ -65,6 +69,7 @@ class MessageThemeData with Diagnosticable {
|
||||
Color? reactionsBackgroundColor,
|
||||
Color? reactionsBorderColor,
|
||||
Color? reactionsMaskColor,
|
||||
Color? linkBackgroundColor,
|
||||
}) =>
|
||||
MessageThemeData(
|
||||
messageTextStyle: messageTextStyle ?? this.messageTextStyle,
|
||||
@@ -80,6 +85,7 @@ class MessageThemeData with Diagnosticable {
|
||||
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
|
||||
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
|
||||
reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor,
|
||||
linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor,
|
||||
);
|
||||
|
||||
/// Linearly interpolate from one [MessageThemeData] to another.
|
||||
@@ -105,6 +111,8 @@ class MessageThemeData with Diagnosticable {
|
||||
reactionsMaskColor:
|
||||
Color.lerp(a.reactionsMaskColor, b.reactionsMaskColor, t),
|
||||
repliesStyle: TextStyle.lerp(a.repliesStyle, b.repliesStyle, t),
|
||||
linkBackgroundColor:
|
||||
Color.lerp(a.linkBackgroundColor, b.linkBackgroundColor, t),
|
||||
);
|
||||
|
||||
/// Merge with a theme
|
||||
@@ -127,6 +135,7 @@ class MessageThemeData with Diagnosticable {
|
||||
reactionsBackgroundColor: other.reactionsBackgroundColor,
|
||||
reactionsBorderColor: other.reactionsBorderColor,
|
||||
reactionsMaskColor: other.reactionsMaskColor,
|
||||
linkBackgroundColor: other.linkBackgroundColor,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -145,7 +154,8 @@ class MessageThemeData with Diagnosticable {
|
||||
reactionsBackgroundColor == other.reactionsBackgroundColor &&
|
||||
reactionsBorderColor == other.reactionsBorderColor &&
|
||||
reactionsMaskColor == other.reactionsMaskColor &&
|
||||
avatarTheme == other.avatarTheme;
|
||||
avatarTheme == other.avatarTheme &&
|
||||
linkBackgroundColor == other.linkBackgroundColor;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -159,7 +169,8 @@ class MessageThemeData with Diagnosticable {
|
||||
reactionsBackgroundColor.hashCode ^
|
||||
reactionsBorderColor.hashCode ^
|
||||
reactionsMaskColor.hashCode ^
|
||||
avatarTheme.hashCode;
|
||||
avatarTheme.hashCode ^
|
||||
linkBackgroundColor.hashCode;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
@@ -175,6 +186,7 @@ class MessageThemeData with Diagnosticable {
|
||||
..add(DiagnosticsProperty('avatarTheme', avatarTheme))
|
||||
..add(ColorProperty('reactionsBackgroundColor', reactionsBackgroundColor))
|
||||
..add(ColorProperty('reactionsBorderColor', reactionsBorderColor))
|
||||
..add(ColorProperty('reactionsMaskColor', reactionsMaskColor));
|
||||
..add(ColorProperty('reactionsMaskColor', reactionsMaskColor))
|
||||
..add(ColorProperty('linkBackgroundColor', linkBackgroundColor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
@@ -127,10 +128,14 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
],
|
||||
);
|
||||
|
||||
final theme = Theme.of(context);
|
||||
return AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
textTheme: Theme.of(context).textTheme,
|
||||
brightness: Theme.of(context).brightness,
|
||||
toolbarTextStyle: theme.textTheme.bodyText2,
|
||||
titleTextStyle: theme.textTheme.headline6,
|
||||
systemOverlayStyle: theme.brightness == Brightness.dark
|
||||
? SystemUiOverlayStyle.light
|
||||
: SystemUiOverlayStyle.dark,
|
||||
elevation: 1,
|
||||
leading: leading ??
|
||||
(showBackButton
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// Widget to show the current list of typing users
|
||||
class TypingIndicator extends StatelessWidget {
|
||||
|
||||
@@ -66,6 +66,12 @@ class UserAvatar extends StatelessWidget {
|
||||
final placeholder =
|
||||
this.placeholder ?? streamChatTheme.placeholderUserImage;
|
||||
|
||||
final backupGradientAvatar = ClipRRect(
|
||||
borderRadius: borderRadius ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius,
|
||||
child: streamChatTheme.defaultUserImage(context, user),
|
||||
);
|
||||
|
||||
Widget avatar = FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: Container(
|
||||
@@ -76,8 +82,7 @@ class UserAvatar extends StatelessWidget {
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.high,
|
||||
imageUrl: user.image!,
|
||||
errorWidget: (context, __, ___) =>
|
||||
streamChatTheme.defaultUserImage(context, user),
|
||||
errorWidget: (context, __, ___) => backupGradientAvatar,
|
||||
placeholder: placeholder != null
|
||||
? (context, __) => placeholder(context, user)
|
||||
: null,
|
||||
@@ -91,11 +96,7 @@ class UserAvatar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
)
|
||||
: ClipRRect(
|
||||
borderRadius: borderRadius ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius,
|
||||
child: streamChatTheme.defaultUserImage(context, user),
|
||||
),
|
||||
: backupGradientAvatar,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/user_list_view.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
///
|
||||
/// It shows the current [User] preview.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Callback called when tapping on a user
|
||||
typedef UserTapCallback = void Function(User, Widget?);
|
||||
@@ -46,12 +46,17 @@ typedef UserItemBuilder = Widget Function(BuildContext, User, bool);
|
||||
/// Modify it to change the widget appearance.
|
||||
class UserListView extends StatefulWidget {
|
||||
/// Instantiate a new UserListView
|
||||
const UserListView({
|
||||
UserListView({
|
||||
Key? key,
|
||||
this.filter,
|
||||
this.filter = const Filter.empty(),
|
||||
this.sort,
|
||||
this.presence,
|
||||
this.pagination,
|
||||
@Deprecated(
|
||||
"'pagination' is deprecated and shouldn't be used. "
|
||||
"This property is no longer used, Please use 'limit' instead",
|
||||
)
|
||||
this.pagination,
|
||||
int? limit,
|
||||
this.onUserTap,
|
||||
this.onUserLongPress,
|
||||
this.userWidget,
|
||||
@@ -71,12 +76,13 @@ class UserListView extends StatefulWidget {
|
||||
crossAxisCount == 1 || groupAlphabetically == false,
|
||||
'Cannot group alphabetically when crossAxisCount > 1',
|
||||
),
|
||||
limit = limit ?? pagination?.limit ?? 30,
|
||||
super(key: key);
|
||||
|
||||
/// The query filters to use.
|
||||
/// You can query on any of the custom fields you've defined on the [Channel].
|
||||
/// You can also filter other built-in channel fields.
|
||||
final Filter? filter;
|
||||
final Filter filter;
|
||||
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can
|
||||
@@ -93,8 +99,15 @@ class UserListView extends StatefulWidget {
|
||||
/// limit: the number of users to return (max is 30)
|
||||
/// offset: the offset (max is 1000)
|
||||
/// message_limit: how many messages should be included to each channel
|
||||
@Deprecated(
|
||||
"'pagination' is deprecated and shouldn't be used. "
|
||||
"This property is no longer used, Please use 'limit' instead",
|
||||
)
|
||||
final PaginationParams? pagination;
|
||||
|
||||
/// The amount of users requested per API call.
|
||||
final int limit;
|
||||
|
||||
/// Function called when tapping on a channel
|
||||
/// By default it calls [Navigator.push] building a [MaterialPageRoute]
|
||||
/// with the widget [userWidget] as child.
|
||||
@@ -184,7 +197,7 @@ class _UserListViewState extends State<UserListView>
|
||||
),
|
||||
listBuilder:
|
||||
widget.listBuilder ?? (context, list) => _buildListView(list),
|
||||
pagination: widget.pagination,
|
||||
limit: widget.limit,
|
||||
sort: widget.sort,
|
||||
filter: widget.filter,
|
||||
presence: widget.presence,
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// This widget is used for showing user tiles for mentions
|
||||
/// Use [title], [subtitle], [leading], [trailing] for
|
||||
/// substituting widgets in respective positions
|
||||
class UserMentionTile extends StatelessWidget {
|
||||
/// Constructor for creating a [UserMentionTile] widget
|
||||
const UserMentionTile(
|
||||
this.user, {
|
||||
Key? key,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
}) : super(key: key);
|
||||
|
||||
/// User to display in the tile
|
||||
final User user;
|
||||
|
||||
/// Widget to display as title
|
||||
final Widget? title;
|
||||
|
||||
/// Widget to display below [title]
|
||||
final Widget? subtitle;
|
||||
|
||||
/// Widget at the start of the tile
|
||||
final Widget? leading;
|
||||
|
||||
/// Widget at the end of tile
|
||||
final Widget? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
return SizedBox(
|
||||
height: 56,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
leading ??
|
||||
UserAvatar(
|
||||
user: user,
|
||||
constraints: BoxConstraints.tight(const Size(40, 40)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
title ??
|
||||
Text(
|
||||
user.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: chatThemeData.textTheme.bodyBold,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
subtitle ??
|
||||
Text(
|
||||
'@${user.id}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: chatThemeData.textTheme.footnoteBold.copyWith(
|
||||
color: chatThemeData.colorTheme.textLowEmphasis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing ??
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 18,
|
||||
left: 8,
|
||||
),
|
||||
child: StreamSvgIcon.mentions(
|
||||
color: chatThemeData.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/user_mention_tile.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// Builder function for building a mention tile.
|
||||
///
|
||||
/// Use [UserMentionTile] for the default implementation.
|
||||
typedef MentionTileBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
User user,
|
||||
);
|
||||
|
||||
/// Overlay for displaying users that can be mentioned.
|
||||
class UserMentionsOverlay extends StatefulWidget {
|
||||
/// Constructor for creating a [UserMentionsOverlay].
|
||||
UserMentionsOverlay({
|
||||
Key? key,
|
||||
required this.query,
|
||||
required this.channel,
|
||||
required this.size,
|
||||
this.client,
|
||||
this.limit = 10,
|
||||
this.mentionAllAppUsers = false,
|
||||
this.mentionsTileBuilder,
|
||||
this.onMentionUserTap,
|
||||
}) : assert(
|
||||
channel.state != null,
|
||||
'Channel ${channel.cid} is not yet initialized',
|
||||
),
|
||||
assert(
|
||||
!mentionAllAppUsers || (mentionAllAppUsers && client != null),
|
||||
'StreamChatClient is required in order to use mentionAllAppUsers',
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
/// Query for searching users.
|
||||
final String query;
|
||||
|
||||
/// Limit applied on user search results.
|
||||
final int limit;
|
||||
|
||||
/// The size of the overlay.
|
||||
final Size size;
|
||||
|
||||
/// The channel to search for users.
|
||||
final Channel channel;
|
||||
|
||||
/// The client to search for users in case [mentionAllAppUsers] is True.
|
||||
final StreamChatClient? client;
|
||||
|
||||
/// When enabled mentions search users across the entire app.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool mentionAllAppUsers;
|
||||
|
||||
/// Customize the tile for the mentions overlay.
|
||||
final MentionTileBuilder? mentionsTileBuilder;
|
||||
|
||||
/// Callback called when a user is selected.
|
||||
final void Function(User user)? onMentionUserTap;
|
||||
|
||||
@override
|
||||
_UserMentionsOverlayState createState() => _UserMentionsOverlayState();
|
||||
}
|
||||
|
||||
class _UserMentionsOverlayState extends State<UserMentionsOverlay> {
|
||||
late Future<List<User>> userMentionsFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
userMentionsFuture = queryMentions(widget.query);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant UserMentionsOverlay oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.channel != oldWidget.channel ||
|
||||
widget.query != oldWidget.query ||
|
||||
widget.mentionAllAppUsers != oldWidget.mentionAllAppUsers ||
|
||||
widget.limit != oldWidget.limit) {
|
||||
userMentionsFuture = queryMentions(widget.query);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8),
|
||||
elevation: 2,
|
||||
color: theme.colorTheme.barsBg,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Container(
|
||||
constraints: BoxConstraints.loose(widget.size),
|
||||
decoration: BoxDecoration(color: theme.colorTheme.barsBg),
|
||||
child: FutureBuilder<List<User>>(
|
||||
future: userMentionsFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) return const Offstage();
|
||||
if (!snapshot.hasData) return const Offstage();
|
||||
final users = snapshot.data!;
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.all(0),
|
||||
shrinkWrap: true,
|
||||
itemCount: users.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = users[index];
|
||||
return Material(
|
||||
color: theme.colorTheme.barsBg,
|
||||
child: InkWell(
|
||||
onTap: () => widget.onMentionUserTap?.call(user),
|
||||
child: widget.mentionsTileBuilder?.call(context, user) ??
|
||||
UserMentionTile(user),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<User> get membersAndWatchers {
|
||||
final state = widget.channel.state!;
|
||||
return {
|
||||
...state.watchers,
|
||||
...state.members.map((it) => it.user),
|
||||
}.whereType<User>().toList(growable: false);
|
||||
}
|
||||
|
||||
Future<List<User>> queryMentions(String query) async {
|
||||
if (widget.mentionAllAppUsers) {
|
||||
return _queryUsers(query);
|
||||
}
|
||||
|
||||
var channelState = widget.channel.state;
|
||||
|
||||
channelState = channelState!;
|
||||
final members = channelState.members;
|
||||
|
||||
// By default, we return maximum 100 members via queryChannels api call.
|
||||
// Thus it is safe to assume, that if number of members in channel.state
|
||||
// is < 100, then all the members are already available on client side
|
||||
// and we don't need to make any api call to queryMembers endpoint.
|
||||
if (members.length < 100) {
|
||||
final matchingUsers = membersAndWatchers.search(query);
|
||||
return matchingUsers.toList(growable: false);
|
||||
}
|
||||
|
||||
final result = await _queryMembers(query);
|
||||
return result
|
||||
.map((it) => it.user)
|
||||
.whereType<User>()
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
Future<List<Member>> _queryMembers(String query) async {
|
||||
final response = await widget.channel.queryMembers(
|
||||
pagination: PaginationParams(limit: widget.limit),
|
||||
filter: query.isEmpty
|
||||
? const Filter.empty()
|
||||
: Filter.autoComplete('name', query),
|
||||
);
|
||||
return response.members;
|
||||
}
|
||||
|
||||
Future<List<User>> _queryUsers(String query) async {
|
||||
assert(
|
||||
widget.client != null,
|
||||
'StreamChatClient is required in order to query all app users',
|
||||
);
|
||||
final response = await widget.client!.queryUsers(
|
||||
pagination: PaginationParams(limit: widget.limit),
|
||||
filter: query.isEmpty
|
||||
? const Filter.empty()
|
||||
: Filter.or([
|
||||
Filter.autoComplete('id', query),
|
||||
Filter.autoComplete('name', query),
|
||||
]),
|
||||
sort: [const SortOption('id', direction: SortOption.ASC)],
|
||||
);
|
||||
return response.users;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Launch URL
|
||||
Future<void> launchURL(BuildContext context, String? url) async {
|
||||
if (url != null && await canLaunch(url)) {
|
||||
Future<void> launchURL(BuildContext context, String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -388,3 +389,39 @@ class Tuple2<T1, T2> {
|
||||
@override
|
||||
int get hashCode => item1.hashCode ^ item2.hashCode;
|
||||
}
|
||||
|
||||
/// Levenshtein algorithm implementation based on:
|
||||
/// http://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows
|
||||
int levenshtein(String s, String t, {bool caseSensitive = true}) {
|
||||
if (!caseSensitive) {
|
||||
// ignore: parameter_assignments
|
||||
s = s.toLowerCase();
|
||||
// ignore: parameter_assignments
|
||||
t = t.toLowerCase();
|
||||
}
|
||||
if (s == t) return 0;
|
||||
if (s.isEmpty) return t.length;
|
||||
if (t.isEmpty) return s.length;
|
||||
|
||||
final v0 = List<int>.filled(t.length + 1, 0);
|
||||
final v1 = List<int>.filled(t.length + 1, 0);
|
||||
|
||||
for (var i = 0; i < t.length + 1; i < i++) {
|
||||
v0[i] = i;
|
||||
}
|
||||
|
||||
for (var i = 0; i < s.length; i++) {
|
||||
v1[0] = i + 1;
|
||||
|
||||
for (var j = 0; j < t.length; j++) {
|
||||
final cost = (s[i] == t[j]) ? 0 : 1;
|
||||
v1[j + 1] = math.min(v1[j] + 1, math.min(v0[j + 1] + 1, v0[j] + cost));
|
||||
}
|
||||
|
||||
for (var j = 0; j < t.length + 1; j++) {
|
||||
v0[j] = v1[j];
|
||||
}
|
||||
}
|
||||
|
||||
return v1[t.length];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
|
||||
/// Widget for displaying a footnote
|
||||
class VisibleFootnote extends StatelessWidget {
|
||||
|
||||
@@ -44,5 +44,6 @@ export 'src/unread_indicator.dart';
|
||||
export 'src/user_avatar.dart';
|
||||
export 'src/user_item.dart';
|
||||
export 'src/user_list_view.dart';
|
||||
export 'src/user_mention_tile.dart';
|
||||
export 'src/utils.dart';
|
||||
export 'src/visible_footnote.dart';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
name: stream_chat_flutter
|
||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
|
||||
version: 2.2.1
|
||||
version: 3.1.1
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
environment:
|
||||
sdk: '>=2.12.0 <3.0.0'
|
||||
sdk: '>=2.14.0 <3.0.0'
|
||||
flutter: ">=1.17.0"
|
||||
|
||||
dependencies:
|
||||
@@ -14,18 +14,18 @@ dependencies:
|
||||
characters: ^1.1.0
|
||||
chewie: ^1.2.0
|
||||
collection: ^1.15.0
|
||||
diacritic: ^0.1.3
|
||||
dio: ^4.0.0
|
||||
ezanimation: ^0.5.0
|
||||
file_picker: ^3.0.1
|
||||
file_picker: ^4.1.3
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_keyboard_visibility: ^5.0.1
|
||||
flutter_markdown: ^0.6.1
|
||||
flutter_portal: ^0.4.0
|
||||
flutter_slidable: ^0.6.0
|
||||
flutter_svg: ^0.22.0
|
||||
http_parser: ^4.0.0
|
||||
image_gallery_saver: ^1.6.9
|
||||
image_gallery_saver: ^1.7.0
|
||||
image_picker: ^0.8.2
|
||||
jiffy: ^4.1.0
|
||||
lottie: ^1.0.1
|
||||
@@ -41,13 +41,13 @@ dependencies:
|
||||
path: packages/scrollable_positioned_list
|
||||
share_plus: ^2.0.3
|
||||
shimmer: ^2.0.0
|
||||
stream_chat_flutter_core: ^2.2.1
|
||||
stream_chat_flutter_core: ^3.1.1
|
||||
substring_highlight: ^1.0.26
|
||||
synchronized: ^3.0.0
|
||||
url_launcher: ^6.0.3
|
||||
video_compress: ^3.0.0
|
||||
video_player: ^2.1.0
|
||||
video_thumbnail: ^0.3.3
|
||||
video_thumbnail: ^0.4.3
|
||||
visibility_detector: ^0.2.0
|
||||
|
||||
flutter:
|
||||
@@ -61,7 +61,7 @@ flutter:
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
golden_toolkit: ^0.9.0
|
||||
golden_toolkit: ^0.10.0
|
||||
mocktail: ^0.1.2
|
||||
path: ^1.8.0
|
||||
pedantic: ^1.11.0
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||
|
||||
void main() {
|
||||
group('src/emoji', () {
|
||||
test('${Emoji.byShortName} should bring the correct emoji', () {
|
||||
final resultEmoji = Emoji.byShortName('smiley');
|
||||
expect(resultEmoji, _mockEmoji);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
final _mockEmoji = Emoji(
|
||||
name: 'grinning face with big eyes',
|
||||
char: '\u{1F603}',
|
||||
shortName: 'smiley',
|
||||
emojiGroup: EmojiGroup.smileysEmotion,
|
||||
emojiSubgroup: EmojiSubgroup.faceSmiling,
|
||||
keywords: [
|
||||
'face',
|
||||
'mouth',
|
||||
'open',
|
||||
'smile',
|
||||
'uc6',
|
||||
'smiley',
|
||||
'happy',
|
||||
'silly',
|
||||
'laugh',
|
||||
'good',
|
||||
'smile',
|
||||
'teeth',
|
||||
'fun',
|
||||
'smileys',
|
||||
'mood',
|
||||
'emotion',
|
||||
'emotions',
|
||||
'emotional',
|
||||
'hooray',
|
||||
'cheek',
|
||||
'cheeky',
|
||||
'excited',
|
||||
'feliz',
|
||||
'heureux',
|
||||
'cheerful',
|
||||
'delighted',
|
||||
'ecstatic',
|
||||
'elated',
|
||||
'glad',
|
||||
'joy',
|
||||
'merry',
|
||||
'funny',
|
||||
'laughing',
|
||||
'lol',
|
||||
'rofl',
|
||||
'lmao',
|
||||
'lmfao',
|
||||
'hilarious',
|
||||
'ha',
|
||||
'haha',
|
||||
'chuckle',
|
||||
'comedy',
|
||||
'giggle',
|
||||
'hehe',
|
||||
'joyful',
|
||||
'laugh out loud',
|
||||
'rire',
|
||||
'tee hee',
|
||||
'jaja',
|
||||
'good job',
|
||||
'nice',
|
||||
'well done',
|
||||
'bravo',
|
||||
'congratulations',
|
||||
'congrats',
|
||||
'smiles',
|
||||
'dentist',
|
||||
':-D',
|
||||
'=D'
|
||||
]);
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
void main() {
|
||||
group('List<User>.search', () {
|
||||
test('should work fine', () {
|
||||
final tommaso = User(id: 'tommaso', name: 'Tommaso');
|
||||
final thierry = User(id: 'thierry', name: 'Thierry');
|
||||
final users = [tommaso, thierry];
|
||||
|
||||
expect(users.search('Tom'), [tommaso]);
|
||||
expect(users.search('Thier'), [thierry]);
|
||||
});
|
||||
|
||||
test('should search using UpperCased', () {
|
||||
final tommaso = User(id: 'tommaso', name: 'Tommaso');
|
||||
final thierry = User(id: 'thierry', name: 'Thierry');
|
||||
final users = [tommaso, thierry];
|
||||
|
||||
expect(users.search('tom'), [tommaso]);
|
||||
expect(users.search('thier'), [thierry]);
|
||||
});
|
||||
|
||||
test('should search by .id or .name', () {
|
||||
final user1 = User(id: 'searchingThis');
|
||||
final user2 = User(id: 'x', name: 'searchingThis');
|
||||
|
||||
expect([user1].search('sear'), [user1]);
|
||||
expect([user2].search('sear'), [user2]);
|
||||
});
|
||||
|
||||
test('should search transliterated', () {
|
||||
final tommaso = User(id: 'tommaso', name: 'Tommaso');
|
||||
final thierry = User(id: 'thierry', name: 'Thierry');
|
||||
final users = [tommaso, thierry];
|
||||
|
||||
expect(users.search('tóm'), [tommaso]);
|
||||
expect(users.search('thíer'), [thierry]);
|
||||
});
|
||||
|
||||
test('search and sorted by distance', () {
|
||||
final tommaso = User(id: 'tommaso', name: 'Tommaso');
|
||||
final tomas = User(id: 'tomas', name: 'Tomas');
|
||||
final users = [tommaso, tomas];
|
||||
|
||||
expect(users.search('tom'), [tomas, tommaso]);
|
||||
});
|
||||
|
||||
test('should work fine with cyrillic diacritics', () {
|
||||
final petyo = User(id: '42', name: 'Петьо');
|
||||
final anastasia = User(id: '13', name: 'Анастасiя');
|
||||
final dmitriy = User(id: '99', name: 'Дмитрий');
|
||||
final users = [petyo, anastasia, dmitriy];
|
||||
|
||||
expect(users.search('petyo'), []);
|
||||
expect(users.search('Пе'), [petyo]);
|
||||
expect(users.search('Ана'), [anastasia]);
|
||||
expect(users.search('Дмитри'), [dmitriy]);
|
||||
expect(users.search('Дмитрии'), [dmitriy]);
|
||||
});
|
||||
|
||||
test('should work fine with french diacritics', () {
|
||||
final user = User(id: 'fra', name: 'françois');
|
||||
|
||||
expect([user].search('françois'), [user]);
|
||||
expect([user].search('franc'), [user]);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -263,9 +263,11 @@ void main() {
|
||||
final client = MockClient();
|
||||
final clientState = MockClientState();
|
||||
final channel = MockChannel();
|
||||
final channelState = MockChannelState();
|
||||
|
||||
when(() => client.state).thenReturn(clientState);
|
||||
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
|
||||
when(() => channel.state).thenReturn(channelState);
|
||||
|
||||
final themeData = ThemeData();
|
||||
final streamTheme = StreamChatThemeData.fromTheme(themeData);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'mocks.dart';
|
||||
|
||||
void main() {
|
||||
late StreamChatClient client;
|
||||
late Channel channel;
|
||||
|
||||
setUp(() {
|
||||
client = MockClient();
|
||||
channel = MockChannel();
|
||||
when(() => channel.on(any(), any(), any(), any()))
|
||||
.thenAnswer((_) => const Stream.empty());
|
||||
final channelClientState = MockChannelState();
|
||||
when(() => channel.state).thenReturn(channelClientState);
|
||||
when(() => channelClientState.threadsStream)
|
||||
.thenAnswer((_) => const Stream.empty());
|
||||
when(() => channelClientState.messagesStream)
|
||||
.thenAnswer((_) => const Stream.empty());
|
||||
when(() => channelClientState.messages).thenReturn([]);
|
||||
when(() => channelClientState.isUpToDate).thenReturn(true);
|
||||
});
|
||||
|
||||
// https://github.com/GetStream/stream-chat-flutter/issues/674
|
||||
testWidgets('renders empty message list view', (tester) async {
|
||||
const emptyWidgetKey = Key('empty_widget');
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StreamChat(
|
||||
client: client,
|
||||
child: StreamChannel(
|
||||
channel: channel,
|
||||
child: MessageListView(
|
||||
emptyBuilder: (_) => Container(key: emptyWidgetKey),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(MessageListView), findsOneWidget);
|
||||
expect(find.byKey(emptyWidgetKey), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -62,7 +62,7 @@ void main() {
|
||||
return Scaffold(
|
||||
body: StreamChannel(
|
||||
channel: MockChannel(),
|
||||
child: const ChannelListView(),
|
||||
child: ChannelListView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart' hide TextTheme;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
class MockStreamChatClient extends Mock implements StreamChatClient {}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
class MockStreamChatClient extends Mock implements StreamChatClient {}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ void main() {
|
||||
body: MessageSearchBloc(
|
||||
child: MessageSearchListView(
|
||||
filters: Filter.in_('members', const ['test_id']),
|
||||
messageQuery: 'test query',
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -100,6 +101,7 @@ void main() {
|
||||
body: MessageSearchBloc(
|
||||
child: MessageSearchListView(
|
||||
filters: Filter.in_('members', const ['test_id']),
|
||||
messageQuery: 'test query',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -59,6 +59,7 @@ final _messageThemeControl = MessageThemeData(
|
||||
messageLinksStyle: TextStyle(
|
||||
color: ColorTheme.light().accentPrimary,
|
||||
),
|
||||
linkBackgroundColor: ColorTheme.light().linkBg,
|
||||
);
|
||||
|
||||
final _messageThemeControlDark = MessageThemeData(
|
||||
@@ -87,4 +88,5 @@ final _messageThemeControlDark = MessageThemeData(
|
||||
messageLinksStyle: TextStyle(
|
||||
color: ColorTheme.dark().accentPrimary,
|
||||
),
|
||||
linkBackgroundColor: ColorTheme.dark().linkBg,
|
||||
);
|
||||
|
||||
@@ -56,7 +56,7 @@ void main() {
|
||||
home: Builder(
|
||||
builder: (BuildContext context) {
|
||||
_context = context;
|
||||
return const Scaffold(
|
||||
return Scaffold(
|
||||
body: UsersBloc(
|
||||
child: UserListView(),
|
||||
),
|
||||
@@ -85,7 +85,7 @@ void main() {
|
||||
home: Builder(
|
||||
builder: (BuildContext context) {
|
||||
_context = context;
|
||||
return const Scaffold(
|
||||
return Scaffold(
|
||||
body: UsersBloc(
|
||||
child: UserListView(),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user