Merge pull request #1625 from GetStream/release/6.4.0
This commit is contained in:
@@ -1,3 +1,12 @@
|
|||||||
|
## 6.4.0
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- [[#1293]](https://github.com/GetStream/stream-chat-flutter/issues/1293) Fixed wrong message order when sending
|
||||||
|
messages quickly.
|
||||||
|
- [[#1612]](https://github.com/GetStream/stream-chat-flutter/issues/1612) Fixed `Channel.isMutedStream` does not emit
|
||||||
|
when channel mute expires.
|
||||||
|
|
||||||
## 6.3.0
|
## 6.3.0
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:rxdart/rxdart.dart';
|
|||||||
import 'package:stream_chat/src/client/retry_queue.dart';
|
import 'package:stream_chat/src/client/retry_queue.dart';
|
||||||
import 'package:stream_chat/src/core/util/utils.dart';
|
import 'package:stream_chat/src/core/util/utils.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
/// The maximum time the incoming [Event.typingStart] event is valid before a
|
/// The maximum time the incoming [Event.typingStart] event is valid before a
|
||||||
/// [Event.typingStop] event is emitted automatically.
|
/// [Event.typingStop] event is emitted automatically.
|
||||||
@@ -563,6 +564,8 @@ class Channel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _sendMessageLock = Lock();
|
||||||
|
|
||||||
/// Send a [message] to this channel.
|
/// Send a [message] to this channel.
|
||||||
///
|
///
|
||||||
/// If [skipPush] is true the message will not send a push notification.
|
/// If [skipPush] is true the message will not send a push notification.
|
||||||
@@ -586,7 +589,7 @@ class Channel {
|
|||||||
);
|
);
|
||||||
// ignore: parameter_assignments
|
// ignore: parameter_assignments
|
||||||
message = message.copyWith(
|
message = message.copyWith(
|
||||||
createdAt: message.createdAt,
|
localCreatedAt: DateTime.now(),
|
||||||
user: _client.state.currentUser,
|
user: _client.state.currentUser,
|
||||||
quotedMessage: quotedMessage,
|
quotedMessage: quotedMessage,
|
||||||
status: MessageSendingStatus.sending,
|
status: MessageSendingStatus.sending,
|
||||||
@@ -615,14 +618,21 @@ class Channel {
|
|||||||
message = await attachmentsUploadCompleter.future;
|
message = await attachmentsUploadCompleter.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
final response = await _client.sendMessage(
|
// Wait for the previous sendMessage call to finish. Otherwise, the order
|
||||||
message,
|
// of messages will not be maintained.
|
||||||
id!,
|
final response = await _sendMessageLock.synchronized(
|
||||||
type,
|
() => _client.sendMessage(
|
||||||
skipPush: skipPush,
|
message,
|
||||||
skipEnrichUrl: skipEnrichUrl,
|
id!,
|
||||||
|
type,
|
||||||
|
skipPush: skipPush,
|
||||||
|
skipEnrichUrl: skipEnrichUrl,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
state!.updateMessage(response.message);
|
|
||||||
|
final sentMessage = response.message.syncWith(message);
|
||||||
|
|
||||||
|
state!.updateMessage(sentMessage);
|
||||||
if (cooldown > 0) cooldownStartedAt = DateTime.now();
|
if (cooldown > 0) cooldownStartedAt = DateTime.now();
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -633,6 +643,8 @@ class Channel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _updateMessageLock = Lock();
|
||||||
|
|
||||||
/// Updates the [message] in this channel.
|
/// Updates the [message] in this channel.
|
||||||
///
|
///
|
||||||
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
|
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
|
||||||
@@ -652,7 +664,7 @@ class Channel {
|
|||||||
// ignore: parameter_assignments
|
// ignore: parameter_assignments
|
||||||
message = message.copyWith(
|
message = message.copyWith(
|
||||||
status: MessageSendingStatus.updating,
|
status: MessageSendingStatus.updating,
|
||||||
updatedAt: message.updatedAt,
|
localUpdatedAt: DateTime.now(),
|
||||||
attachments: message.attachments.map(
|
attachments: message.attachments.map(
|
||||||
(it) {
|
(it) {
|
||||||
if (it.uploadState.isSuccess) return it;
|
if (it.uploadState.isSuccess) return it;
|
||||||
@@ -678,16 +690,20 @@ class Channel {
|
|||||||
message = await attachmentsUploadCompleter.future;
|
message = await attachmentsUploadCompleter.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
final response = await _client.updateMessage(
|
// Wait for the previous update call to finish. Otherwise, the order of
|
||||||
message,
|
// messages will not be maintained.
|
||||||
skipEnrichUrl: skipEnrichUrl,
|
final response = await _updateMessageLock.synchronized(
|
||||||
|
() => _client.updateMessage(
|
||||||
|
message,
|
||||||
|
skipEnrichUrl: skipEnrichUrl,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final m = response.message.copyWith(
|
final updatedMessage = response.message
|
||||||
ownReactions: message.ownReactions,
|
.syncWith(message)
|
||||||
);
|
.copyWith(ownReactions: message.ownReactions);
|
||||||
|
|
||||||
state?.updateMessage(m);
|
state?.updateMessage(updatedMessage);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -714,16 +730,20 @@ class Channel {
|
|||||||
bool skipEnrichUrl = false,
|
bool skipEnrichUrl = false,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final response = await _client.partialUpdateMessage(
|
// Wait for the previous update call to finish. Otherwise, the order of
|
||||||
message.id,
|
// messages will not be maintained.
|
||||||
set: set,
|
final response = await _updateMessageLock.synchronized(
|
||||||
unset: unset,
|
() => _client.partialUpdateMessage(
|
||||||
skipEnrichUrl: skipEnrichUrl,
|
message.id,
|
||||||
|
set: set,
|
||||||
|
unset: unset,
|
||||||
|
skipEnrichUrl: skipEnrichUrl,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final updatedMessage = response.message.copyWith(
|
final updatedMessage = response.message
|
||||||
ownReactions: message.ownReactions,
|
.syncWith(message)
|
||||||
);
|
.copyWith(ownReactions: message.ownReactions);
|
||||||
|
|
||||||
state?.updateMessage(updatedMessage);
|
state?.updateMessage(updatedMessage);
|
||||||
|
|
||||||
@@ -736,6 +756,8 @@ class Channel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _deleteMessageLock = Lock();
|
||||||
|
|
||||||
/// Deletes the [message] from the channel.
|
/// Deletes the [message] from the channel.
|
||||||
Future<EmptyResponse> deleteMessage(Message message, {bool? hard}) async {
|
Future<EmptyResponse> deleteMessage(Message message, {bool? hard}) async {
|
||||||
final hardDelete = hard ?? false;
|
final hardDelete = hard ?? false;
|
||||||
@@ -746,7 +768,7 @@ class Channel {
|
|||||||
state!.deleteMessage(
|
state!.deleteMessage(
|
||||||
message.copyWith(
|
message.copyWith(
|
||||||
type: 'deleted',
|
type: 'deleted',
|
||||||
deletedAt: message.deletedAt ?? DateTime.now(),
|
localDeletedAt: DateTime.now(),
|
||||||
status: MessageSendingStatus.sent,
|
status: MessageSendingStatus.sent,
|
||||||
),
|
),
|
||||||
hardDelete: hardDelete,
|
hardDelete: hardDelete,
|
||||||
@@ -770,13 +792,18 @@ class Channel {
|
|||||||
|
|
||||||
state?.deleteMessage(message, hardDelete: hardDelete);
|
state?.deleteMessage(message, hardDelete: hardDelete);
|
||||||
|
|
||||||
final response = await _client.deleteMessage(message.id, hard: hard);
|
// Wait for the previous delete call to finish. Otherwise, the order of
|
||||||
|
// messages will not be maintained.
|
||||||
state?.deleteMessage(
|
final response = await _deleteMessageLock.synchronized(
|
||||||
message.copyWith(status: MessageSendingStatus.sent),
|
() => _client.deleteMessage(message.id, hard: hard),
|
||||||
hardDelete: hardDelete,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final deletedMessage = message.copyWith(
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
|
);
|
||||||
|
|
||||||
|
state?.deleteMessage(deletedMessage, hardDelete: hardDelete);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is StreamChatNetworkError && e.isRetriable) {
|
if (e is StreamChatNetworkError && e.isRetriable) {
|
||||||
@@ -1420,15 +1447,32 @@ class Channel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timer to keep track of mute expiration. This is used to update the channel
|
||||||
|
// state when the mute expires.
|
||||||
|
Timer? _muteExpirationTimer;
|
||||||
|
|
||||||
/// Mutes the channel.
|
/// Mutes the channel.
|
||||||
Future<EmptyResponse> mute({Duration? expiration}) {
|
Future<EmptyResponse> mute({Duration? expiration}) {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
|
|
||||||
|
// If there is a expiration set, we will set a timer to automatically unmute
|
||||||
|
// the channel when the mute expires.
|
||||||
|
if (expiration != null) {
|
||||||
|
_muteExpirationTimer?.cancel();
|
||||||
|
_muteExpirationTimer = Timer(expiration, unmute);
|
||||||
|
}
|
||||||
|
|
||||||
return _client.muteChannel(cid!, expiration: expiration);
|
return _client.muteChannel(cid!, expiration: expiration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unmute the channel.
|
/// Unmute the channel.
|
||||||
Future<EmptyResponse> unmute() {
|
Future<EmptyResponse> unmute() {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
|
|
||||||
|
// Cancel the mute expiration timer if it is set.
|
||||||
|
_muteExpirationTimer?.cancel();
|
||||||
|
_muteExpirationTimer = null;
|
||||||
|
|
||||||
return _client.unmuteChannel(cid!);
|
return _client.unmuteChannel(cid!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1558,6 +1602,7 @@ class Channel {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
client.state.removeChannel('$cid');
|
client.state.removeChannel('$cid');
|
||||||
state?.dispose();
|
state?.dispose();
|
||||||
|
_muteExpirationTimer?.cancel();
|
||||||
_keyStrokeHandler.cancel();
|
_keyStrokeHandler.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1942,8 +1987,9 @@ class ChannelClientState {
|
|||||||
)
|
)
|
||||||
.listen((event) {
|
.listen((event) {
|
||||||
final message = event.message!;
|
final message = event.message!;
|
||||||
if (isUpToDate ||
|
final showInChannel =
|
||||||
(message.parentId != null && message.showInChannel != true)) {
|
message.parentId != null && message.showInChannel != true;
|
||||||
|
if (isUpToDate || showInChannel) {
|
||||||
updateMessage(message);
|
updateMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1960,10 +2006,10 @@ class ChannelClientState {
|
|||||||
var newMessages = [...messages];
|
var newMessages = [...messages];
|
||||||
final oldIndex = newMessages.indexWhere((m) => m.id == message.id);
|
final oldIndex = newMessages.indexWhere((m) => m.id == message.id);
|
||||||
if (oldIndex != -1) {
|
if (oldIndex != -1) {
|
||||||
var updatedMessage = message;
|
final oldMessage = newMessages[oldIndex];
|
||||||
|
var updatedMessage = message.syncWith(oldMessage);
|
||||||
// Add quoted message to the message if it is not present.
|
// Add quoted message to the message if it is not present.
|
||||||
if (message.quotedMessageId != null && message.quotedMessage == null) {
|
if (message.quotedMessageId != null && message.quotedMessage == null) {
|
||||||
final oldMessage = newMessages[oldIndex];
|
|
||||||
updatedMessage = updatedMessage.copyWith(
|
updatedMessage = updatedMessage.copyWith(
|
||||||
quotedMessage: oldMessage.quotedMessage,
|
quotedMessage: oldMessage.quotedMessage,
|
||||||
);
|
);
|
||||||
@@ -1980,7 +2026,7 @@ class ChannelClientState {
|
|||||||
return it.copyWith(
|
return it.copyWith(
|
||||||
quotedMessage: updatedMessage.copyWith(
|
quotedMessage: updatedMessage.copyWith(
|
||||||
type: 'deleted',
|
type: 'deleted',
|
||||||
deletedAt: updatedMessage.deletedAt ?? DateTime.now(),
|
deletedAt: DateTime.now(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
@@ -2004,7 +2050,7 @@ class ChannelClientState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_channelState = _channelState.copyWith(
|
_channelState = _channelState.copyWith(
|
||||||
messages: newMessages..sort(_sortByCreatedAt),
|
messages: newMessages.sorted(_sortByCreatedAt),
|
||||||
pinnedMessages: newPinnedMessages,
|
pinnedMessages: newPinnedMessages,
|
||||||
channel: _channelState.channel?.copyWith(
|
channel: _channelState.channel?.copyWith(
|
||||||
lastMessageAt: message.createdAt,
|
lastMessageAt: message.createdAt,
|
||||||
@@ -2226,7 +2272,7 @@ class ChannelClientState {
|
|||||||
...newThreads[parentId]!.where(
|
...newThreads[parentId]!.where(
|
||||||
(newMessage) => !messages.any((m) => m.id == newMessage.id),
|
(newMessage) => !messages.any((m) => m.id == newMessage.id),
|
||||||
),
|
),
|
||||||
]..sort(_sortByCreatedAt);
|
].sorted(_sortByCreatedAt);
|
||||||
} else {
|
} else {
|
||||||
newThreads[parentId] = messages;
|
newThreads[parentId] = messages;
|
||||||
}
|
}
|
||||||
@@ -2245,15 +2291,10 @@ class ChannelClientState {
|
|||||||
|
|
||||||
/// Update channelState with updated information.
|
/// Update channelState with updated information.
|
||||||
void updateChannelState(ChannelState updatedState) {
|
void updateChannelState(ChannelState updatedState) {
|
||||||
final _existingStateMessages = _channelState.messages ?? [];
|
final _existingStateMessages = [...messages];
|
||||||
final _updatedStateMessages = updatedState.messages ?? [];
|
|
||||||
final newMessages = <Message>[
|
final newMessages = <Message>[
|
||||||
..._updatedStateMessages,
|
..._existingStateMessages.merge(updatedState.messages),
|
||||||
..._existingStateMessages
|
].sorted(_sortByCreatedAt);
|
||||||
.where((m) =>
|
|
||||||
!_updatedStateMessages.any((newMessage) => newMessage.id == m.id))
|
|
||||||
.toList(),
|
|
||||||
]..sort(_sortByCreatedAt);
|
|
||||||
|
|
||||||
final _existingStateWatchers = _channelState.watchers ?? [];
|
final _existingStateWatchers = _channelState.watchers ?? [];
|
||||||
final _updatedStateWatchers = updatedState.watchers ?? [];
|
final _updatedStateWatchers = updatedState.watchers ?? [];
|
||||||
@@ -2471,3 +2512,21 @@ bool _pinIsValid(Message message) {
|
|||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
return message.pinExpires!.isAfter(now);
|
return message.pinExpires!.isAfter(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on Iterable<Message> {
|
||||||
|
Iterable<Message> merge(Iterable<Message>? other) {
|
||||||
|
if (other == null) return this;
|
||||||
|
|
||||||
|
final messageMap = {for (final message in this) message.id: message};
|
||||||
|
|
||||||
|
for (final message in other) {
|
||||||
|
messageMap.update(
|
||||||
|
message.id,
|
||||||
|
message.syncWith,
|
||||||
|
ifAbsent: () => message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageMap.values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import 'package:stream_chat/src/event_type.dart';
|
|||||||
import 'package:stream_chat/src/ws/connection_status.dart';
|
import 'package:stream_chat/src/ws/connection_status.dart';
|
||||||
import 'package:stream_chat/src/ws/websocket.dart';
|
import 'package:stream_chat/src/ws/websocket.dart';
|
||||||
import 'package:stream_chat/version.dart';
|
import 'package:stream_chat/version.dart';
|
||||||
import 'package:synchronized/extension.dart';
|
import 'package:synchronized/synchronized.dart';
|
||||||
|
|
||||||
/// Handler function used for logging records. Function requires a single
|
/// Handler function used for logging records. Function requires a single
|
||||||
/// [LogRecord] as the only parameter.
|
/// [LogRecord] as the only parameter.
|
||||||
@@ -526,10 +526,13 @@ class StreamChatClient {
|
|||||||
event.type == eventType4);
|
event.type == eventType4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lock to make sure only one sync process is running at a time.
|
||||||
|
final _syncLock = Lock();
|
||||||
|
|
||||||
/// Get the events missed while offline to sync the offline storage
|
/// Get the events missed while offline to sync the offline storage
|
||||||
/// Will automatically fetch [cids] and [lastSyncedAt] if [persistenceEnabled]
|
/// Will automatically fetch [cids] and [lastSyncedAt] if [persistenceEnabled]
|
||||||
Future<void> sync({List<String>? cids, DateTime? lastSyncAt}) {
|
Future<void> sync({List<String>? cids, DateTime? lastSyncAt}) {
|
||||||
return synchronized(() async {
|
return _syncLock.synchronized(() async {
|
||||||
final channels = cids ?? await chatPersistenceClient?.getChannelCids();
|
final channels = cids ?? await chatPersistenceClient?.getChannelCids();
|
||||||
if (channels == null || channels.isEmpty) {
|
if (channels == null || channels.isEmpty) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -64,8 +64,11 @@ class Message extends Equatable {
|
|||||||
this.showInChannel,
|
this.showInChannel,
|
||||||
this.command,
|
this.command,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
|
this.localCreatedAt,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
this.deletedAt,
|
this.localUpdatedAt,
|
||||||
|
DateTime? deletedAt,
|
||||||
|
this.localDeletedAt,
|
||||||
this.user,
|
this.user,
|
||||||
this.pinned = false,
|
this.pinned = false,
|
||||||
this.pinnedAt,
|
this.pinnedAt,
|
||||||
@@ -76,8 +79,9 @@ class Message extends Equatable {
|
|||||||
this.i18n,
|
this.i18n,
|
||||||
}) : id = id ?? const Uuid().v4(),
|
}) : id = id ?? const Uuid().v4(),
|
||||||
pinExpires = pinExpires?.toUtc(),
|
pinExpires = pinExpires?.toUtc(),
|
||||||
_createdAt = createdAt,
|
remoteCreatedAt = createdAt,
|
||||||
_updatedAt = updatedAt,
|
remoteUpdatedAt = updatedAt,
|
||||||
|
remoteDeletedAt = deletedAt,
|
||||||
_quotedMessageId = quotedMessageId;
|
_quotedMessageId = quotedMessageId;
|
||||||
|
|
||||||
/// Create a new instance from JSON.
|
/// Create a new instance from JSON.
|
||||||
@@ -161,21 +165,49 @@ class Message extends Equatable {
|
|||||||
@JsonKey(includeToJson: false)
|
@JsonKey(includeToJson: false)
|
||||||
final String? command;
|
final String? command;
|
||||||
|
|
||||||
final DateTime? _createdAt;
|
/// Indicates when the message was created.
|
||||||
|
///
|
||||||
/// Reserved field indicating when the message was deleted.
|
/// Returns the latest between [localCreatedAt] and [remoteCreatedAt].
|
||||||
|
/// If both are null, returns [DateTime.now].
|
||||||
@JsonKey(includeToJson: false)
|
@JsonKey(includeToJson: false)
|
||||||
final DateTime? deletedAt;
|
DateTime get createdAt => localCreatedAt ?? remoteCreatedAt ?? DateTime.now();
|
||||||
|
|
||||||
/// Reserved field indicating when the message was created.
|
/// Indicates when the message was created locally.
|
||||||
|
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||||
|
final DateTime? localCreatedAt;
|
||||||
|
|
||||||
|
/// Indicates when the message was created on the server.
|
||||||
|
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||||
|
final DateTime? remoteCreatedAt;
|
||||||
|
|
||||||
|
/// Indicates when the message was updated last time.
|
||||||
|
///
|
||||||
|
/// Returns the latest between [localUpdatedAt] and [remoteUpdatedAt].
|
||||||
|
/// If both are null, returns [createdAt].
|
||||||
@JsonKey(includeToJson: false)
|
@JsonKey(includeToJson: false)
|
||||||
DateTime get createdAt => _createdAt ?? DateTime.now();
|
DateTime get updatedAt => localUpdatedAt ?? remoteUpdatedAt ?? createdAt;
|
||||||
|
|
||||||
final DateTime? _updatedAt;
|
/// Indicates when the message was updated locally.
|
||||||
|
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||||
|
final DateTime? localUpdatedAt;
|
||||||
|
|
||||||
/// Reserved field indicating when the message was updated last time.
|
/// Indicates when the message was updated on the server.
|
||||||
|
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||||
|
final DateTime? remoteUpdatedAt;
|
||||||
|
|
||||||
|
/// Indicates when the message was deleted.
|
||||||
|
///
|
||||||
|
/// Returns the latest between [localDeletedAt] and [remoteDeletedAt].
|
||||||
@JsonKey(includeToJson: false)
|
@JsonKey(includeToJson: false)
|
||||||
DateTime get updatedAt => _updatedAt ?? DateTime.now();
|
DateTime? get deletedAt => localDeletedAt ?? remoteDeletedAt;
|
||||||
|
|
||||||
|
/// Indicates when the message was deleted locally.
|
||||||
|
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||||
|
final DateTime? localDeletedAt;
|
||||||
|
|
||||||
|
/// Indicates when the message was deleted on the server.
|
||||||
|
@JsonKey(includeToJson: false, includeFromJson: false)
|
||||||
|
final DateTime? remoteDeletedAt;
|
||||||
|
|
||||||
/// User who sent the message.
|
/// User who sent the message.
|
||||||
@JsonKey(includeToJson: false)
|
@JsonKey(includeToJson: false)
|
||||||
@@ -273,8 +305,11 @@ class Message extends Equatable {
|
|||||||
bool? showInChannel,
|
bool? showInChannel,
|
||||||
String? command,
|
String? command,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
|
DateTime? localCreatedAt,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
|
DateTime? localUpdatedAt,
|
||||||
DateTime? deletedAt,
|
DateTime? deletedAt,
|
||||||
|
DateTime? localDeletedAt,
|
||||||
User? user,
|
User? user,
|
||||||
bool? pinned,
|
bool? pinned,
|
||||||
DateTime? pinnedAt,
|
DateTime? pinnedAt,
|
||||||
@@ -338,9 +373,12 @@ class Message extends Equatable {
|
|||||||
threadParticipants: threadParticipants ?? this.threadParticipants,
|
threadParticipants: threadParticipants ?? this.threadParticipants,
|
||||||
showInChannel: showInChannel ?? this.showInChannel,
|
showInChannel: showInChannel ?? this.showInChannel,
|
||||||
command: command ?? this.command,
|
command: command ?? this.command,
|
||||||
createdAt: createdAt ?? _createdAt,
|
createdAt: createdAt ?? remoteCreatedAt,
|
||||||
updatedAt: updatedAt ?? _updatedAt,
|
localCreatedAt: localCreatedAt ?? this.localCreatedAt,
|
||||||
deletedAt: deletedAt ?? this.deletedAt,
|
updatedAt: updatedAt ?? remoteUpdatedAt,
|
||||||
|
localUpdatedAt: localUpdatedAt ?? this.localUpdatedAt,
|
||||||
|
deletedAt: deletedAt ?? remoteDeletedAt,
|
||||||
|
localDeletedAt: localDeletedAt ?? this.localDeletedAt,
|
||||||
user: user ?? this.user,
|
user: user ?? this.user,
|
||||||
pinned: pinned ?? this.pinned,
|
pinned: pinned ?? this.pinned,
|
||||||
pinnedAt: pinnedAt ?? this.pinnedAt,
|
pinnedAt: pinnedAt ?? this.pinnedAt,
|
||||||
@@ -374,9 +412,12 @@ class Message extends Equatable {
|
|||||||
threadParticipants: other.threadParticipants,
|
threadParticipants: other.threadParticipants,
|
||||||
showInChannel: other.showInChannel,
|
showInChannel: other.showInChannel,
|
||||||
command: other.command,
|
command: other.command,
|
||||||
createdAt: other.createdAt,
|
createdAt: other.remoteCreatedAt,
|
||||||
updatedAt: other.updatedAt,
|
localCreatedAt: other.localCreatedAt,
|
||||||
deletedAt: other.deletedAt,
|
updatedAt: other.remoteUpdatedAt,
|
||||||
|
localUpdatedAt: other.localUpdatedAt,
|
||||||
|
deletedAt: other.remoteDeletedAt,
|
||||||
|
localDeletedAt: other.localDeletedAt,
|
||||||
user: other.user,
|
user: other.user,
|
||||||
pinned: other.pinned,
|
pinned: other.pinned,
|
||||||
pinnedAt: other.pinnedAt,
|
pinnedAt: other.pinnedAt,
|
||||||
@@ -387,6 +428,28 @@ class Message extends Equatable {
|
|||||||
i18n: other.i18n,
|
i18n: other.i18n,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Returns a new [Message] that is [other] with local changes applied to it.
|
||||||
|
///
|
||||||
|
/// This ensures that the local sync changes are not lost when the message is
|
||||||
|
/// updated on the server.
|
||||||
|
///
|
||||||
|
/// For example, when a message is sent, it is immediately shown
|
||||||
|
/// optimistically in the UI. When the message is received from the server,
|
||||||
|
/// it will not contain the local changes. This method can be used to merge
|
||||||
|
/// the local changes back into the message.
|
||||||
|
///
|
||||||
|
/// This also helps in maintaining the order of the messages in the channel
|
||||||
|
/// when the messages are sorted by the [createdAt] field.
|
||||||
|
Message syncWith(Message? other) {
|
||||||
|
if (other == null) return this;
|
||||||
|
|
||||||
|
return copyWith(
|
||||||
|
localCreatedAt: other.localCreatedAt,
|
||||||
|
localUpdatedAt: other.localUpdatedAt,
|
||||||
|
localDeletedAt: other.localDeletedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
id,
|
id,
|
||||||
@@ -407,9 +470,12 @@ class Message extends Equatable {
|
|||||||
shadowed,
|
shadowed,
|
||||||
silent,
|
silent,
|
||||||
command,
|
command,
|
||||||
_createdAt,
|
localCreatedAt,
|
||||||
_updatedAt,
|
remoteCreatedAt,
|
||||||
deletedAt,
|
localUpdatedAt,
|
||||||
|
remoteUpdatedAt,
|
||||||
|
localDeletedAt,
|
||||||
|
remoteDeletedAt,
|
||||||
user,
|
user,
|
||||||
pinned,
|
pinned,
|
||||||
pinnedAt,
|
pinnedAt,
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
|
|||||||
/// Current package version
|
/// Current package version
|
||||||
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
||||||
// ignore: constant_identifier_names
|
// ignore: constant_identifier_names
|
||||||
const PACKAGE_VERSION = '6.3.0';
|
const PACKAGE_VERSION = '6.4.0';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat
|
name: stream_chat
|
||||||
homepage: https://getstream.io/
|
homepage: https://getstream.io/
|
||||||
description: The official Dart client for Stream Chat, a service for building chat applications.
|
description: The official Dart client for Stream Chat, a service for building chat applications.
|
||||||
version: 6.3.0
|
version: 6.4.0
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
|
|||||||
@@ -2481,6 +2481,31 @@ void main() {
|
|||||||
)).called(1);
|
)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('`.mute with expiration`', () async {
|
||||||
|
const expiration = Duration(seconds: 3);
|
||||||
|
|
||||||
|
when(() => client.muteChannel(
|
||||||
|
channelCid,
|
||||||
|
expiration: expiration,
|
||||||
|
)).thenAnswer((_) async => EmptyResponse());
|
||||||
|
|
||||||
|
when(() => client.unmuteChannel(channelCid))
|
||||||
|
.thenAnswer((_) async => EmptyResponse());
|
||||||
|
|
||||||
|
final res = await channel.mute(expiration: expiration);
|
||||||
|
|
||||||
|
expect(res, isNotNull);
|
||||||
|
|
||||||
|
verify(() => client.muteChannel(
|
||||||
|
channelCid,
|
||||||
|
expiration: expiration,
|
||||||
|
)).called(1);
|
||||||
|
|
||||||
|
// wait for expiration
|
||||||
|
await Future.delayed(expiration);
|
||||||
|
verify(() => client.unmuteChannel(channelCid)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('`.unmute`', () async {
|
test('`.unmute`', () async {
|
||||||
when(
|
when(
|
||||||
() => client.unmuteChannel(channelCid),
|
() => client.unmuteChannel(channelCid),
|
||||||
|
|||||||
@@ -1,3 +1,100 @@
|
|||||||
|
## 6.4.0
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- [[#1600]](https://github.com/GetStream/stream-chat-flutter/issues/1600) Fixed type `ImageDecoderCallback` not found
|
||||||
|
error on pre-Flutter 3.10.0 versions.
|
||||||
|
- [[#1605]](https://github.com/GetStream/stream-chat-flutter/issues/1605) Fixed Null exception is thrown on message list
|
||||||
|
for unread messages when `ScrollToBottomButton` is pressed.
|
||||||
|
- [[#1615]](https://github.com/GetStream/stream-chat-flutter/issues/1615) Fixed `StreamAttachmentPickerBottomSheet` not
|
||||||
|
able to find the `StreamChatTheme` when used in nested MaterialApp.
|
||||||
|
|
||||||
|
✅ Added
|
||||||
|
|
||||||
|
- Added support for `StreamMessageInput.allowedAttachmentPickerTypes` to specify the allowed attachment picker types.
|
||||||
|
[#1601](https://github.com/GetStream/stream-chat-flutter/issues/1376)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
StreamMessageInput(
|
||||||
|
...,
|
||||||
|
allowedAttachmentPickerTypes: const [
|
||||||
|
AttachmentPickerType.files,
|
||||||
|
AttachmentPickerType.images,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Added support for `StreamMessageWidget.onConfirmDeleteTap` to override the default action on delete confirmation.
|
||||||
|
[#1604](https://github.com/GetStream/stream-chat-flutter/issues/1604)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
StreamMessageWidget(
|
||||||
|
...,
|
||||||
|
onConfirmDeleteTap: (message) async {
|
||||||
|
final channel = StreamChannel.of(context).channel;
|
||||||
|
await channel.deleteMessage(message, hard: false);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Added support for `StreamMessageWidget.quotedMessageBuilder` and `StreamMessageInput.quotedMessageBuilder` to override
|
||||||
|
the default quoted message widget. [#1547](https://github.com/GetStream/stream-chat-flutter/issues/1547)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
StreamMessageWidget(
|
||||||
|
...,
|
||||||
|
quotedMessageBuilder: (context, message) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.red,
|
||||||
|
child: Text('Quoted Message'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Added support for `StreamChannelAvatar.ownSpaceAvatarBuilder`, `StreamChannelAvatar.oneToOneAvatarBuilder` and
|
||||||
|
`StreamChannelAvatar.groupAvatarBuilder` to override the default avatar
|
||||||
|
widget.[#1614](https://github.com/GetStream/stream-chat-flutter/issues/1614)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
StreamChannelAvatar(
|
||||||
|
...,
|
||||||
|
ownSpaceAvatarBuilder: (context, channel) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.red,
|
||||||
|
child: Text('Own Space Avatar'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
oneToOneAvatarBuilder: (context, channel) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.red,
|
||||||
|
child: Text('One to One Avatar'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
groupAvatarBuilder: (context, channel) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.red,
|
||||||
|
child: Text('Group Avatar'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Added support for `StreamMessageInput.contentInsertionConfiguration` to specify the content insertion configuration.
|
||||||
|
[#1613](https://github.com/GetStream/stream-chat-flutter/issues/1613)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
StreamMessageInput(
|
||||||
|
...,
|
||||||
|
contentInsertionConfiguration: ContentInsertionConfiguration(
|
||||||
|
onContentInserted: (content) {
|
||||||
|
// Do something with the content.
|
||||||
|
controller.addAttachment(...);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
## 6.3.0
|
## 6.3.0
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'package:stream_chat_flutter_example/debug/error_dialog.dart';
|
||||||
|
|
||||||
|
class DebugBanUser extends StatelessWidget {
|
||||||
|
const DebugBanUser({
|
||||||
|
super.key,
|
||||||
|
required this.client,
|
||||||
|
});
|
||||||
|
|
||||||
|
final StreamChatClient client;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Ban User',
|
||||||
|
hintText: 'User Id',
|
||||||
|
isDense: true,
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onSubmitted: (value) async {
|
||||||
|
final userId = value.trim();
|
||||||
|
try {
|
||||||
|
debugPrint('[banUser] userId: $userId');
|
||||||
|
final result = await client.banUser(userId);
|
||||||
|
debugPrint('[banUser] completed: $result');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[banUser] failed: $e');
|
||||||
|
showErrorDialog(context, e, 'Ban User');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'package:stream_chat_flutter_example/debug/error_dialog.dart';
|
||||||
|
|
||||||
|
class DebugMuteUser extends StatelessWidget {
|
||||||
|
const DebugMuteUser({
|
||||||
|
super.key,
|
||||||
|
required this.client,
|
||||||
|
});
|
||||||
|
|
||||||
|
final StreamChatClient client;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Mute User',
|
||||||
|
hintText: 'User Id',
|
||||||
|
isDense: true,
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onSubmitted: (value) async {
|
||||||
|
final userId = value.trim();
|
||||||
|
try {
|
||||||
|
debugPrint('[muteUser] userId: $userId');
|
||||||
|
final result = await client.muteUser(userId);
|
||||||
|
debugPrint('[muteUser] completed: $result');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[muteUser] failed: $e');
|
||||||
|
showErrorDialog(context, e, 'Mute User');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'package:stream_chat_flutter_example/debug/error_dialog.dart';
|
||||||
|
|
||||||
|
class DebugRemoveShadowBan extends StatelessWidget {
|
||||||
|
const DebugRemoveShadowBan({
|
||||||
|
super.key,
|
||||||
|
required this.client,
|
||||||
|
});
|
||||||
|
|
||||||
|
final StreamChatClient client;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Remove Shadow Ban',
|
||||||
|
hintText: 'User Id',
|
||||||
|
isDense: true,
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onSubmitted: (value) async {
|
||||||
|
final userId = value.trim();
|
||||||
|
try {
|
||||||
|
debugPrint('[removeShadowBan] userId: $userId');
|
||||||
|
final result = await client.removeShadowBan(userId);
|
||||||
|
debugPrint('[removeShadowBan] result: $result');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[removeShadowBan] failed: $e');
|
||||||
|
showErrorDialog(context, e, 'Remove Shadow Ban');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'package:stream_chat_flutter_example/debug/error_dialog.dart';
|
||||||
|
|
||||||
|
class DebugShadowBan extends StatelessWidget {
|
||||||
|
const DebugShadowBan({
|
||||||
|
super.key,
|
||||||
|
required this.client,
|
||||||
|
});
|
||||||
|
|
||||||
|
final StreamChatClient client;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Shadow Ban',
|
||||||
|
hintText: 'User Id',
|
||||||
|
isDense: true,
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onSubmitted: (value) async {
|
||||||
|
final userId = value.trim();
|
||||||
|
try {
|
||||||
|
debugPrint('[shadowBan] userId: $userId');
|
||||||
|
final result = await client.shadowBan(userId);
|
||||||
|
debugPrint('[shadowBan] completed: $result');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[shadowBan] failed: $e');
|
||||||
|
showErrorDialog(context, e, 'Shadow Ban');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'package:stream_chat_flutter_example/debug/error_dialog.dart';
|
||||||
|
|
||||||
|
class DebugUnbanUser extends StatelessWidget {
|
||||||
|
const DebugUnbanUser({
|
||||||
|
super.key,
|
||||||
|
required this.client,
|
||||||
|
});
|
||||||
|
|
||||||
|
final StreamChatClient client;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Unban User',
|
||||||
|
hintText: 'User Id',
|
||||||
|
isDense: true,
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onSubmitted: (value) async {
|
||||||
|
final userId = value.trim();
|
||||||
|
try {
|
||||||
|
debugPrint('[unbanUser] userId: $userId');
|
||||||
|
final result = await client.unbanUser(userId);
|
||||||
|
debugPrint('[unbanUser] completed: $result');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[unbanUser] failed: $e');
|
||||||
|
showErrorDialog(context, e, 'Unban User');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
import 'package:stream_chat_flutter_example/debug/error_dialog.dart';
|
||||||
|
|
||||||
|
class DebugUnmuteUser extends StatelessWidget {
|
||||||
|
const DebugUnmuteUser({
|
||||||
|
super.key,
|
||||||
|
required this.client,
|
||||||
|
});
|
||||||
|
|
||||||
|
final StreamChatClient client;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Unmute User',
|
||||||
|
hintText: 'User Id',
|
||||||
|
isDense: true,
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onSubmitted: (value) async {
|
||||||
|
final userId = value.trim();
|
||||||
|
try {
|
||||||
|
debugPrint('[unmuteUser] userId: $userId');
|
||||||
|
final result = await client.unmuteUser(userId);
|
||||||
|
debugPrint('[unmuteUser] completed: $result');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('[unmuteUser] failed: $e');
|
||||||
|
showErrorDialog(context, e, 'Unmute User');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
import 'package:stream_chat_flutter_example/debug/actions/ban_user.dart';
|
||||||
|
import 'package:stream_chat_flutter_example/debug/actions/mute_user.dart';
|
||||||
|
import 'package:stream_chat_flutter_example/debug/actions/remove_shadow_ban.dart';
|
||||||
|
import 'package:stream_chat_flutter_example/debug/actions/shadow_ban.dart';
|
||||||
|
import 'package:stream_chat_flutter_example/debug/actions/unban_user.dart';
|
||||||
|
import 'package:stream_chat_flutter_example/debug/actions/unmute_user.dart';
|
||||||
|
import 'package:stream_chat_flutter_example/debug/members.dart';
|
||||||
|
import 'package:stream_chat_flutter_example/debug/mutes.dart';
|
||||||
|
|
||||||
|
class DebugChannelPage extends StatefulWidget {
|
||||||
|
const DebugChannelPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _DebugChannelPageState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DebugChannelPageState extends State<DebugChannelPage> {
|
||||||
|
late final Channel _channel = StreamChannel.of(context).channel;
|
||||||
|
|
||||||
|
StreamSubscription<ChannelState>? _channelSubscription;
|
||||||
|
StreamSubscription<OwnUser?>? _ownUserSubscription;
|
||||||
|
|
||||||
|
ChannelState? _channelState;
|
||||||
|
OwnUser? _ownUser;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_channelSubscription = _channel.state!.channelStateStream.listen((state) {
|
||||||
|
setState(() => _channelState = state);
|
||||||
|
});
|
||||||
|
_ownUserSubscription =
|
||||||
|
_channel.client.state.currentUserStream.listen((ownUser) {
|
||||||
|
setState(() => _ownUser = ownUser);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_channelSubscription?.cancel();
|
||||||
|
_ownUserSubscription?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final members =
|
||||||
|
_channelState?.members ?? _channel.state?.members ?? const [];
|
||||||
|
final mutes =
|
||||||
|
_ownUser?.mutes ?? _channel.client.state.currentUser?.mutes ?? const [];
|
||||||
|
//SingleChildScrollView
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_channel.name ?? _channel.cid ?? '?'),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
DebugMe(client: _channel.client),
|
||||||
|
DebugMembers(members: members),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DebugMutes(mutes: mutes),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DebugMuteUser(client: _channel.client),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DebugUnmuteUser(client: _channel.client),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DebugBanUser(client: _channel.client),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DebugUnbanUser(client: _channel.client),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DebugShadowBan(client: _channel.client),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DebugRemoveShadowBan(client: _channel.client),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DebugMe extends StatelessWidget {
|
||||||
|
const DebugMe({
|
||||||
|
super.key,
|
||||||
|
required this.client,
|
||||||
|
});
|
||||||
|
|
||||||
|
final StreamChatClient client;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Me: ',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
client.state.currentUser?.id ?? '?',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Future<void> showErrorDialog(
|
||||||
|
BuildContext context,
|
||||||
|
Object e,
|
||||||
|
String operation,
|
||||||
|
) async {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false, // user must tap button!
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('$operation Failed'),
|
||||||
|
content: SingleChildScrollView(child: Text('$e')),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Close'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
class DebugMembers extends StatelessWidget {
|
||||||
|
const DebugMembers({
|
||||||
|
super.key,
|
||||||
|
required this.members,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Member> members;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
color: Colors.orange,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: const Text('Members'),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
color: Colors.orange,
|
||||||
|
height: 100,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: members.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final member = members[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Colors.yellow,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(member.user?.name ?? '?'),
|
||||||
|
Text('ID: ${member.user?.id ?? '?'}'),
|
||||||
|
Text('Ban: ${member.banned ? 'T' : 'F'}'),
|
||||||
|
Text('ShBan: ${member.shadowBanned ? 'T' : 'F'}'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
class DebugMutes extends StatelessWidget {
|
||||||
|
const DebugMutes({super.key, required this.mutes});
|
||||||
|
|
||||||
|
final List<Mute> mutes;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
color: Colors.lightBlueAccent,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: const Text('Mutes'),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
color: Colors.lightBlueAccent,
|
||||||
|
height: 80,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: mutes.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final mute = mutes[index];
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Colors.yellow,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('By: ${mute.user.name} (${mute.user.id})'),
|
||||||
|
Text(
|
||||||
|
'Who: ${mute.target.name} (${mute.target.id})',
|
||||||
|
),
|
||||||
|
Text('Exp: ${mute.expires}'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
// ignore_for_file: public_member_api_docs
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:responsive_builder/responsive_builder.dart';
|
import 'package:responsive_builder/responsive_builder.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
import 'package:stream_chat_flutter_example/debug/channel_page.dart';
|
||||||
import 'package:stream_chat_localizations/stream_chat_localizations.dart';
|
import 'package:stream_chat_localizations/stream_chat_localizations.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
@@ -255,6 +258,19 @@ class _ChannelPageState extends State<ChannelPage> {
|
|||||||
widget.onBackPressed!(context);
|
widget.onBackPressed!(context);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
onImageTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return StreamChannel(
|
||||||
|
channel: StreamChannel.of(context).channel,
|
||||||
|
child: const DebugChannelPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
showBackButton: widget.showBackButton,
|
showBackButton: widget.showBackButton,
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
/// WidgetBuilder for [StreamGroupAvatar].
|
||||||
|
typedef StreamGroupAvatarBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
List<Member> members,
|
||||||
|
// ignore: avoid_positional_boolean_parameters
|
||||||
|
bool isSelected,
|
||||||
|
);
|
||||||
|
|
||||||
/// {@template streamGroupAvatar}
|
/// {@template streamGroupAvatar}
|
||||||
/// Widget for constructing a group of images
|
/// Widget for constructing a group of images
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
|
/// WidgetBuilder for [StreamUserAvatar].
|
||||||
|
typedef StreamUserAvatarBuilder = Widget Function(
|
||||||
|
BuildContext context,
|
||||||
|
User user,
|
||||||
|
// ignore: avoid_positional_boolean_parameters
|
||||||
|
bool isSelected,
|
||||||
|
);
|
||||||
|
|
||||||
/// {@template streamUserAvatar}
|
/// {@template streamUserAvatar}
|
||||||
/// Displays a user's avatar.
|
/// Displays a user's avatar.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ class StreamChannelAvatar extends StatelessWidget {
|
|||||||
this.selected = false,
|
this.selected = false,
|
||||||
this.selectionColor,
|
this.selectionColor,
|
||||||
this.selectionThickness = 4,
|
this.selectionThickness = 4,
|
||||||
|
this.ownSpaceAvatarBuilder,
|
||||||
|
this.oneToOneAvatarBuilder,
|
||||||
|
this.groupAvatarBuilder,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
channel.state != null,
|
channel.state != null,
|
||||||
'Channel ${channel.id} is not initialized',
|
'Channel ${channel.id} is not initialized',
|
||||||
@@ -80,6 +83,21 @@ class StreamChannelAvatar extends StatelessWidget {
|
|||||||
/// Thickness of selection image
|
/// Thickness of selection image
|
||||||
final double selectionThickness;
|
final double selectionThickness;
|
||||||
|
|
||||||
|
/// Builder to create avatar for own space channel.
|
||||||
|
///
|
||||||
|
/// Defaults to [StreamUserAvatar].
|
||||||
|
final StreamUserAvatarBuilder? ownSpaceAvatarBuilder;
|
||||||
|
|
||||||
|
/// Builder to create avatar for one to one channel.
|
||||||
|
///
|
||||||
|
/// Defaults to [StreamUserAvatar].
|
||||||
|
final StreamUserAvatarBuilder? oneToOneAvatarBuilder;
|
||||||
|
|
||||||
|
/// Builder to create avatar for group channel.
|
||||||
|
///
|
||||||
|
/// Defaults to [StreamGroupAvatar].
|
||||||
|
final StreamGroupAvatarBuilder? groupAvatarBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final client = channel.client.state;
|
final client = channel.client.state;
|
||||||
@@ -146,15 +164,22 @@ class StreamChannelAvatar extends StatelessWidget {
|
|||||||
return BetterStreamBuilder<User>(
|
return BetterStreamBuilder<User>(
|
||||||
stream: client.currentUserStream.map((it) => it!),
|
stream: client.currentUserStream.map((it) => it!),
|
||||||
initialData: currentUser,
|
initialData: currentUser,
|
||||||
builder: (context, user) => StreamUserAvatar(
|
builder: (context, user) {
|
||||||
borderRadius: borderRadius ?? previewTheme?.borderRadius,
|
final ownSpaceBuilder = ownSpaceAvatarBuilder;
|
||||||
user: user,
|
if (ownSpaceBuilder != null) {
|
||||||
constraints: constraints ?? previewTheme?.constraints,
|
return ownSpaceBuilder(context, user, selected);
|
||||||
onTap: onTap != null ? (_) => onTap!() : null,
|
}
|
||||||
selected: selected,
|
|
||||||
selectionColor: selectionColor ?? colorTheme.accentPrimary,
|
return StreamUserAvatar(
|
||||||
selectionThickness: selectionThickness,
|
borderRadius: borderRadius ?? previewTheme?.borderRadius,
|
||||||
),
|
user: user,
|
||||||
|
constraints: constraints ?? previewTheme?.constraints,
|
||||||
|
onTap: onTap != null ? (_) => onTap!() : null,
|
||||||
|
selected: selected,
|
||||||
|
selectionColor: selectionColor ?? colorTheme.accentPrimary,
|
||||||
|
selectionThickness: selectionThickness,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,18 +194,30 @@ class StreamChannelAvatar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
initialData: member,
|
initialData: member,
|
||||||
builder: (context, member) => StreamUserAvatar(
|
builder: (context, member) {
|
||||||
borderRadius: borderRadius ?? previewTheme?.borderRadius,
|
final oneToOneBuilder = oneToOneAvatarBuilder;
|
||||||
user: member.user!,
|
if (oneToOneBuilder != null) {
|
||||||
constraints: constraints ?? previewTheme?.constraints,
|
return oneToOneBuilder(context, member.user!, selected);
|
||||||
onTap: onTap != null ? (_) => onTap!() : null,
|
}
|
||||||
selected: selected,
|
|
||||||
selectionColor: selectionColor ?? colorTheme.accentPrimary,
|
return StreamUserAvatar(
|
||||||
selectionThickness: selectionThickness,
|
borderRadius: borderRadius ?? previewTheme?.borderRadius,
|
||||||
),
|
user: member.user!,
|
||||||
|
constraints: constraints ?? previewTheme?.constraints,
|
||||||
|
onTap: onTap != null ? (_) => onTap!() : null,
|
||||||
|
selected: selected,
|
||||||
|
selectionColor: selectionColor ?? colorTheme.accentPrimary,
|
||||||
|
selectionThickness: selectionThickness,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final groupBuilder = groupAvatarBuilder;
|
||||||
|
if (groupBuilder != null) {
|
||||||
|
return groupBuilder(context, otherMembers, selected);
|
||||||
|
}
|
||||||
|
|
||||||
// Group conversation
|
// Group conversation
|
||||||
return StreamGroupAvatar(
|
return StreamGroupAvatar(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
|
|||||||
+10
-1
@@ -19,6 +19,7 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
this.showDeleteMessage = true,
|
this.showDeleteMessage = true,
|
||||||
this.showEditMessage = true,
|
this.showEditMessage = true,
|
||||||
this.onReplyTap,
|
this.onReplyTap,
|
||||||
|
this.onConfirmDeleteTap,
|
||||||
this.onThreadReplyTap,
|
this.onThreadReplyTap,
|
||||||
this.showCopyMessage = true,
|
this.showCopyMessage = true,
|
||||||
this.showReplyMessage = true,
|
this.showReplyMessage = true,
|
||||||
@@ -44,6 +45,9 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
/// The action to perform when "reply" is tapped
|
/// The action to perform when "reply" is tapped
|
||||||
final OnMessageTap? onReplyTap;
|
final OnMessageTap? onReplyTap;
|
||||||
|
|
||||||
|
/// The action to perform when delete confirmation button is tapped.
|
||||||
|
final Future<void> Function(Message)? onConfirmDeleteTap;
|
||||||
|
|
||||||
/// Message in focus for actions
|
/// Message in focus for actions
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
@@ -363,7 +367,12 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
if (answer == true) {
|
if (answer == true) {
|
||||||
try {
|
try {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
await StreamChannel.of(context).channel.deleteMessage(widget.message);
|
final onConfirmDeleteTap = widget.onConfirmDeleteTap;
|
||||||
|
if (onConfirmDeleteTap != null) {
|
||||||
|
await onConfirmDeleteTap(widget.message);
|
||||||
|
} else {
|
||||||
|
await StreamChannel.of(context).channel.deleteMessage(widget.message);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_showErrorAlertBottomSheet();
|
_showErrorAlertBottomSheet();
|
||||||
}
|
}
|
||||||
|
|||||||
+93
-87
@@ -693,6 +693,7 @@ Widget mobileAttachmentPickerBuilder({
|
|||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required StreamAttachmentPickerController controller,
|
required StreamAttachmentPickerController controller,
|
||||||
Iterable<AttachmentPickerOption>? customOptions,
|
Iterable<AttachmentPickerOption>? customOptions,
|
||||||
|
List<AttachmentPickerType> allowedTypes = AttachmentPickerType.values,
|
||||||
ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400),
|
ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400),
|
||||||
ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg,
|
ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg,
|
||||||
int attachmentThumbnailQuality = 100,
|
int attachmentThumbnailQuality = 100,
|
||||||
@@ -702,74 +703,76 @@ Widget mobileAttachmentPickerBuilder({
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
onSendAttachments: Navigator.of(context).pop,
|
onSendAttachments: Navigator.of(context).pop,
|
||||||
options: {
|
options: {
|
||||||
if (customOptions != null) ...customOptions,
|
...{
|
||||||
AttachmentPickerOption(
|
if (customOptions != null) ...customOptions,
|
||||||
key: 'gallery-picker',
|
AttachmentPickerOption(
|
||||||
icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(),
|
key: 'gallery-picker',
|
||||||
supportedTypes: [
|
icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(),
|
||||||
AttachmentPickerType.images,
|
supportedTypes: [
|
||||||
AttachmentPickerType.videos,
|
AttachmentPickerType.images,
|
||||||
],
|
AttachmentPickerType.videos,
|
||||||
optionViewBuilder: (context, controller) {
|
],
|
||||||
final selectedIds = controller.value.map((it) => it.id);
|
optionViewBuilder: (context, controller) {
|
||||||
return StreamGalleryPicker(
|
final selectedIds = controller.value.map((it) => it.id);
|
||||||
selectedMediaItems: selectedIds,
|
return StreamGalleryPicker(
|
||||||
mediaThumbnailSize: attachmentThumbnailSize,
|
selectedMediaItems: selectedIds,
|
||||||
mediaThumbnailFormat: attachmentThumbnailFormat,
|
mediaThumbnailSize: attachmentThumbnailSize,
|
||||||
mediaThumbnailQuality: attachmentThumbnailQuality,
|
mediaThumbnailFormat: attachmentThumbnailFormat,
|
||||||
mediaThumbnailScale: attachmentThumbnailScale,
|
mediaThumbnailQuality: attachmentThumbnailQuality,
|
||||||
onMediaItemSelected: (media) async {
|
mediaThumbnailScale: attachmentThumbnailScale,
|
||||||
if (selectedIds.contains(media.id)) {
|
onMediaItemSelected: (media) async {
|
||||||
return controller.removeAssetAttachment(media);
|
if (selectedIds.contains(media.id)) {
|
||||||
}
|
return controller.removeAssetAttachment(media);
|
||||||
return controller.addAssetAttachment(media);
|
}
|
||||||
},
|
return controller.addAssetAttachment(media);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
AttachmentPickerOption(
|
),
|
||||||
key: 'file-picker',
|
AttachmentPickerOption(
|
||||||
icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(),
|
key: 'file-picker',
|
||||||
supportedTypes: [AttachmentPickerType.files],
|
icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(),
|
||||||
optionViewBuilder: (context, controller) {
|
supportedTypes: [AttachmentPickerType.files],
|
||||||
return StreamFilePicker(
|
optionViewBuilder: (context, controller) {
|
||||||
onFilePicked: (file) async {
|
return StreamFilePicker(
|
||||||
if (file != null) await controller.addAttachment(file);
|
onFilePicked: (file) async {
|
||||||
return Navigator.pop(context, controller.value);
|
if (file != null) await controller.addAttachment(file);
|
||||||
},
|
return Navigator.pop(context, controller.value);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
AttachmentPickerOption(
|
),
|
||||||
key: 'image-picker',
|
AttachmentPickerOption(
|
||||||
icon: StreamSvgIcon.camera(size: 36).toIconThemeSvgIcon(),
|
key: 'image-picker',
|
||||||
supportedTypes: [AttachmentPickerType.images],
|
icon: StreamSvgIcon.camera(size: 36).toIconThemeSvgIcon(),
|
||||||
optionViewBuilder: (context, controller) {
|
supportedTypes: [AttachmentPickerType.images],
|
||||||
return StreamImagePicker(
|
optionViewBuilder: (context, controller) {
|
||||||
onImagePicked: (image) async {
|
return StreamImagePicker(
|
||||||
if (image != null) {
|
onImagePicked: (image) async {
|
||||||
await controller.addAttachment(image);
|
if (image != null) {
|
||||||
}
|
await controller.addAttachment(image);
|
||||||
return Navigator.pop(context, controller.value);
|
}
|
||||||
},
|
return Navigator.pop(context, controller.value);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
AttachmentPickerOption(
|
),
|
||||||
key: 'video-picker',
|
AttachmentPickerOption(
|
||||||
icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(),
|
key: 'video-picker',
|
||||||
supportedTypes: [AttachmentPickerType.videos],
|
icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(),
|
||||||
optionViewBuilder: (context, controller) {
|
supportedTypes: [AttachmentPickerType.videos],
|
||||||
return StreamVideoPicker(
|
optionViewBuilder: (context, controller) {
|
||||||
onVideoPicked: (video) async {
|
return StreamVideoPicker(
|
||||||
if (video != null) {
|
onVideoPicked: (video) async {
|
||||||
await controller.addAttachment(video);
|
if (video != null) {
|
||||||
}
|
await controller.addAttachment(video);
|
||||||
return Navigator.pop(context, controller.value);
|
}
|
||||||
},
|
return Navigator.pop(context, controller.value);
|
||||||
);
|
},
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
|
),
|
||||||
|
}..where((option) => option.supportedTypes.every(allowedTypes.contains)),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -779,6 +782,7 @@ Widget webOrDesktopAttachmentPickerBuilder({
|
|||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required StreamAttachmentPickerController controller,
|
required StreamAttachmentPickerController controller,
|
||||||
Iterable<WebOrDesktopAttachmentPickerOption>? customOptions,
|
Iterable<WebOrDesktopAttachmentPickerOption>? customOptions,
|
||||||
|
List<AttachmentPickerType> allowedTypes = AttachmentPickerType.values,
|
||||||
ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400),
|
ThumbnailSize attachmentThumbnailSize = const ThumbnailSize(400, 400),
|
||||||
ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg,
|
ThumbnailFormat attachmentThumbnailFormat = ThumbnailFormat.jpeg,
|
||||||
int attachmentThumbnailQuality = 100,
|
int attachmentThumbnailQuality = 100,
|
||||||
@@ -787,25 +791,27 @@ Widget webOrDesktopAttachmentPickerBuilder({
|
|||||||
return StreamWebOrDesktopAttachmentPickerBottomSheet(
|
return StreamWebOrDesktopAttachmentPickerBottomSheet(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
options: {
|
options: {
|
||||||
if (customOptions != null) ...customOptions,
|
...{
|
||||||
WebOrDesktopAttachmentPickerOption(
|
if (customOptions != null) ...customOptions,
|
||||||
key: 'image-picker',
|
WebOrDesktopAttachmentPickerOption(
|
||||||
type: AttachmentPickerType.images,
|
key: 'image-picker',
|
||||||
icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(),
|
type: AttachmentPickerType.images,
|
||||||
title: context.translations.uploadAPhotoLabel,
|
icon: StreamSvgIcon.pictures(size: 36).toIconThemeSvgIcon(),
|
||||||
),
|
title: context.translations.uploadAPhotoLabel,
|
||||||
WebOrDesktopAttachmentPickerOption(
|
),
|
||||||
key: 'video-picker',
|
WebOrDesktopAttachmentPickerOption(
|
||||||
type: AttachmentPickerType.videos,
|
key: 'video-picker',
|
||||||
icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(),
|
type: AttachmentPickerType.videos,
|
||||||
title: context.translations.uploadAVideoLabel,
|
icon: StreamSvgIcon.record(size: 36).toIconThemeSvgIcon(),
|
||||||
),
|
title: context.translations.uploadAVideoLabel,
|
||||||
WebOrDesktopAttachmentPickerOption(
|
),
|
||||||
key: 'file-picker',
|
WebOrDesktopAttachmentPickerOption(
|
||||||
type: AttachmentPickerType.files,
|
key: 'file-picker',
|
||||||
icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(),
|
type: AttachmentPickerType.files,
|
||||||
title: context.translations.uploadAFileLabel,
|
icon: StreamSvgIcon.files(size: 36).toIconThemeSvgIcon(),
|
||||||
),
|
title: context.translations.uploadAFileLabel,
|
||||||
|
),
|
||||||
|
}.where((option) => option.supportedTypes.every(allowedTypes.contains)),
|
||||||
},
|
},
|
||||||
onOptionTap: (context, controller, option) async {
|
onOptionTap: (context, controller, option) async {
|
||||||
final attachment = await StreamAttachmentHandler.instance.pickFile(
|
final attachment = await StreamAttachmentHandler.instance.pickFile(
|
||||||
|
|||||||
+3
@@ -66,6 +66,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
|||||||
Future<T?> showStreamAttachmentPickerModalBottomSheet<T>({
|
Future<T?> showStreamAttachmentPickerModalBottomSheet<T>({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
Iterable<AttachmentPickerOption>? customOptions,
|
Iterable<AttachmentPickerOption>? customOptions,
|
||||||
|
List<AttachmentPickerType> allowedTypes = AttachmentPickerType.values,
|
||||||
List<Attachment>? initialAttachments,
|
List<Attachment>? initialAttachments,
|
||||||
StreamAttachmentPickerController? controller,
|
StreamAttachmentPickerController? controller,
|
||||||
Color? backgroundColor,
|
Color? backgroundColor,
|
||||||
@@ -117,6 +118,7 @@ Future<T?> showStreamAttachmentPickerModalBottomSheet<T>({
|
|||||||
return webOrDesktopAttachmentPickerBuilder.call(
|
return webOrDesktopAttachmentPickerBuilder.call(
|
||||||
context: context,
|
context: context,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
allowedTypes: allowedTypes,
|
||||||
customOptions: customOptions?.map(
|
customOptions: customOptions?.map(
|
||||||
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
|
WebOrDesktopAttachmentPickerOption.fromAttachmentPickerOption,
|
||||||
),
|
),
|
||||||
@@ -130,6 +132,7 @@ Future<T?> showStreamAttachmentPickerModalBottomSheet<T>({
|
|||||||
return mobileAttachmentPickerBuilder.call(
|
return mobileAttachmentPickerBuilder.call(
|
||||||
context: context,
|
context: context,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
allowedTypes: allowedTypes,
|
||||||
customOptions: customOptions,
|
customOptions: customOptions,
|
||||||
attachmentThumbnailSize: attachmentThumbnailSize,
|
attachmentThumbnailSize: attachmentThumbnailSize,
|
||||||
attachmentThumbnailFormat: attachmentThumbnailFormat,
|
attachmentThumbnailFormat: attachmentThumbnailFormat,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class StreamQuotedMessageWidget extends StatelessWidget {
|
|||||||
this.textLimit = 170,
|
this.textLimit = 170,
|
||||||
this.attachmentThumbnailBuilders,
|
this.attachmentThumbnailBuilders,
|
||||||
this.padding = const EdgeInsets.all(8),
|
this.padding = const EdgeInsets.all(8),
|
||||||
this.onTap,
|
|
||||||
this.onQuotedMessageClear,
|
this.onQuotedMessageClear,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,9 +45,6 @@ class StreamQuotedMessageWidget extends StatelessWidget {
|
|||||||
/// Padding around the widget
|
/// Padding around the widget
|
||||||
final EdgeInsetsGeometry padding;
|
final EdgeInsetsGeometry padding;
|
||||||
|
|
||||||
/// Callback for tap on widget
|
|
||||||
final GestureTapCallback? onTap;
|
|
||||||
|
|
||||||
/// Callback for clearing quoted messages.
|
/// Callback for clearing quoted messages.
|
||||||
final VoidCallback? onQuotedMessageClear;
|
final VoidCallback? onQuotedMessageClear;
|
||||||
|
|
||||||
@@ -77,19 +73,12 @@ class StreamQuotedMessageWidget extends StatelessWidget {
|
|||||||
showOnlineStatus: false,
|
showOnlineStatus: false,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
return MouseRegion(
|
return Padding(
|
||||||
cursor: SystemMouseCursors.click,
|
padding: padding,
|
||||||
child: GestureDetector(
|
child: Row(
|
||||||
behavior: HitTestBehavior.opaque,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
onTap: onTap,
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Padding(
|
children: reverse ? children.reversed.toList() : children,
|
||||||
padding: padding,
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: reverse ? children.reversed.toList() : children,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -258,23 +247,26 @@ class _ParseAttachments extends StatelessWidget {
|
|||||||
child = attachmentBuilder(context, attachment);
|
child = attachmentBuilder(context, attachment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
child = AbsorbPointer(child: child);
|
|
||||||
|
final isImageFile = attachment.title?.mimeType?.type == 'image';
|
||||||
|
final isVideoFile = attachment.title?.mimeType?.type == 'video';
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
shape: attachment.type == 'file'
|
shape: attachment.type == 'file' && (!isImageFile && !isVideoFile)
|
||||||
? null
|
? null
|
||||||
: RoundedRectangleBorder(
|
: RoundedRectangleBorder(
|
||||||
side: const BorderSide(width: 0, color: Colors.transparent),
|
side: const BorderSide(width: 0, color: Colors.transparent),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: child,
|
child: AbsorbPointer(child: child),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
||||||
get _defaultAttachmentBuilder {
|
get _defaultAttachmentBuilder {
|
||||||
return {
|
final builders = <String, QuotedMessageAttachmentThumbnailBuilder>{
|
||||||
'image': (_, attachment) {
|
'image': (_, attachment) {
|
||||||
return StreamImageAttachment(
|
return StreamImageAttachment(
|
||||||
attachment: attachment,
|
attachment: attachment,
|
||||||
@@ -315,16 +307,33 @@ class _ParseAttachments extends StatelessWidget {
|
|||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
'file': (_, attachment) {
|
|
||||||
return SizedBox(
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
child: getFileTypeImage(
|
|
||||||
attachment.extraData['mime_type'] as String?,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
builders['file'] = (_, attachment) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 32,
|
||||||
|
width: 32,
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final isImageFile = attachment.title?.mimeType?.type == 'image';
|
||||||
|
if (isImageFile) {
|
||||||
|
return builders['image']!(context, attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
final isVideoFile = attachment.title?.mimeType?.type == 'video';
|
||||||
|
if (isVideoFile) {
|
||||||
|
return builders['video']!(context, attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFileTypeImage(
|
||||||
|
attachment.extraData['mime_type'] as String?,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return builders;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,12 +122,14 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
this.maxAttachmentSize = kDefaultMaxAttachmentSize,
|
this.maxAttachmentSize = kDefaultMaxAttachmentSize,
|
||||||
this.onError,
|
this.onError,
|
||||||
this.attachmentLimit = 10,
|
this.attachmentLimit = 10,
|
||||||
|
this.allowedAttachmentPickerTypes = AttachmentPickerType.values,
|
||||||
this.onAttachmentLimitExceed,
|
this.onAttachmentLimitExceed,
|
||||||
this.attachmentButtonBuilder,
|
this.attachmentButtonBuilder,
|
||||||
this.commandButtonBuilder,
|
this.commandButtonBuilder,
|
||||||
this.customAutocompleteTriggers = const [],
|
this.customAutocompleteTriggers = const [],
|
||||||
this.mentionAllAppUsers = false,
|
this.mentionAllAppUsers = false,
|
||||||
this.sendButtonBuilder,
|
this.sendButtonBuilder,
|
||||||
|
this.quotedMessageBuilder,
|
||||||
this.shouldKeepFocusAfterMessage,
|
this.shouldKeepFocusAfterMessage,
|
||||||
this.validator = _defaultValidator,
|
this.validator = _defaultValidator,
|
||||||
this.restorationId,
|
this.restorationId,
|
||||||
@@ -143,6 +145,7 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
_defaultClearQuotedMessageKeyPredicate,
|
_defaultClearQuotedMessageKeyPredicate,
|
||||||
this.ogPreviewFilter = _defaultOgPreviewFilter,
|
this.ogPreviewFilter = _defaultOgPreviewFilter,
|
||||||
this.hintGetter = _defaultHintGetter,
|
this.hintGetter = _defaultHintGetter,
|
||||||
|
this.contentInsertionConfiguration,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The predicate used to send a message on desktop/web
|
/// The predicate used to send a message on desktop/web
|
||||||
@@ -233,6 +236,12 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
/// A limit for the no. of attachments that can be sent with a single message.
|
/// A limit for the no. of attachments that can be sent with a single message.
|
||||||
final int attachmentLimit;
|
final int attachmentLimit;
|
||||||
|
|
||||||
|
/// The list of allowed attachment types which can be picked using the
|
||||||
|
/// attachment button.
|
||||||
|
///
|
||||||
|
/// By default, all the attachment types are allowed.
|
||||||
|
final List<AttachmentPickerType> allowedAttachmentPickerTypes;
|
||||||
|
|
||||||
/// A callback for when the [attachmentLimit] is exceeded.
|
/// A callback for when the [attachmentLimit] is exceeded.
|
||||||
///
|
///
|
||||||
/// This will override the default error alert behaviour.
|
/// This will override the default error alert behaviour.
|
||||||
@@ -258,6 +267,9 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
/// Builder for creating send button
|
/// Builder for creating send button
|
||||||
final MessageRelatedBuilder? sendButtonBuilder;
|
final MessageRelatedBuilder? sendButtonBuilder;
|
||||||
|
|
||||||
|
/// Builder for building quoted message
|
||||||
|
final Widget Function(BuildContext, Message)? quotedMessageBuilder;
|
||||||
|
|
||||||
/// Defines if the [StreamMessageInput] loses focuses after a message is sent.
|
/// Defines if the [StreamMessageInput] loses focuses after a message is sent.
|
||||||
/// The default behaviour keeps focus until a command is enabled.
|
/// The default behaviour keeps focus until a command is enabled.
|
||||||
final bool? shouldKeepFocusAfterMessage;
|
final bool? shouldKeepFocusAfterMessage;
|
||||||
@@ -295,6 +307,9 @@ class StreamMessageInput extends StatefulWidget {
|
|||||||
/// Returns the hint text for the message input.
|
/// Returns the hint text for the message input.
|
||||||
final HintGetter hintGetter;
|
final HintGetter hintGetter;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
|
||||||
|
final ContentInsertionConfiguration? contentInsertionConfiguration;
|
||||||
|
|
||||||
static String? _defaultHintGetter(
|
static String? _defaultHintGetter(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
HintType type,
|
HintType type,
|
||||||
@@ -771,8 +786,8 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
|||||||
Future<void> _onAttachmentButtonPressed() async {
|
Future<void> _onAttachmentButtonPressed() async {
|
||||||
final attachments = await showStreamAttachmentPickerModalBottomSheet(
|
final attachments = await showStreamAttachmentPickerModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
allowedTypes: widget.allowedAttachmentPickerTypes,
|
||||||
initialAttachments: _effectiveController.attachments,
|
initialAttachments: _effectiveController.attachments,
|
||||||
useRootNavigator: true,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (attachments != null) {
|
if (attachments != null) {
|
||||||
@@ -860,6 +875,8 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
|||||||
decoration: _getInputDecoration(context),
|
decoration: _getInputDecoration(context),
|
||||||
textCapitalization: widget.textCapitalization,
|
textCapitalization: widget.textCapitalization,
|
||||||
autocorrect: widget.autoCorrect,
|
autocorrect: widget.autoCorrect,
|
||||||
|
contentInsertionConfiguration:
|
||||||
|
widget.contentInsertionConfiguration,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1118,14 +1135,20 @@ class StreamMessageInputState extends State<StreamMessageInput>
|
|||||||
if (!_hasQuotedMessage) return const Offstage();
|
if (!_hasQuotedMessage) return const Offstage();
|
||||||
final containsUrl = _effectiveController.message.quotedMessage!.attachments
|
final containsUrl = _effectiveController.message.quotedMessage!.attachments
|
||||||
.any((element) => element.titleLink != null);
|
.any((element) => element.titleLink != null);
|
||||||
return StreamQuotedMessageWidget(
|
|
||||||
reverse: true,
|
return widget.quotedMessageBuilder?.call(
|
||||||
showBorder: !containsUrl,
|
context,
|
||||||
message: _effectiveController.message.quotedMessage!,
|
_effectiveController.message.quotedMessage!,
|
||||||
messageTheme: _streamChatTheme.otherMessageTheme,
|
) ??
|
||||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
StreamQuotedMessageWidget(
|
||||||
onQuotedMessageClear: widget.onQuotedMessageCleared,
|
reverse: true,
|
||||||
);
|
showBorder: !containsUrl,
|
||||||
|
message: _effectiveController.message.quotedMessage!,
|
||||||
|
messageTheme: _streamChatTheme.otherMessageTheme,
|
||||||
|
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||||
|
onQuotedMessageClear: widget.onQuotedMessageCleared,
|
||||||
|
attachmentThumbnailBuilders: widget.attachmentThumbnailBuilders,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAttachments() {
|
Widget _buildAttachments() {
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
this.restorationId,
|
this.restorationId,
|
||||||
this.scribbleEnabled = true,
|
this.scribbleEnabled = true,
|
||||||
this.enableIMEPersonalizedLearning = true,
|
this.enableIMEPersonalizedLearning = true,
|
||||||
|
this.contentInsertionConfiguration,
|
||||||
}) : assert(obscuringCharacter.length == 1, ''),
|
}) : assert(obscuringCharacter.length == 1, ''),
|
||||||
smartDashesType = smartDashesType ??
|
smartDashesType = smartDashesType ??
|
||||||
(obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
(obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||||
@@ -526,6 +527,9 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
|
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
|
||||||
final bool enableIMEPersonalizedLearning;
|
final bool enableIMEPersonalizedLearning;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.contentInsertionConfiguration}
|
||||||
|
final ContentInsertionConfiguration? contentInsertionConfiguration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_StreamMessageTextFieldState createState() => _StreamMessageTextFieldState();
|
_StreamMessageTextFieldState createState() => _StreamMessageTextFieldState();
|
||||||
|
|
||||||
@@ -622,6 +626,9 @@ class StreamMessageTextField extends StatefulWidget {
|
|||||||
properties.add(DiagnosticsProperty<bool>(
|
properties.add(DiagnosticsProperty<bool>(
|
||||||
'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning,
|
'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning,
|
||||||
defaultValue: true));
|
defaultValue: true));
|
||||||
|
properties.add(DiagnosticsProperty<ContentInsertionConfiguration>(
|
||||||
|
'contentInsertionConfiguration', contentInsertionConfiguration,
|
||||||
|
defaultValue: null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,6 +734,7 @@ class _StreamMessageTextFieldState extends State<StreamMessageTextField>
|
|||||||
restorationId: widget.restorationId,
|
restorationId: widget.restorationId,
|
||||||
scribbleEnabled: widget.scribbleEnabled,
|
scribbleEnabled: widget.scribbleEnabled,
|
||||||
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
|
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
|
||||||
|
contentInsertionConfiguration: widget.contentInsertionConfiguration,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -852,20 +852,25 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
streamChannel!.channel.markRead();
|
streamChannel!.channel.markRead();
|
||||||
}
|
}
|
||||||
|
|
||||||
final index = unreadCount > 0 ? unreadCount + 1 : 0;
|
// If the channel is not up to date, we need to reload it before scrolling
|
||||||
|
// to the end of the list.
|
||||||
if (!_upToDate) {
|
if (!_upToDate) {
|
||||||
_bottomPaginationActive = false;
|
// Reset the pagination variables.
|
||||||
initialAlignment = 0;
|
|
||||||
initialIndex = 0;
|
initialIndex = 0;
|
||||||
|
initialAlignment = 0;
|
||||||
|
_bottomPaginationActive = false;
|
||||||
|
|
||||||
|
// Reload the channel to get the latest messages.
|
||||||
await streamChannel!.reloadChannel();
|
await streamChannel!.reloadChannel();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
// Wait for the frame to be rendered with the updated channel state.
|
||||||
_scrollController!.jumpTo(index: index);
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
});
|
}
|
||||||
} else {
|
|
||||||
|
// Scroll to the end of the list.
|
||||||
|
if (_scrollController?.isAttached == true) {
|
||||||
_scrollController!.scrollTo(
|
_scrollController!.scrollTo(
|
||||||
index: index,
|
index: 0,
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(seconds: 1),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class MessageCard extends StatefulWidget {
|
|||||||
this.borderSide,
|
this.borderSide,
|
||||||
this.borderRadiusGeometry,
|
this.borderRadiusGeometry,
|
||||||
this.textBuilder,
|
this.textBuilder,
|
||||||
|
this.quotedMessageBuilder,
|
||||||
this.onLinkTap,
|
this.onLinkTap,
|
||||||
this.onMentionTap,
|
this.onMentionTap,
|
||||||
this.onQuotedMessageTap,
|
this.onQuotedMessageTap,
|
||||||
@@ -82,6 +83,9 @@ class MessageCard extends StatefulWidget {
|
|||||||
/// {@macro textBuilder}
|
/// {@macro textBuilder}
|
||||||
final Widget Function(BuildContext, Message)? textBuilder;
|
final Widget Function(BuildContext, Message)? textBuilder;
|
||||||
|
|
||||||
|
/// {@macro quotedMessageBuilder}
|
||||||
|
final Widget Function(BuildContext, Message)? quotedMessageBuilder;
|
||||||
|
|
||||||
/// {@macro onLinkTap}
|
/// {@macro onLinkTap}
|
||||||
final void Function(String)? onLinkTap;
|
final void Function(String)? onLinkTap;
|
||||||
|
|
||||||
@@ -129,8 +133,12 @@ class _MessageCardState extends State<MessageCard> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final onQuotedMessageTap = widget.onQuotedMessageTap;
|
||||||
|
final quotedMessageBuilder = widget.quotedMessageBuilder;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
margin: EdgeInsets.symmetric(
|
margin: EdgeInsets.symmetric(
|
||||||
horizontal: (widget.isFailedState ? 15.0 : 0.0) +
|
horizontal: (widget.isFailedState ? 15.0 : 0.0) +
|
||||||
(widget.showUserAvatar == DisplayWidget.gone ? 0 : 4.0),
|
(widget.showUserAvatar == DisplayWidget.gone ? 0 : 4.0),
|
||||||
@@ -150,15 +158,27 @@ class _MessageCardState extends State<MessageCard> {
|
|||||||
maxWidth: widthLimit ?? double.infinity,
|
maxWidth: widthLimit ?? double.infinity,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.hasQuotedMessage)
|
if (widget.hasQuotedMessage)
|
||||||
QuotedMessage(
|
MouseRegion(
|
||||||
reverse: widget.reverse,
|
cursor: SystemMouseCursors.click,
|
||||||
message: widget.message,
|
child: InkWell(
|
||||||
hasNonUrlAttachments: widget.hasNonUrlAttachments,
|
onTap: !widget.message.quotedMessage!.isDeleted &&
|
||||||
onQuotedMessageTap: widget.onQuotedMessageTap,
|
onQuotedMessageTap != null
|
||||||
|
? () => onQuotedMessageTap(widget.message.quotedMessageId)
|
||||||
|
: null,
|
||||||
|
child: quotedMessageBuilder?.call(
|
||||||
|
context,
|
||||||
|
widget.message.quotedMessage!,
|
||||||
|
) ??
|
||||||
|
QuotedMessage(
|
||||||
|
reverse: widget.reverse,
|
||||||
|
message: widget.message,
|
||||||
|
hasNonUrlAttachments: widget.hasNonUrlAttachments,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (widget.hasNonUrlAttachments)
|
if (widget.hasNonUrlAttachments)
|
||||||
ParseAttachments(
|
ParseAttachments(
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
this.showInChannelIndicator = false,
|
this.showInChannelIndicator = false,
|
||||||
this.onReplyTap,
|
this.onReplyTap,
|
||||||
this.onThreadTap,
|
this.onThreadTap,
|
||||||
|
this.onConfirmDeleteTap,
|
||||||
this.showUsername = true,
|
this.showUsername = true,
|
||||||
this.showTimestamp = true,
|
this.showTimestamp = true,
|
||||||
this.showReactions = true,
|
this.showReactions = true,
|
||||||
@@ -78,6 +79,7 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
this.onMessageActions,
|
this.onMessageActions,
|
||||||
this.onShowMessage,
|
this.onShowMessage,
|
||||||
this.userAvatarBuilder,
|
this.userAvatarBuilder,
|
||||||
|
this.quotedMessageBuilder,
|
||||||
this.editMessageInputBuilder,
|
this.editMessageInputBuilder,
|
||||||
this.textBuilder,
|
this.textBuilder,
|
||||||
@Deprecated('''
|
@Deprecated('''
|
||||||
@@ -307,6 +309,11 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final void Function(Message)? onReplyTap;
|
final void Function(Message)? onReplyTap;
|
||||||
|
|
||||||
|
/// {@template onDeleteTap}
|
||||||
|
/// The function called when delete confirmation button is tapped.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final Future<void> Function(Message)? onConfirmDeleteTap;
|
||||||
|
|
||||||
/// {@template editMessageInputBuilder}
|
/// {@template editMessageInputBuilder}
|
||||||
/// Widget builder for edit message layout
|
/// Widget builder for edit message layout
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
@@ -348,6 +355,11 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final Widget Function(BuildContext, User)? userAvatarBuilder;
|
final Widget Function(BuildContext, User)? userAvatarBuilder;
|
||||||
|
|
||||||
|
/// {@template quotedMessageBuilder}
|
||||||
|
/// Widget builder for building quoted message
|
||||||
|
/// {@endtemplate}
|
||||||
|
final Widget Function(BuildContext, Message)? quotedMessageBuilder;
|
||||||
|
|
||||||
/// {@template message}
|
/// {@template message}
|
||||||
/// The message to display.
|
/// The message to display.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
@@ -568,8 +580,10 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
void Function(User)? onMentionTap,
|
void Function(User)? onMentionTap,
|
||||||
void Function(Message)? onThreadTap,
|
void Function(Message)? onThreadTap,
|
||||||
void Function(Message)? onReplyTap,
|
void Function(Message)? onReplyTap,
|
||||||
|
Future<void> Function(Message)? onConfirmDeleteTap,
|
||||||
Widget Function(BuildContext, Message)? editMessageInputBuilder,
|
Widget Function(BuildContext, Message)? editMessageInputBuilder,
|
||||||
Widget Function(BuildContext, Message)? textBuilder,
|
Widget Function(BuildContext, Message)? textBuilder,
|
||||||
|
Widget Function(BuildContext, Message)? quotedMessageBuilder,
|
||||||
@Deprecated('''
|
@Deprecated('''
|
||||||
Use [bottomRowBuilderWithDefaultWidget] instead.
|
Use [bottomRowBuilderWithDefaultWidget] instead.
|
||||||
Will be removed in the next major version.
|
Will be removed in the next major version.
|
||||||
@@ -659,9 +673,11 @@ class StreamMessageWidget extends StatefulWidget {
|
|||||||
onMentionTap: onMentionTap ?? this.onMentionTap,
|
onMentionTap: onMentionTap ?? this.onMentionTap,
|
||||||
onThreadTap: onThreadTap ?? this.onThreadTap,
|
onThreadTap: onThreadTap ?? this.onThreadTap,
|
||||||
onReplyTap: onReplyTap ?? this.onReplyTap,
|
onReplyTap: onReplyTap ?? this.onReplyTap,
|
||||||
|
onConfirmDeleteTap: onConfirmDeleteTap ?? this.onConfirmDeleteTap,
|
||||||
editMessageInputBuilder:
|
editMessageInputBuilder:
|
||||||
editMessageInputBuilder ?? this.editMessageInputBuilder,
|
editMessageInputBuilder ?? this.editMessageInputBuilder,
|
||||||
textBuilder: textBuilder ?? this.textBuilder,
|
textBuilder: textBuilder ?? this.textBuilder,
|
||||||
|
quotedMessageBuilder: quotedMessageBuilder ?? this.quotedMessageBuilder,
|
||||||
bottomRowBuilderWithDefaultWidget: _bottomRowBuilderWithDefaultWidget,
|
bottomRowBuilderWithDefaultWidget: _bottomRowBuilderWithDefaultWidget,
|
||||||
onMessageActions: onMessageActions ?? this.onMessageActions,
|
onMessageActions: onMessageActions ?? this.onMessageActions,
|
||||||
message: message ?? this.message,
|
message: message ?? this.message,
|
||||||
@@ -957,6 +973,7 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
borderSide: widget.borderSide,
|
borderSide: widget.borderSide,
|
||||||
borderRadiusGeometry: widget.borderRadiusGeometry,
|
borderRadiusGeometry: widget.borderRadiusGeometry,
|
||||||
textBuilder: widget.textBuilder,
|
textBuilder: widget.textBuilder,
|
||||||
|
quotedMessageBuilder: widget.quotedMessageBuilder,
|
||||||
onLinkTap: widget.onLinkTap,
|
onLinkTap: widget.onLinkTap,
|
||||||
onMentionTap: widget.onMentionTap,
|
onMentionTap: widget.onMentionTap,
|
||||||
onQuotedMessageTap: widget.onQuotedMessageTap,
|
onQuotedMessageTap: widget.onQuotedMessageTap,
|
||||||
@@ -1098,16 +1115,21 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
),
|
),
|
||||||
onClick: () async {
|
onClick: () async {
|
||||||
Navigator.of(context, rootNavigator: true).pop();
|
Navigator.of(context, rootNavigator: true).pop();
|
||||||
final deleted = await showDialog(
|
final deleted = await showDialog<bool?>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (_) => const DeleteMessageDialog(),
|
builder: (_) => const DeleteMessageDialog(),
|
||||||
);
|
);
|
||||||
if (deleted) {
|
if (deleted == true) {
|
||||||
try {
|
try {
|
||||||
await StreamChannel.of(context)
|
final onConfirmDeleteTap = widget.onConfirmDeleteTap;
|
||||||
.channel
|
if (onConfirmDeleteTap != null) {
|
||||||
.deleteMessage(widget.message);
|
await onConfirmDeleteTap(widget.message);
|
||||||
|
} else {
|
||||||
|
await StreamChannel.of(context)
|
||||||
|
.channel
|
||||||
|
.deleteMessage(widget.message);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -1197,21 +1219,4 @@ class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void retryMessage(BuildContext context) {
|
|
||||||
final channel = StreamChannel.of(context).channel;
|
|
||||||
if (widget.message.status == MessageSendingStatus.failed) {
|
|
||||||
channel.sendMessage(widget.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (widget.message.status == MessageSendingStatus.failed_update) {
|
|
||||||
channel.updateMessage(widget.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (widget.message.status == MessageSendingStatus.failed_delete) {
|
|
||||||
channel.deleteMessage(widget.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class MessageWidgetContent extends StatelessWidget {
|
|||||||
this.onMentionTap,
|
this.onMentionTap,
|
||||||
this.onLinkTap,
|
this.onLinkTap,
|
||||||
this.textBuilder,
|
this.textBuilder,
|
||||||
|
this.quotedMessageBuilder,
|
||||||
@Deprecated('''
|
@Deprecated('''
|
||||||
Use [bottomRowBuilderWithDefaultWidget] instead.
|
Use [bottomRowBuilderWithDefaultWidget] instead.
|
||||||
Will be removed in the next major version.
|
Will be removed in the next major version.
|
||||||
@@ -170,6 +171,9 @@ class MessageWidgetContent extends StatelessWidget {
|
|||||||
/// {@macro textBuilder}
|
/// {@macro textBuilder}
|
||||||
final Widget Function(BuildContext, Message)? textBuilder;
|
final Widget Function(BuildContext, Message)? textBuilder;
|
||||||
|
|
||||||
|
/// {@macro quotedMessageBuilder}
|
||||||
|
final Widget Function(BuildContext, Message)? quotedMessageBuilder;
|
||||||
|
|
||||||
/// {@macro showReactionPickerIndicator}
|
/// {@macro showReactionPickerIndicator}
|
||||||
final bool showReactionPickerIndicator;
|
final bool showReactionPickerIndicator;
|
||||||
|
|
||||||
@@ -351,6 +355,8 @@ class MessageWidgetContent extends StatelessWidget {
|
|||||||
onMentionTap: onMentionTap,
|
onMentionTap: onMentionTap,
|
||||||
onLinkTap: onLinkTap,
|
onLinkTap: onLinkTap,
|
||||||
textBuilder: textBuilder,
|
textBuilder: textBuilder,
|
||||||
|
quotedMessageBuilder:
|
||||||
|
quotedMessageBuilder,
|
||||||
borderRadiusGeometry:
|
borderRadiusGeometry:
|
||||||
borderRadiusGeometry,
|
borderRadiusGeometry,
|
||||||
borderSide: borderSide,
|
borderSide: borderSide,
|
||||||
|
|||||||
@@ -7,63 +7,41 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
|||||||
///
|
///
|
||||||
/// Used in [QuotedMessageCard]. Should not be used elsewhere.
|
/// Used in [QuotedMessageCard]. Should not be used elsewhere.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class QuotedMessage extends StatefulWidget {
|
class QuotedMessage extends StatelessWidget {
|
||||||
/// {@macro quotedMessage}
|
/// {@macro quotedMessage}
|
||||||
const QuotedMessage({
|
const QuotedMessage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.reverse,
|
required this.reverse,
|
||||||
required this.hasNonUrlAttachments,
|
required this.hasNonUrlAttachments,
|
||||||
this.onQuotedMessageTap,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// {@macro message}
|
/// {@macro message}
|
||||||
final Message message;
|
final Message message;
|
||||||
|
|
||||||
/// {@macro onQuotedMessageTap}
|
|
||||||
final OnQuotedMessageTap? onQuotedMessageTap;
|
|
||||||
|
|
||||||
/// {@macro reverse}
|
/// {@macro reverse}
|
||||||
final bool reverse;
|
final bool reverse;
|
||||||
|
|
||||||
/// {@macro hasNonUrlAttachments}
|
/// {@macro hasNonUrlAttachments}
|
||||||
final bool hasNonUrlAttachments;
|
final bool hasNonUrlAttachments;
|
||||||
|
|
||||||
@override
|
|
||||||
State<QuotedMessage> createState() => _QuotedMessageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _QuotedMessageState extends State<QuotedMessage> {
|
|
||||||
late StreamChatState _streamChat;
|
|
||||||
late StreamChatThemeData _streamChatTheme;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
_streamChatTheme = StreamChatTheme.of(context);
|
|
||||||
_streamChat = StreamChat.of(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isMyMessage = widget.message.user?.id == _streamChat.currentUser?.id;
|
final streamChat = StreamChat.of(context);
|
||||||
final onTap = widget.message.quotedMessage?.isDeleted != true &&
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
widget.onQuotedMessageTap != null
|
|
||||||
? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId)
|
final isMyMessage = message.user?.id == streamChat.currentUser?.id;
|
||||||
: null;
|
|
||||||
final chatThemeData = _streamChatTheme;
|
|
||||||
return StreamQuotedMessageWidget(
|
return StreamQuotedMessageWidget(
|
||||||
onTap: onTap,
|
message: message.quotedMessage!,
|
||||||
message: widget.message.quotedMessage!,
|
|
||||||
messageTheme: isMyMessage
|
messageTheme: isMyMessage
|
||||||
? chatThemeData.otherMessageTheme
|
? chatThemeData.otherMessageTheme
|
||||||
: chatThemeData.ownMessageTheme,
|
: chatThemeData.ownMessageTheme,
|
||||||
reverse: widget.reverse,
|
reverse: reverse,
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
right: 8,
|
right: 8,
|
||||||
left: 8,
|
left: 8,
|
||||||
top: 8,
|
top: 8,
|
||||||
bottom: widget.hasNonUrlAttachments ? 8 : 0,
|
bottom: hasNonUrlAttachments ? 8 : 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -202,9 +202,10 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ImageStreamCompleter loadImage(
|
@Deprecated('Will get replaced by loadImage in the next major version.')
|
||||||
|
ImageStreamCompleter loadBuffer(
|
||||||
MediaThumbnailProvider key,
|
MediaThumbnailProvider key,
|
||||||
ImageDecoderCallback decode,
|
DecoderBufferCallback decode,
|
||||||
) {
|
) {
|
||||||
return MultiFrameImageStreamCompleter(
|
return MultiFrameImageStreamCompleter(
|
||||||
codec: _loadAsync(key, decode),
|
codec: _loadAsync(key, decode),
|
||||||
@@ -219,9 +220,10 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will get replaced by loadImage in the next major version.')
|
||||||
Future<ui.Codec> _loadAsync(
|
Future<ui.Codec> _loadAsync(
|
||||||
MediaThumbnailProvider key,
|
MediaThumbnailProvider key,
|
||||||
ImageDecoderCallback decode,
|
DecoderBufferCallback decode,
|
||||||
) async {
|
) async {
|
||||||
assert(key == this, '$key is not $this');
|
assert(key == this, '$key is not $this');
|
||||||
final bytes = await media.thumbnailDataWithSize(
|
final bytes = await media.thumbnailDataWithSize(
|
||||||
|
|||||||
+1
@@ -157,6 +157,7 @@ class StreamUserListTile extends StatelessWidget {
|
|||||||
trailing: selected ? selectedWidget : null,
|
trailing: selected ? selectedWidget : null,
|
||||||
title: title,
|
title: title,
|
||||||
subtitle: subtitle,
|
subtitle: subtitle,
|
||||||
|
tileColor: tileColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat_flutter
|
name: stream_chat_flutter
|
||||||
homepage: https://github.com/GetStream/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.
|
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
|
||||||
version: 6.3.0
|
version: 6.4.0
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ dependencies:
|
|||||||
rxdart: ^0.27.0
|
rxdart: ^0.27.0
|
||||||
share_plus: ^6.3.0
|
share_plus: ^6.3.0
|
||||||
shimmer: ^3.0.0
|
shimmer: ^3.0.0
|
||||||
stream_chat_flutter_core: ^6.3.0
|
stream_chat_flutter_core: ^6.4.0
|
||||||
synchronized: ^3.0.0
|
synchronized: ^3.0.0
|
||||||
thumblr: ^0.0.4
|
thumblr: ^0.0.4
|
||||||
url_launcher: ^6.1.0
|
url_launcher: ^6.1.0
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
## 6.4.0
|
||||||
|
|
||||||
|
- Updated `stream_chat` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat/changelog).
|
||||||
|
|
||||||
## 6.3.0
|
## 6.3.0
|
||||||
|
|
||||||
- Updated `stream_chat` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat/changelog).
|
- Updated `stream_chat` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat/changelog).
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class StreamChannelListEventHandler {
|
|||||||
final channels = [...controller.currentItems];
|
final channels = [...controller.currentItems];
|
||||||
|
|
||||||
final channelIndex = channels.indexWhere((it) => it.cid == channelCid);
|
final channelIndex = channels.indexWhere((it) => it.cid == channelCid);
|
||||||
if (channelIndex <= 0) {
|
if (channelIndex < 0) {
|
||||||
// If the channel is not in the list, It might be hidden.
|
// If the channel is not in the list, It might be hidden.
|
||||||
// So, we just refresh the list.
|
// So, we just refresh the list.
|
||||||
await controller.refresh(resetValue: false);
|
await controller.refresh(resetValue: false);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat_flutter_core
|
name: stream_chat_flutter_core
|
||||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||||
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
|
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
|
||||||
version: 6.3.0
|
version: 6.4.0
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ dependencies:
|
|||||||
freezed_annotation: ^2.0.3
|
freezed_annotation: ^2.0.3
|
||||||
meta: ^1.8.0
|
meta: ^1.8.0
|
||||||
rxdart: ^0.27.0
|
rxdart: ^0.27.0
|
||||||
stream_chat: ^6.3.0
|
stream_chat: ^6.4.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.3.3
|
build_runner: ^2.3.3
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
## 5.4.0
|
||||||
|
|
||||||
|
* Updated `stream_chat_flutter` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat_flutter/changelog).
|
||||||
|
|
||||||
## 5.3.0
|
## 5.3.0
|
||||||
|
|
||||||
* Updated `stream_chat_flutter` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat_flutter/changelog).
|
* Updated `stream_chat_flutter` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat_flutter/changelog).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: stream_chat_localizations
|
name: stream_chat_localizations
|
||||||
description: The Official localizations for Stream Chat Flutter, a service for building chat applications
|
description: The Official localizations for Stream Chat Flutter, a service for building chat applications
|
||||||
version: 5.3.0
|
version: 5.4.0
|
||||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
@@ -14,7 +14,7 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
stream_chat_flutter: ^6.3.0
|
stream_chat_flutter: ^6.4.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
dart_code_metrics: ^5.7.2
|
dart_code_metrics: ^5.7.2
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
## 6.4.0
|
||||||
|
|
||||||
|
- Updated `stream_chat` dependency to [`6.4.0`](https://pub.dev/packages/stream_chat/changelog).
|
||||||
|
|
||||||
## 6.3.0
|
## 6.3.0
|
||||||
|
|
||||||
- Updated `stream_chat` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat/changelog).
|
- Updated `stream_chat` dependency to [`6.3.0`](https://pub.dev/packages/stream_chat/changelog).
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:stream_chat/stream_chat.dart';
|
|||||||
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
|
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
|
||||||
import 'package:stream_chat_persistence/src/entity/messages.dart';
|
import 'package:stream_chat_persistence/src/entity/messages.dart';
|
||||||
import 'package:stream_chat_persistence/src/entity/users.dart';
|
import 'package:stream_chat_persistence/src/entity/users.dart';
|
||||||
|
|
||||||
import 'package:stream_chat_persistence/src/mapper/mapper.dart';
|
import 'package:stream_chat_persistence/src/mapper/mapper.dart';
|
||||||
|
|
||||||
part 'message_dao.g.dart';
|
part 'message_dao.g.dart';
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class DriftChatDatabase extends _$DriftChatDatabase {
|
|||||||
|
|
||||||
// you should bump this number whenever you change or add a table definition.
|
// you should bump this number whenever you change or add a table definition.
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 11;
|
int get schemaVersion => 12;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -53,14 +53,52 @@ class Messages extends Table {
|
|||||||
/// A used command name.
|
/// A used command name.
|
||||||
TextColumn get command => text().nullable()();
|
TextColumn get command => text().nullable()();
|
||||||
|
|
||||||
/// The DateTime when the message was created.
|
/// The DateTime on which the message was created.
|
||||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
///
|
||||||
|
/// Returns the latest between [localCreatedAt] and [remoteCreatedAt].
|
||||||
|
/// If both are null, returns [currentDateAndTime].
|
||||||
|
Expression<DateTime> get createdAt {
|
||||||
|
return coalesce<DateTime>(
|
||||||
|
[localCreatedAt, remoteCreatedAt, currentDateAndTime],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// The DateTime when the message was updated last time.
|
/// The DateTime on which the message was created on the client.
|
||||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
DateTimeColumn get localCreatedAt => dateTime().nullable()();
|
||||||
|
|
||||||
/// The DateTime when the message was deleted.
|
/// The DateTime on which the message was created on the server.
|
||||||
DateTimeColumn get deletedAt => dateTime().nullable()();
|
DateTimeColumn get remoteCreatedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
/// The DateTime on which the message was updated last time.
|
||||||
|
///
|
||||||
|
/// Returns the latest between [localUpdatedAt] and [remoteUpdatedAt].
|
||||||
|
/// If both are null, returns [createdAt].
|
||||||
|
Expression<DateTime> get updatedAt {
|
||||||
|
return coalesce<DateTime>(
|
||||||
|
[localUpdatedAt, remoteUpdatedAt, createdAt],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The DateTime on which the message was updated on the client.
|
||||||
|
DateTimeColumn get localUpdatedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
/// The DateTime on which the message was updated on the server.
|
||||||
|
DateTimeColumn get remoteUpdatedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
/// The DateTime on which the message was deleted.
|
||||||
|
///
|
||||||
|
/// Returns the latest between [localDeletedAt] and [remoteDeletedAt].
|
||||||
|
Expression<DateTime> get deletedAt {
|
||||||
|
return coalesce<DateTime>(
|
||||||
|
[localDeletedAt, remoteDeletedAt],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The DateTime on which the message was deleted on the client.
|
||||||
|
DateTimeColumn get localDeletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
/// The DateTime on which the message was deleted on the server.
|
||||||
|
DateTimeColumn get remoteDeletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
/// Id of the User who sent the message
|
/// Id of the User who sent the message
|
||||||
TextColumn get userId => text().nullable()();
|
TextColumn get userId => text().nullable()();
|
||||||
|
|||||||
@@ -21,9 +21,13 @@ extension MessageEntityX on MessageEntity {
|
|||||||
final json = jsonDecode(it);
|
final json = jsonDecode(it);
|
||||||
return Attachment.fromData(json);
|
return Attachment.fromData(json);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
createdAt: createdAt,
|
|
||||||
extraData: extraData ?? <String, Object>{},
|
extraData: extraData ?? <String, Object>{},
|
||||||
updatedAt: updatedAt,
|
createdAt: remoteCreatedAt,
|
||||||
|
localCreatedAt: localCreatedAt,
|
||||||
|
updatedAt: remoteUpdatedAt,
|
||||||
|
localUpdatedAt: localUpdatedAt,
|
||||||
|
deletedAt: remoteDeletedAt,
|
||||||
|
localDeletedAt: localDeletedAt,
|
||||||
id: id,
|
id: id,
|
||||||
type: type,
|
type: type,
|
||||||
status: status,
|
status: status,
|
||||||
@@ -37,7 +41,6 @@ extension MessageEntityX on MessageEntity {
|
|||||||
showInChannel: showInChannel,
|
showInChannel: showInChannel,
|
||||||
text: messageText,
|
text: messageText,
|
||||||
user: user,
|
user: user,
|
||||||
deletedAt: deletedAt,
|
|
||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
pinnedAt: pinnedAt,
|
pinnedAt: pinnedAt,
|
||||||
pinExpires: pinExpires,
|
pinExpires: pinExpires,
|
||||||
@@ -59,7 +62,8 @@ extension MessageX on Message {
|
|||||||
parentId: parentId,
|
parentId: parentId,
|
||||||
quotedMessageId: quotedMessageId,
|
quotedMessageId: quotedMessageId,
|
||||||
command: command,
|
command: command,
|
||||||
createdAt: createdAt,
|
remoteCreatedAt: remoteCreatedAt,
|
||||||
|
localCreatedAt: localCreatedAt,
|
||||||
shadowed: shadowed,
|
shadowed: shadowed,
|
||||||
showInChannel: showInChannel,
|
showInChannel: showInChannel,
|
||||||
replyCount: replyCount,
|
replyCount: replyCount,
|
||||||
@@ -67,10 +71,12 @@ extension MessageX on Message {
|
|||||||
reactionCounts: reactionCounts,
|
reactionCounts: reactionCounts,
|
||||||
mentionedUsers: mentionedUsers.map(jsonEncode).toList(),
|
mentionedUsers: mentionedUsers.map(jsonEncode).toList(),
|
||||||
status: status,
|
status: status,
|
||||||
updatedAt: updatedAt,
|
remoteUpdatedAt: remoteUpdatedAt,
|
||||||
|
localUpdatedAt: localUpdatedAt,
|
||||||
extraData: extraData,
|
extraData: extraData,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
deletedAt: deletedAt,
|
remoteDeletedAt: remoteDeletedAt,
|
||||||
|
localDeletedAt: localDeletedAt,
|
||||||
messageText: text,
|
messageText: text,
|
||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
pinnedAt: pinnedAt,
|
pinnedAt: pinnedAt,
|
||||||
|
|||||||
@@ -21,9 +21,13 @@ extension PinnedMessageEntityX on PinnedMessageEntity {
|
|||||||
final json = jsonDecode(it);
|
final json = jsonDecode(it);
|
||||||
return Attachment.fromData(json);
|
return Attachment.fromData(json);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
createdAt: createdAt,
|
|
||||||
extraData: extraData ?? <String, Object>{},
|
extraData: extraData ?? <String, Object>{},
|
||||||
updatedAt: updatedAt,
|
createdAt: remoteCreatedAt,
|
||||||
|
localCreatedAt: localCreatedAt,
|
||||||
|
updatedAt: remoteUpdatedAt,
|
||||||
|
localUpdatedAt: localUpdatedAt,
|
||||||
|
deletedAt: remoteDeletedAt,
|
||||||
|
localDeletedAt: localDeletedAt,
|
||||||
id: id,
|
id: id,
|
||||||
type: type,
|
type: type,
|
||||||
status: status,
|
status: status,
|
||||||
@@ -37,7 +41,6 @@ extension PinnedMessageEntityX on PinnedMessageEntity {
|
|||||||
showInChannel: showInChannel,
|
showInChannel: showInChannel,
|
||||||
text: messageText,
|
text: messageText,
|
||||||
user: user,
|
user: user,
|
||||||
deletedAt: deletedAt,
|
|
||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
pinnedAt: pinnedAt,
|
pinnedAt: pinnedAt,
|
||||||
pinExpires: pinExpires,
|
pinExpires: pinExpires,
|
||||||
@@ -60,7 +63,8 @@ extension PMessageX on Message {
|
|||||||
parentId: parentId,
|
parentId: parentId,
|
||||||
quotedMessageId: quotedMessageId,
|
quotedMessageId: quotedMessageId,
|
||||||
command: command,
|
command: command,
|
||||||
createdAt: createdAt,
|
remoteCreatedAt: remoteCreatedAt,
|
||||||
|
localCreatedAt: localCreatedAt,
|
||||||
shadowed: shadowed,
|
shadowed: shadowed,
|
||||||
showInChannel: showInChannel,
|
showInChannel: showInChannel,
|
||||||
replyCount: replyCount,
|
replyCount: replyCount,
|
||||||
@@ -68,10 +72,12 @@ extension PMessageX on Message {
|
|||||||
reactionCounts: reactionCounts,
|
reactionCounts: reactionCounts,
|
||||||
mentionedUsers: mentionedUsers.map(jsonEncode).toList(),
|
mentionedUsers: mentionedUsers.map(jsonEncode).toList(),
|
||||||
status: status,
|
status: status,
|
||||||
updatedAt: updatedAt,
|
remoteUpdatedAt: remoteUpdatedAt,
|
||||||
|
localUpdatedAt: localUpdatedAt,
|
||||||
extraData: extraData,
|
extraData: extraData,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
deletedAt: deletedAt,
|
remoteDeletedAt: remoteDeletedAt,
|
||||||
|
localDeletedAt: localDeletedAt,
|
||||||
messageText: text,
|
messageText: text,
|
||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
pinnedAt: pinnedAt,
|
pinnedAt: pinnedAt,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat_persistence
|
name: stream_chat_persistence
|
||||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||||
description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter.
|
description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter.
|
||||||
version: 6.3.0
|
version: 6.4.0
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ dependencies:
|
|||||||
path: ^1.8.2
|
path: ^1.8.2
|
||||||
path_provider: ^2.0.1
|
path_provider: ^2.0.1
|
||||||
sqlite3_flutter_libs: ^0.5.0
|
sqlite3_flutter_libs: ^0.5.0
|
||||||
stream_chat: ^6.3.0
|
stream_chat: ^6.4.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.3.3
|
build_runner: ^2.3.3
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ void main() {
|
|||||||
parentId: 'testParentId',
|
parentId: 'testParentId',
|
||||||
quotedMessageId: quotedMessage.id,
|
quotedMessageId: quotedMessage.id,
|
||||||
command: 'testCommand',
|
command: 'testCommand',
|
||||||
createdAt: DateTime.now(),
|
localCreatedAt: DateTime.now(),
|
||||||
|
remoteCreatedAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||||
shadowed: math.Random().nextBool(),
|
shadowed: math.Random().nextBool(),
|
||||||
showInChannel: math.Random().nextBool(),
|
showInChannel: math.Random().nextBool(),
|
||||||
replyCount: 33,
|
replyCount: 33,
|
||||||
@@ -52,10 +53,12 @@ void main() {
|
|||||||
jsonEncode(User(id: 'testuser')),
|
jsonEncode(User(id: 'testuser')),
|
||||||
],
|
],
|
||||||
status: MessageSendingStatus.sent,
|
status: MessageSendingStatus.sent,
|
||||||
updatedAt: DateTime.now(),
|
localUpdatedAt: DateTime.now(),
|
||||||
|
remoteUpdatedAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||||
extraData: {'extra_test_data': 'extraData'},
|
extraData: {'extra_test_data': 'extraData'},
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
deletedAt: DateTime.now(),
|
localDeletedAt: DateTime.now(),
|
||||||
|
remoteDeletedAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||||
messageText: 'Hello',
|
messageText: 'Hello',
|
||||||
pinned: true,
|
pinned: true,
|
||||||
pinExpires: DateTime.now().toUtc(),
|
pinExpires: DateTime.now().toUtc(),
|
||||||
@@ -81,7 +84,8 @@ void main() {
|
|||||||
expect(message.parentId, entity.parentId);
|
expect(message.parentId, entity.parentId);
|
||||||
expect(message.quotedMessageId, entity.quotedMessageId);
|
expect(message.quotedMessageId, entity.quotedMessageId);
|
||||||
expect(message.command, entity.command);
|
expect(message.command, entity.command);
|
||||||
expect(message.createdAt, isSameDateAs(entity.createdAt));
|
expect(message.localCreatedAt, isSameDateAs(entity.localCreatedAt));
|
||||||
|
expect(message.remoteCreatedAt, isSameDateAs(entity.remoteCreatedAt));
|
||||||
expect(message.shadowed, entity.shadowed);
|
expect(message.shadowed, entity.shadowed);
|
||||||
expect(message.showInChannel, entity.showInChannel);
|
expect(message.showInChannel, entity.showInChannel);
|
||||||
for (var i = 0; i < message.mentionedUsers.length; i++) {
|
for (var i = 0; i < message.mentionedUsers.length; i++) {
|
||||||
@@ -93,10 +97,12 @@ void main() {
|
|||||||
expect(message.reactionScores, entity.reactionScores);
|
expect(message.reactionScores, entity.reactionScores);
|
||||||
expect(message.reactionCounts, entity.reactionCounts);
|
expect(message.reactionCounts, entity.reactionCounts);
|
||||||
expect(message.status, entity.status);
|
expect(message.status, entity.status);
|
||||||
expect(message.updatedAt, isSameDateAs(entity.updatedAt));
|
expect(message.localUpdatedAt, isSameDateAs(entity.localUpdatedAt));
|
||||||
|
expect(message.remoteUpdatedAt, isSameDateAs(entity.remoteUpdatedAt));
|
||||||
expect(message.extraData, entity.extraData);
|
expect(message.extraData, entity.extraData);
|
||||||
expect(message.user!.id, entity.userId);
|
expect(message.user!.id, entity.userId);
|
||||||
expect(message.deletedAt, isSameDateAs(entity.deletedAt));
|
expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt));
|
||||||
|
expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt));
|
||||||
expect(message.text, entity.messageText);
|
expect(message.text, entity.messageText);
|
||||||
expect(message.pinned, entity.pinned);
|
expect(message.pinned, entity.pinned);
|
||||||
expect(message.pinExpires, isSameDateAs(entity.pinExpires));
|
expect(message.pinExpires, isSameDateAs(entity.pinExpires));
|
||||||
@@ -144,7 +150,8 @@ void main() {
|
|||||||
parentId: 'testParentId',
|
parentId: 'testParentId',
|
||||||
quotedMessageId: quotedMessage.id,
|
quotedMessageId: quotedMessage.id,
|
||||||
command: 'testCommand',
|
command: 'testCommand',
|
||||||
createdAt: DateTime.now(),
|
localCreatedAt: DateTime.now(),
|
||||||
|
createdAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||||
shadowed: math.Random().nextBool(),
|
shadowed: math.Random().nextBool(),
|
||||||
showInChannel: math.Random().nextBool(),
|
showInChannel: math.Random().nextBool(),
|
||||||
replyCount: 33,
|
replyCount: 33,
|
||||||
@@ -157,10 +164,12 @@ void main() {
|
|||||||
(prev, curr) =>
|
(prev, curr) =>
|
||||||
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
|
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
|
||||||
),
|
),
|
||||||
updatedAt: DateTime.now(),
|
localUpdatedAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||||
extraData: const {'extra_test_data': 'extraData'},
|
extraData: const {'extra_test_data': 'extraData'},
|
||||||
user: user,
|
user: user,
|
||||||
deletedAt: DateTime.now(),
|
localDeletedAt: DateTime.now(),
|
||||||
|
deletedAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||||
text: 'Hello',
|
text: 'Hello',
|
||||||
pinned: true,
|
pinned: true,
|
||||||
pinExpires: DateTime.now(),
|
pinExpires: DateTime.now(),
|
||||||
@@ -179,7 +188,8 @@ void main() {
|
|||||||
expect(entity.parentId, message.parentId);
|
expect(entity.parentId, message.parentId);
|
||||||
expect(entity.quotedMessageId, message.quotedMessageId);
|
expect(entity.quotedMessageId, message.quotedMessageId);
|
||||||
expect(entity.command, message.command);
|
expect(entity.command, message.command);
|
||||||
expect(entity.createdAt, isSameDateAs(message.createdAt));
|
expect(entity.localCreatedAt, isSameDateAs(message.localCreatedAt));
|
||||||
|
expect(entity.remoteCreatedAt, isSameDateAs(message.remoteCreatedAt));
|
||||||
expect(entity.shadowed, message.shadowed);
|
expect(entity.shadowed, message.shadowed);
|
||||||
expect(entity.showInChannel, message.showInChannel);
|
expect(entity.showInChannel, message.showInChannel);
|
||||||
expect(entity.replyCount, message.replyCount);
|
expect(entity.replyCount, message.replyCount);
|
||||||
@@ -188,10 +198,12 @@ void main() {
|
|||||||
expect(entity.reactionScores, message.reactionScores);
|
expect(entity.reactionScores, message.reactionScores);
|
||||||
expect(entity.reactionCounts, message.reactionCounts);
|
expect(entity.reactionCounts, message.reactionCounts);
|
||||||
expect(entity.status, message.status);
|
expect(entity.status, message.status);
|
||||||
expect(entity.updatedAt, isSameDateAs(message.updatedAt));
|
expect(entity.localUpdatedAt, isSameDateAs(message.localUpdatedAt));
|
||||||
|
expect(entity.remoteUpdatedAt, isSameDateAs(message.remoteUpdatedAt));
|
||||||
expect(entity.extraData, message.extraData);
|
expect(entity.extraData, message.extraData);
|
||||||
expect(entity.userId, message.user!.id);
|
expect(entity.userId, message.user!.id);
|
||||||
expect(entity.deletedAt, isSameDateAs(message.deletedAt));
|
expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt));
|
||||||
|
expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt));
|
||||||
expect(entity.messageText, message.text);
|
expect(entity.messageText, message.text);
|
||||||
expect(entity.pinned, message.pinned);
|
expect(entity.pinned, message.pinned);
|
||||||
expect(entity.pinExpires, isSameDateAs(message.pinExpires));
|
expect(entity.pinExpires, isSameDateAs(message.pinExpires));
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ void main() {
|
|||||||
parentId: 'testParentId',
|
parentId: 'testParentId',
|
||||||
quotedMessageId: quotedMessage.id,
|
quotedMessageId: quotedMessage.id,
|
||||||
command: 'testCommand',
|
command: 'testCommand',
|
||||||
createdAt: DateTime.now(),
|
localCreatedAt: DateTime.now(),
|
||||||
|
remoteCreatedAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||||
shadowed: math.Random().nextBool(),
|
shadowed: math.Random().nextBool(),
|
||||||
showInChannel: math.Random().nextBool(),
|
showInChannel: math.Random().nextBool(),
|
||||||
replyCount: 33,
|
replyCount: 33,
|
||||||
@@ -48,12 +49,16 @@ void main() {
|
|||||||
(prev, curr) =>
|
(prev, curr) =>
|
||||||
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
|
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
|
||||||
),
|
),
|
||||||
mentionedUsers: [],
|
mentionedUsers: [
|
||||||
|
jsonEncode(User(id: 'testuser')),
|
||||||
|
],
|
||||||
status: MessageSendingStatus.sent,
|
status: MessageSendingStatus.sent,
|
||||||
updatedAt: DateTime.now(),
|
localUpdatedAt: DateTime.now(),
|
||||||
|
remoteUpdatedAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||||
extraData: {'extra_test_data': 'extraData'},
|
extraData: {'extra_test_data': 'extraData'},
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
deletedAt: DateTime.now(),
|
localDeletedAt: DateTime.now(),
|
||||||
|
remoteDeletedAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||||
messageText: 'Hello',
|
messageText: 'Hello',
|
||||||
pinned: true,
|
pinned: true,
|
||||||
pinExpires: DateTime.now().toUtc(),
|
pinExpires: DateTime.now().toUtc(),
|
||||||
@@ -79,17 +84,25 @@ void main() {
|
|||||||
expect(message.parentId, entity.parentId);
|
expect(message.parentId, entity.parentId);
|
||||||
expect(message.quotedMessageId, entity.quotedMessageId);
|
expect(message.quotedMessageId, entity.quotedMessageId);
|
||||||
expect(message.command, entity.command);
|
expect(message.command, entity.command);
|
||||||
expect(message.createdAt, isSameDateAs(entity.createdAt));
|
expect(message.localCreatedAt, isSameDateAs(entity.localCreatedAt));
|
||||||
|
expect(message.remoteCreatedAt, isSameDateAs(entity.remoteCreatedAt));
|
||||||
expect(message.shadowed, entity.shadowed);
|
expect(message.shadowed, entity.shadowed);
|
||||||
expect(message.showInChannel, entity.showInChannel);
|
expect(message.showInChannel, entity.showInChannel);
|
||||||
|
for (var i = 0; i < message.mentionedUsers.length; i++) {
|
||||||
|
final entityMentionedUser =
|
||||||
|
User.fromJson(jsonDecode(entity.mentionedUsers[i]));
|
||||||
|
expect(message.mentionedUsers[i].id, entityMentionedUser.id);
|
||||||
|
}
|
||||||
expect(message.replyCount, entity.replyCount);
|
expect(message.replyCount, entity.replyCount);
|
||||||
expect(message.reactionScores, entity.reactionScores);
|
expect(message.reactionScores, entity.reactionScores);
|
||||||
expect(message.reactionCounts, entity.reactionCounts);
|
expect(message.reactionCounts, entity.reactionCounts);
|
||||||
expect(message.status, entity.status);
|
expect(message.status, entity.status);
|
||||||
expect(message.updatedAt, isSameDateAs(entity.updatedAt));
|
expect(message.localUpdatedAt, isSameDateAs(entity.localUpdatedAt));
|
||||||
|
expect(message.remoteUpdatedAt, isSameDateAs(entity.remoteUpdatedAt));
|
||||||
expect(message.extraData, entity.extraData);
|
expect(message.extraData, entity.extraData);
|
||||||
expect(message.user!.id, entity.userId);
|
expect(message.user!.id, entity.userId);
|
||||||
expect(message.deletedAt, isSameDateAs(entity.deletedAt));
|
expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt));
|
||||||
|
expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt));
|
||||||
expect(message.text, entity.messageText);
|
expect(message.text, entity.messageText);
|
||||||
expect(message.pinned, entity.pinned);
|
expect(message.pinned, entity.pinned);
|
||||||
expect(message.pinExpires, isSameDateAs(entity.pinExpires));
|
expect(message.pinExpires, isSameDateAs(entity.pinExpires));
|
||||||
@@ -108,7 +121,7 @@ void main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toPinnedEntity should map message into PinnedMessageEntity', () {
|
test('toEntity should map message into MessageEntity', () {
|
||||||
const cid = 'testCid';
|
const cid = 'testCid';
|
||||||
final user = User(id: 'testUserId');
|
final user = User(id: 'testUserId');
|
||||||
final quotedMessage = Message(id: 'testQuotedMessageId');
|
final quotedMessage = Message(id: 'testQuotedMessageId');
|
||||||
@@ -137,20 +150,26 @@ void main() {
|
|||||||
parentId: 'testParentId',
|
parentId: 'testParentId',
|
||||||
quotedMessageId: quotedMessage.id,
|
quotedMessageId: quotedMessage.id,
|
||||||
command: 'testCommand',
|
command: 'testCommand',
|
||||||
createdAt: DateTime.now(),
|
localCreatedAt: DateTime.now(),
|
||||||
|
createdAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||||
shadowed: math.Random().nextBool(),
|
shadowed: math.Random().nextBool(),
|
||||||
showInChannel: math.Random().nextBool(),
|
showInChannel: math.Random().nextBool(),
|
||||||
replyCount: 33,
|
replyCount: 33,
|
||||||
|
mentionedUsers: [
|
||||||
|
User(id: 'testuser'),
|
||||||
|
],
|
||||||
reactionScores: {for (final r in reactions) r.type: r.score},
|
reactionScores: {for (final r in reactions) r.type: r.score},
|
||||||
reactionCounts: reactions.fold(
|
reactionCounts: reactions.fold(
|
||||||
{},
|
{},
|
||||||
(prev, curr) =>
|
(prev, curr) =>
|
||||||
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
|
prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1),
|
||||||
),
|
),
|
||||||
updatedAt: DateTime.now(),
|
localUpdatedAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||||
extraData: const {'extra_test_data': 'extraData'},
|
extraData: const {'extra_test_data': 'extraData'},
|
||||||
user: user,
|
user: user,
|
||||||
deletedAt: DateTime.now(),
|
localDeletedAt: DateTime.now(),
|
||||||
|
deletedAt: DateTime.now().add(const Duration(seconds: 1)),
|
||||||
text: 'Hello',
|
text: 'Hello',
|
||||||
pinned: true,
|
pinned: true,
|
||||||
pinExpires: DateTime.now(),
|
pinExpires: DateTime.now(),
|
||||||
@@ -169,17 +188,22 @@ void main() {
|
|||||||
expect(entity.parentId, message.parentId);
|
expect(entity.parentId, message.parentId);
|
||||||
expect(entity.quotedMessageId, message.quotedMessageId);
|
expect(entity.quotedMessageId, message.quotedMessageId);
|
||||||
expect(entity.command, message.command);
|
expect(entity.command, message.command);
|
||||||
expect(entity.createdAt, isSameDateAs(message.createdAt));
|
expect(entity.localCreatedAt, isSameDateAs(message.localCreatedAt));
|
||||||
|
expect(entity.remoteCreatedAt, isSameDateAs(message.remoteCreatedAt));
|
||||||
expect(entity.shadowed, message.shadowed);
|
expect(entity.shadowed, message.shadowed);
|
||||||
expect(entity.showInChannel, message.showInChannel);
|
expect(entity.showInChannel, message.showInChannel);
|
||||||
expect(entity.replyCount, message.replyCount);
|
expect(entity.replyCount, message.replyCount);
|
||||||
|
expect(
|
||||||
|
entity.mentionedUsers, message.mentionedUsers.map(jsonEncode).toList());
|
||||||
expect(entity.reactionScores, message.reactionScores);
|
expect(entity.reactionScores, message.reactionScores);
|
||||||
expect(entity.reactionCounts, message.reactionCounts);
|
expect(entity.reactionCounts, message.reactionCounts);
|
||||||
expect(entity.status, message.status);
|
expect(entity.status, message.status);
|
||||||
expect(entity.updatedAt, isSameDateAs(message.updatedAt));
|
expect(entity.localUpdatedAt, isSameDateAs(message.localUpdatedAt));
|
||||||
|
expect(entity.remoteUpdatedAt, isSameDateAs(message.remoteUpdatedAt));
|
||||||
expect(entity.extraData, message.extraData);
|
expect(entity.extraData, message.extraData);
|
||||||
expect(entity.userId, message.user!.id);
|
expect(entity.userId, message.user!.id);
|
||||||
expect(entity.deletedAt, isSameDateAs(message.deletedAt));
|
expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt));
|
||||||
|
expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt));
|
||||||
expect(entity.messageText, message.text);
|
expect(entity.messageText, message.text);
|
||||||
expect(entity.pinned, message.pinned);
|
expect(entity.pinned, message.pinned);
|
||||||
expect(entity.pinExpires, isSameDateAs(message.pinExpires));
|
expect(entity.pinExpires, isSameDateAs(message.pinExpires));
|
||||||
|
|||||||
Reference in New Issue
Block a user