Merge branch 'develop' into overlay-alt
This commit is contained in:
@@ -1,19 +1,55 @@
|
||||
## Upcoming
|
||||
|
||||
🐞 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.
|
||||
|
||||
🔄 Changed
|
||||
|
||||
- Animation curves changed from default `Curves.linear` to `Curves.easeOut` and `Curves.easeIn` for attachment controls.
|
||||
|
||||
## 2.2.1
|
||||
|
||||
🛑️ Breaking Changes from `2.2.1`
|
||||
⚠️ Deprecated
|
||||
|
||||
- `MessageSearchListView` paginationParams property is now non-nullable with a default value.
|
||||
- `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 non-nullable with a default value.
|
||||
- `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
|
||||
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +258,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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -608,6 +608,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
crossFadeState: _actionsShrunk
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstCurve: Curves.easeOut,
|
||||
secondCurve: Curves.easeIn,
|
||||
firstChild: IconButton(
|
||||
onPressed: () {
|
||||
if (_actionsShrunk) {
|
||||
@@ -634,20 +636,17 @@ class MessageInputState extends State<MessageInput> {
|
||||
!widget.showCommandsButton &&
|
||||
widget.actions?.isNotEmpty != true
|
||||
? const Offstage()
|
||||
: FittedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
if (!widget.disableAttachments)
|
||||
_buildAttachmentButton(context),
|
||||
if (widget.showCommandsButton &&
|
||||
widget.editMessage == null &&
|
||||
channel.state != null &&
|
||||
channel.config?.commands.isNotEmpty == true)
|
||||
_buildCommandButton(context),
|
||||
...widget.actions ?? [],
|
||||
].insertBetween(const SizedBox(width: 8)),
|
||||
),
|
||||
: Wrap(
|
||||
children: <Widget>[
|
||||
if (!widget.disableAttachments)
|
||||
_buildAttachmentButton(context),
|
||||
if (widget.showCommandsButton &&
|
||||
widget.editMessage == null &&
|
||||
channel.state != null &&
|
||||
channel.config?.commands.isNotEmpty == true)
|
||||
_buildCommandButton(context),
|
||||
...widget.actions ?? [],
|
||||
].insertBetween(const SizedBox(width: 8)),
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
alignment: Alignment.center,
|
||||
@@ -1001,108 +1000,120 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
height: _openFilePickerSection ? _kMinMediaPickerSize : 0,
|
||||
child: Material(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
height: _kMinMediaPickerSize,
|
||||
child: Material(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.pictures(
|
||||
color: _getIconColor(0),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.pictures(
|
||||
color: _getIconColor(0),
|
||||
),
|
||||
onPressed:
|
||||
_attachmentContainsFile && _attachments.isNotEmpty
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
_filePickerIndex = 0;
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
iconSize: 32,
|
||||
icon: StreamSvgIcon.files(
|
||||
color: _getIconColor(1),
|
||||
),
|
||||
onPressed:
|
||||
!_attachmentContainsFile && _attachments.isNotEmpty
|
||||
? null
|
||||
: () {
|
||||
pickFile(DefaultAttachmentTypes.file);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.camera(
|
||||
color: _getIconColor(2),
|
||||
),
|
||||
onPressed: attachmentLimitCrossed ||
|
||||
(_attachmentContainsFile &&
|
||||
_attachments.isNotEmpty)
|
||||
? null
|
||||
: () {
|
||||
pickFile(DefaultAttachmentTypes.image,
|
||||
camera: true);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
icon: StreamSvgIcon.record(
|
||||
color: _getIconColor(3),
|
||||
),
|
||||
onPressed: attachmentLimitCrossed ||
|
||||
(_attachmentContainsFile &&
|
||||
_attachments.isNotEmpty)
|
||||
? null
|
||||
: () {
|
||||
pickFile(DefaultAttachmentTypes.video,
|
||||
camera: true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
onPressed: _attachmentContainsFile && _attachments.isNotEmpty
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
_filePickerIndex = 0;
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
iconSize: 32,
|
||||
icon: StreamSvgIcon.files(
|
||||
color: _getIconColor(1),
|
||||
),
|
||||
onPressed: !_attachmentContainsFile && _attachments.isNotEmpty
|
||||
? null
|
||||
: () {
|
||||
pickFile(DefaultAttachmentTypes.file);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.camera(
|
||||
color: _getIconColor(2),
|
||||
),
|
||||
onPressed: attachmentLimitCrossed ||
|
||||
(_attachmentContainsFile && _attachments.isNotEmpty)
|
||||
? null
|
||||
: () {
|
||||
pickFile(DefaultAttachmentTypes.image, camera: true);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
icon: StreamSvgIcon.record(
|
||||
color: _getIconColor(3),
|
||||
),
|
||||
onPressed: attachmentLimitCrossed ||
|
||||
(_attachmentContainsFile && _attachments.isNotEmpty)
|
||||
? null
|
||||
: () {
|
||||
pickFile(DefaultAttachmentTypes.video, camera: true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.inputBg,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_openFilePickerSection)
|
||||
Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: _PickerWidget(
|
||||
filePickerIndex: _filePickerIndex,
|
||||
streamChatTheme: _streamChatTheme,
|
||||
containsFile: _attachmentContainsFile,
|
||||
selectedMedias: _attachments.keys.toList(),
|
||||
onAddMoreFilesClick: pickFile,
|
||||
onMediaSelected: (media) {
|
||||
if (_attachments.containsKey(media.id)) {
|
||||
setState(() => _attachments.remove(media.id));
|
||||
} else {
|
||||
_addAssetAttachment(media);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_openFilePickerSection)
|
||||
Expanded(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: _PickerWidget(
|
||||
filePickerIndex: _filePickerIndex,
|
||||
streamChatTheme: _streamChatTheme,
|
||||
containsFile: _attachmentContainsFile,
|
||||
selectedMedias: _attachments.keys.toList(),
|
||||
onAddMoreFilesClick: pickFile,
|
||||
onMediaSelected: (media) {
|
||||
if (_attachments.containsKey(media.id)) {
|
||||
setState(() => _attachments.remove(media.id));
|
||||
} else {
|
||||
_addAssetAttachment(media);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1246,7 +1257,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
Widget _buildReplyToMessage() {
|
||||
if (!_hasQuotedMessage) return const Offstage();
|
||||
final containsUrl = widget.quotedMessage!.attachments
|
||||
.any((element) => element.ogScrapeUrl != null) ==
|
||||
.any((element) => element.titleLink != null) ==
|
||||
true;
|
||||
return QuotedMessageWidget(
|
||||
reverse: true,
|
||||
@@ -1964,7 +1975,7 @@ class _PickerWidgetState extends State<_PickerWidget> {
|
||||
future: requestPermission,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const Offstage();
|
||||
}
|
||||
|
||||
if (snapshot.data!) {
|
||||
|
||||
@@ -991,7 +991,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)
|
||||
|
||||
@@ -53,12 +53,17 @@ typedef EmptyMessageSearchBuilder = Widget Function(
|
||||
/// Modify it to change the widget appearance.
|
||||
class MessageSearchListView extends StatefulWidget {
|
||||
/// Instantiate a new MessageSearchListView
|
||||
const MessageSearchListView({
|
||||
MessageSearchListView({
|
||||
Key? key,
|
||||
required this.filters,
|
||||
this.messageQuery,
|
||||
this.sortOptions,
|
||||
this.paginationParams = const PaginationParams(limit: 30),
|
||||
@Deprecated(
|
||||
"'paginationParams' is deprecated and shouldn't be used. "
|
||||
"This property is no longer used, Please use 'limit' instead",
|
||||
)
|
||||
this.paginationParams,
|
||||
int? limit,
|
||||
this.messageFilters,
|
||||
this.separatorBuilder,
|
||||
this.itemBuilder,
|
||||
@@ -71,7 +76,8 @@ class MessageSearchListView extends StatefulWidget {
|
||||
this.loadingBuilder,
|
||||
this.childBuilder,
|
||||
this.messageSearchListController,
|
||||
}) : super(key: key);
|
||||
}) : limit = limit ?? paginationParams?.limit ?? 30,
|
||||
super(key: key);
|
||||
|
||||
/// Message String to search on
|
||||
final String? messageQuery;
|
||||
@@ -93,7 +99,14 @@ class MessageSearchListView 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
|
||||
final PaginationParams paginationParams;
|
||||
@Deprecated(
|
||||
"'paginationParams' is deprecated and shouldn't be used. "
|
||||
"This property is no longer used, Please use 'limit' instead",
|
||||
)
|
||||
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 +165,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 ??
|
||||
|
||||
@@ -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,
|
||||
@@ -566,12 +580,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)
|
||||
.isNotEmpty;
|
||||
bool get hasNonUrlAttachments =>
|
||||
widget.message.attachments.where((it) => it.titleLink == null).isNotEmpty;
|
||||
|
||||
bool get hasUrlAttachments =>
|
||||
widget.message.attachments.any((it) => it.ogScrapeUrl != null) == true;
|
||||
widget.message.attachments.any((it) => it.titleLink != null) == true;
|
||||
|
||||
bool get showBottomRow =>
|
||||
showThreadReplyIndicator ||
|
||||
@@ -975,9 +988,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() ??
|
||||
@@ -1143,7 +1156,7 @@ 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)
|
||||
.forEach((e) {
|
||||
if (attachmentGroups[e.type] == null) {
|
||||
attachmentGroups[e.type!] = [];
|
||||
|
||||
@@ -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,7 +280,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
};
|
||||
|
||||
Color? _getBackgroundColor(BuildContext context) {
|
||||
if (_containsScrapeUrl) {
|
||||
if (_containsLinkAttachment) {
|
||||
return StreamChatTheme.of(context).colorTheme.linkBg;
|
||||
}
|
||||
return messageTheme.messageBackgroundColor;
|
||||
|
||||
@@ -30,10 +30,8 @@ class UrlAttachment extends StatelessWidget {
|
||||
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,
|
||||
|
||||
@@ -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 = const PaginationParams(limit: 30),
|
||||
@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,11 +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.
|
||||
// TODO: Make it non-nullable in a future breaking release
|
||||
final Filter? filter;
|
||||
|
||||
/// The sorting used for the channels matching the filters.
|
||||
@@ -93,7 +100,14 @@ 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
|
||||
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 users requested per API call.
|
||||
final int limit;
|
||||
|
||||
/// Function called when tapping on a channel
|
||||
/// By default it calls [Navigator.push] building a [MaterialPageRoute]
|
||||
@@ -184,7 +198,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,
|
||||
|
||||
@@ -7,8 +7,8 @@ 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(
|
||||
|
||||
@@ -62,7 +62,7 @@ void main() {
|
||||
return Scaffold(
|
||||
body: StreamChannel(
|
||||
channel: MockChannel(),
|
||||
child: const ChannelListView(),
|
||||
child: ChannelListView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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